Repository navigation
Expand file tree
/
Copy pathsession.py
More file actions
147 lines (127 loc) · 7.57 KB
/
Copy pathsession.py
File metadata and controls
147 lines (127 loc) · 7.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""Ephemeral Agent Run — assembles working memory for each turn.
The inner box on the whiteboard: everything here is rebuilt per run and thrown
away. What persists lives in waku/memory. Working memory =
system prompt (SOUL.md) ← who Prometheus is
+ durable facts & episodes ← what Prometheus remembers (gated!)
+ current chat history ← this conversation
+ the user's new message
"""
from __future__ import annotations
from prometheus.config import Settings
from prometheus.memory import tool_note
DEFAULT_SOUL = """\
You are Prometheus, a personal assistant running locally on your user's laptop.
You are concise, warm, and proactive. You remember what your user tells you.
Rules:
- When the user wants to schedule something, use create_event. Resolve relative
dates and times ("next Tuesday", "in 30 minutes") to ISO timestamps yourself;
the current date and time are given below — trust them, never ask the user
what time it is.
- When the user asks what's on their calendar (a day, a week, "yesterday"), use
list_events — you CAN read the calendar, not just write to it.
- When the user shares something durable about a person, project, or preference,
use save_note to remember it.
- When asked to message someone, use send_message (it drafts to a local outbox).
- If memory context is provided below, trust it — it came from your own store.
- Call each tool at most once per request. Your history shows [tools used: ...]
lines for past turns — if a tool already ran, do NOT run it again; answer
from that record instead.
- Be honest about where things live. Every tool's output states exactly where
its artifact landed (local calendar file, Apple Calendar, memory database at
~/.prometheus/state.db, with each fact also a file in ~/.prometheus/memory/) — relay
that truthfully, and never claim something synced anywhere the tool output
doesn't say.
- You can manage your own memory: use manage_memory to correct or forget facts,
update_soul to save a standing preference the user gives you, and create_skill
to save a repeatable workflow the user teaches you (only after they say yes).
"""
def load_soul(settings: Settings) -> str:
"""SOUL.md is the editable persona file, created on first run. Changing it
changes who your Prometheus is — that's procedural memory at its simplest."""
soul_path = settings.home / "SOUL.md"
if not soul_path.exists():
soul_path.write_text(DEFAULT_SOUL, encoding="utf-8")
return soul_path.read_text(encoding="utf-8")
class Session:
"""Holds one conversation: the chat history plus the recipe for the
system prompt. One Session per gateway connection."""
def __init__(self, settings: Settings, memory=None, session_id: str = "default"):
self.settings = settings
self.memory = memory # waku.memory.Memory (None until Phase-2 wiring)
self.session_id = session_id
self.history: list[dict] = []
# what the retrieval gate found for the latest build_system, so
# consolidation can tell a fact the turn read from one it learned
self.retrieved = ""
def build_system(self, user_message: str, notify=None) -> str:
from datetime import datetime
# The agent runs on your laptop, so it should know your laptop's clock.
# Local time WITH the timezone name — enough to resolve "in 30 minutes".
now = datetime.now().astimezone()
parts = [load_soul(self.settings),
f"\nRight now it is {now:%A, %Y-%m-%d %H:%M} ({now:%Z}, UTC{now:%z}).",
# the agent should know its own brain — "what model are you?"
# is the first question every curious user asks
(f"Your model: you are running on '{self.settings.model}' via the "
f"'{self.settings.provider}' provider, inside Prometheus, a local-first "
f"open-source agent harness (github.com/Engine-User/Prometheus-AI-Agent).")]
# Spec 009 E: a call the deployment refuses is a wasted step and, on a
# metered tenant, a wasted turn. Spec 014: with the person's own treg
# key the relay is not in the path, so treg's tools are all theirs.
from prometheus.tools.treg import unavailable
if unavailable_tools := unavailable(self.settings.unavailable_tools):
parts.append("\nNot available here, so never call them: "
+ ", ".join(unavailable_tools) + ".")
if self.memory is not None:
# Hero moment #1: a cheap judge decides IF we retrieve at all —
# default-on retrieval is slow and biases answers (see
# memory/retrieval_gate.py for the why).
retrieved = self.memory.gated_retrieve(user_message, notify=notify)
self.retrieved = retrieved
if retrieved:
parts.append("\nRelevant memory:\n" + retrieved)
skills = self.memory.matching_skills(user_message)
if skills:
parts.append("\nRelevant skill instructions:\n" + skills)
return "\n".join(parts)
def add_exchange(self, user_message: str, reply: str, tool_calls: list | None = None,
source: str = "cli", meta: dict | None = None) -> int | None:
"""Record the turn in history (working memory) and, if memory is wired,
in the chat log (so consolidation can distill it later). Returns the
chat log's assistant row id, or None without memory.
Tool activity is folded into the assistant's history entry as a compact
[tools used: ...] line. Without it, the model forgets it already acted
and happily re-runs the same tool next turn (the triple-booked-meeting
bug from the first live test). Compact means name, short args and the
output cut to ~300 characters, never the full output: this record is
re-sent to the model on every later turn and kept in chat_log, so a
full search result here cost tokens on every turn after it and drew a
block thousands of lines tall in a reopened thread (waku/memory/tool_note.py)."""
record = reply
if tool_calls:
record = f"{reply}\n{tool_note.note(tool_calls)}"
self.history.append({"role": "user", "content": user_message})
self.history.append({"role": "assistant", "content": record})
if self.memory is None:
return None
return self.memory.log_chat(user_message, record, session_id=self.session_id,
source=source, meta=meta)
# ---- session lifecycle (the "New chat" / history feature)
# A session is just a tag on chat_log rows. Starting a new one clears working
# memory; switching reloads a past conversation's history so replies have
# context. Consolidation still reads ALL unconsolidated rows regardless.
def start_new(self, session_id: str) -> None:
self.session_id = session_id
self.history = []
def switch(self, session_id: str) -> None:
self.session_id = session_id
self.history = []
if self.memory is None:
return
# only the recent tail of a past conversation goes back into working
# memory (respond() also windows it, but don't hold the whole thread)
turns = self.settings.history_turns
for user_msg, reply in list(self.memory.session_history(session_id))[-turns:]:
self.history.append({"role": "user", "content": user_msg})
# rows from before notes were compact may carry a full tool output
self.history.append({"role": "assistant", "content": tool_note.clip(reply)})