openmuse 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openmuse/__init__.py +10 -0
- openmuse/__main__.py +4 -0
- openmuse/agent/__init__.py +3 -0
- openmuse/agent/core.py +257 -0
- openmuse/app.py +150 -0
- openmuse/cli.py +563 -0
- openmuse/config.example.toml +155 -0
- openmuse/config.py +308 -0
- openmuse/console.py +136 -0
- openmuse/goals/__init__.py +3 -0
- openmuse/goals/store.py +233 -0
- openmuse/llm/__init__.py +15 -0
- openmuse/llm/base.py +119 -0
- openmuse/llm/factory.py +24 -0
- openmuse/llm/mock.py +39 -0
- openmuse/llm/openai_chat.py +188 -0
- openmuse/llm/openai_responses.py +213 -0
- openmuse/llm/prompt_tools.py +187 -0
- openmuse/logger.py +41 -0
- openmuse/memory/__init__.py +3 -0
- openmuse/memory/store.py +158 -0
- openmuse/prompts.py +116 -0
- openmuse/schema.py +185 -0
- openmuse/sentinel/__init__.py +5 -0
- openmuse/sentinel/audit.py +63 -0
- openmuse/sentinel/gate.py +191 -0
- openmuse/sentinel/policy.py +137 -0
- openmuse/server/__init__.py +86 -0
- openmuse/server/api.py +436 -0
- openmuse/server/events.py +142 -0
- openmuse/server/service.py +685 -0
- openmuse/server/static/assets/index-C0fjpYij.js +299 -0
- openmuse/server/static/assets/index-D2k1_9-v.css +1 -0
- openmuse/server/static/icon-192.png +0 -0
- openmuse/server/static/icon-512.png +0 -0
- openmuse/server/static/icon.svg +11 -0
- openmuse/server/static/index.html +25 -0
- openmuse/server/static/manifest.webmanifest +16 -0
- openmuse/server/webui.py +301 -0
- openmuse/tools/__init__.py +34 -0
- openmuse/tools/base.py +159 -0
- openmuse/tools/browser.py +238 -0
- openmuse/tools/email_tool.py +256 -0
- openmuse/tools/files.py +144 -0
- openmuse/tools/goal_tools.py +137 -0
- openmuse/tools/mcp_tools.py +157 -0
- openmuse/tools/memory_tools.py +110 -0
- openmuse/tools/shell.py +154 -0
- openmuse/tools/terminate.py +58 -0
- openmuse/tools/web.py +196 -0
- openmuse/ui.py +90 -0
- openmuse/vault/__init__.py +3 -0
- openmuse/vault/vault.py +144 -0
- openmuse-0.1.0.dist-info/METADATA +295 -0
- openmuse-0.1.0.dist-info/RECORD +58 -0
- openmuse-0.1.0.dist-info/WHEEL +4 -0
- openmuse-0.1.0.dist-info/entry_points.txt +2 -0
- openmuse-0.1.0.dist-info/licenses/LICENSE +21 -0
openmuse/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""OpenMuse: an open-source version of Meta's Muse personal agent.
|
|
2
|
+
|
|
3
|
+
The agent does the work (search, browse, files, code, email, long-running goals);
|
|
4
|
+
a separate Sentinel decides what may run and what leaves the machine; a credential
|
|
5
|
+
vault keeps secrets out of the model's sight; every action lands in an audit log.
|
|
6
|
+
``openmuse serve`` adds the phone app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = ["__version__"]
|
openmuse/__main__.py
ADDED
openmuse/agent/core.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""The agent loop: think (LLM) → act (tools through the Sentinel) → repeat."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from openmuse import prompts
|
|
12
|
+
from openmuse.config import Settings
|
|
13
|
+
from openmuse.goals import GoalStore
|
|
14
|
+
from openmuse.llm.base import BaseLLM
|
|
15
|
+
from openmuse.logger import logger
|
|
16
|
+
from openmuse.memory import MemoryStore
|
|
17
|
+
from openmuse.schema import AgentState, Message, Role, ToolResult
|
|
18
|
+
from openmuse.sentinel import AuditLog, Sentinel
|
|
19
|
+
from openmuse.tools.base import ToolCollection
|
|
20
|
+
from openmuse.ui import UI
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MuseAgent:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
settings: Settings,
|
|
27
|
+
llm: BaseLLM,
|
|
28
|
+
tools: ToolCollection,
|
|
29
|
+
sentinel: Sentinel,
|
|
30
|
+
ui: UI,
|
|
31
|
+
audit: AuditLog,
|
|
32
|
+
memory: MemoryStore | None = None,
|
|
33
|
+
goals: GoalStore | None = None,
|
|
34
|
+
session_file: Path | None = None,
|
|
35
|
+
):
|
|
36
|
+
self.settings = settings
|
|
37
|
+
self.llm = llm
|
|
38
|
+
self.tools = tools
|
|
39
|
+
self.sentinel = sentinel
|
|
40
|
+
self.ui = ui
|
|
41
|
+
self.audit = audit
|
|
42
|
+
self.memory = memory
|
|
43
|
+
self.goals = goals
|
|
44
|
+
self.session_file = session_file
|
|
45
|
+
self.messages: list[Message] = []
|
|
46
|
+
self.state = AgentState.IDLE
|
|
47
|
+
self.turns = 0
|
|
48
|
+
# Optional queue of user messages that arrive *while* a run is in progress (the app
|
|
49
|
+
# lets you interrupt or pile on requests). They are folded into the conversation
|
|
50
|
+
# before the next model call instead of waiting for the current run to finish.
|
|
51
|
+
self.inbox: asyncio.Queue[str] | None = None
|
|
52
|
+
|
|
53
|
+
def _drain_inbox(self) -> int:
|
|
54
|
+
if self.inbox is None:
|
|
55
|
+
return 0
|
|
56
|
+
count = 0
|
|
57
|
+
while not self.inbox.empty():
|
|
58
|
+
try:
|
|
59
|
+
text = self.inbox.get_nowait()
|
|
60
|
+
except asyncio.QueueEmpty: # pragma: no cover
|
|
61
|
+
break
|
|
62
|
+
self.messages.append(Message.user(text))
|
|
63
|
+
self.audit.record("user_message", content=text, interjected=True)
|
|
64
|
+
count += 1
|
|
65
|
+
return count
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------ prompt
|
|
68
|
+
def build_system_prompt(self, user_input: str) -> str:
|
|
69
|
+
a = self.settings.agent
|
|
70
|
+
language_rule = (
|
|
71
|
+
prompts.LANGUAGE_AUTO.format(detected=prompts.detect_language(user_input))
|
|
72
|
+
if a.language in ("", "auto")
|
|
73
|
+
else prompts.LANGUAGE_FIXED.format(language=a.language)
|
|
74
|
+
)
|
|
75
|
+
memories = ""
|
|
76
|
+
if self.memory is not None and self.settings.memory.enabled:
|
|
77
|
+
items = self.memory.relevant(user_input, limit=self.settings.memory.max_inject)
|
|
78
|
+
if items:
|
|
79
|
+
memories = prompts.MEMORY_SECTION.format(
|
|
80
|
+
items="\n".join(f"- {m.render()}" for m in items)
|
|
81
|
+
)
|
|
82
|
+
goals = ""
|
|
83
|
+
if self.goals is not None:
|
|
84
|
+
active = self.goals.list("active")[:5]
|
|
85
|
+
if active:
|
|
86
|
+
lines = []
|
|
87
|
+
for g in active:
|
|
88
|
+
nxt = g.next_step
|
|
89
|
+
lines.append(
|
|
90
|
+
f"- {g.id}: {g.title} (progress {g.progress}"
|
|
91
|
+
+ (f", next: {nxt.idx}. {nxt.title}" if nxt else "")
|
|
92
|
+
+ ")"
|
|
93
|
+
)
|
|
94
|
+
goals = prompts.GOALS_SECTION.format(items="\n".join(lines))
|
|
95
|
+
profile = (
|
|
96
|
+
prompts.USER_PROFILE_SECTION.format(profile=a.user_profile.strip())
|
|
97
|
+
if a.user_profile.strip()
|
|
98
|
+
else ""
|
|
99
|
+
)
|
|
100
|
+
extra = (
|
|
101
|
+
f"\n## Additional instructions\n{a.instructions.strip()}\n"
|
|
102
|
+
if a.instructions.strip()
|
|
103
|
+
else ""
|
|
104
|
+
)
|
|
105
|
+
return prompts.SYSTEM_PROMPT.format(
|
|
106
|
+
name=a.name,
|
|
107
|
+
language_rule=language_rule,
|
|
108
|
+
now=datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%A, UTC%z)"),
|
|
109
|
+
workspace=str(a.workspace.resolve()),
|
|
110
|
+
sentinel_mode=self.settings.sentinel.mode,
|
|
111
|
+
tool_names=", ".join(t.name for t in self.tools),
|
|
112
|
+
user_profile=profile,
|
|
113
|
+
memories=memories,
|
|
114
|
+
goals=goals,
|
|
115
|
+
extra=extra,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------ context window
|
|
119
|
+
def context_messages(self) -> list[Message]:
|
|
120
|
+
limit = self.settings.agent.max_context_messages
|
|
121
|
+
msgs = self.messages
|
|
122
|
+
if len(msgs) <= limit:
|
|
123
|
+
return list(msgs)
|
|
124
|
+
cutoff = len(msgs) - limit
|
|
125
|
+
# Never start in the middle of a tool exchange: advance to the next user message.
|
|
126
|
+
while cutoff < len(msgs) and msgs[cutoff].role != Role.USER:
|
|
127
|
+
cutoff += 1
|
|
128
|
+
return list(msgs[cutoff:]) if cutoff < len(msgs) else list(msgs[-limit:])
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------ stuck detection
|
|
131
|
+
def _is_stuck(self) -> bool:
|
|
132
|
+
assistants = [m for m in self.messages if m.role == Role.ASSISTANT][-3:]
|
|
133
|
+
if len(assistants) < 3:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def sig(m: Message) -> str:
|
|
137
|
+
calls = [(tc.function.name, tc.function.arguments) for tc in (m.tool_calls or [])]
|
|
138
|
+
return json.dumps([m.content, calls], ensure_ascii=False, sort_keys=True)
|
|
139
|
+
|
|
140
|
+
return len({sig(m) for m in assistants}) == 1
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------ main loop
|
|
143
|
+
async def run(self, user_input: str) -> str:
|
|
144
|
+
if self.state == AgentState.RUNNING:
|
|
145
|
+
raise RuntimeError("agent is already running")
|
|
146
|
+
self.state = AgentState.RUNNING
|
|
147
|
+
self.turns += 1
|
|
148
|
+
self.messages.append(Message.user(user_input))
|
|
149
|
+
self.audit.record("user_message", content=user_input)
|
|
150
|
+
system_prompt = self.build_system_prompt(user_input)
|
|
151
|
+
tool_params = self.tools.to_params()
|
|
152
|
+
final: str | None = None
|
|
153
|
+
step = 0
|
|
154
|
+
empty_replies = 0
|
|
155
|
+
try:
|
|
156
|
+
while step < self.settings.agent.max_steps:
|
|
157
|
+
step += 1
|
|
158
|
+
if self._drain_inbox():
|
|
159
|
+
logger.debug("folded queued user message(s) into the running turn")
|
|
160
|
+
# the language rule and the memory section follow the latest message
|
|
161
|
+
system_prompt = self.build_system_prompt(self.messages[-1].content or "")
|
|
162
|
+
context = [Message.system(system_prompt), *self.context_messages()]
|
|
163
|
+
response = await self.llm.ask(
|
|
164
|
+
context, tools=tool_params, on_delta=self.ui.on_text_delta
|
|
165
|
+
)
|
|
166
|
+
assistant = response.to_message()
|
|
167
|
+
assistant.meta.update({"step": step, "usage": response.usage})
|
|
168
|
+
self.messages.append(assistant)
|
|
169
|
+
self.ui.on_assistant_message(response.content, response.reasoning)
|
|
170
|
+
self.audit.record(
|
|
171
|
+
"assistant_message",
|
|
172
|
+
step=step,
|
|
173
|
+
content=response.content or "",
|
|
174
|
+
tool_calls=[tc.function.name for tc in response.tool_calls],
|
|
175
|
+
usage=response.usage,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if not response.tool_calls:
|
|
179
|
+
if response.content:
|
|
180
|
+
final = response.content
|
|
181
|
+
break
|
|
182
|
+
empty_replies += 1
|
|
183
|
+
if empty_replies >= 2:
|
|
184
|
+
final = ""
|
|
185
|
+
break
|
|
186
|
+
self.messages.append(
|
|
187
|
+
Message.user(
|
|
188
|
+
"(Your reply was empty. Continue the task, or call `terminate` if it is done.)"
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
stop = False
|
|
194
|
+
for call in response.tool_calls:
|
|
195
|
+
tool = self.tools.get(call.name)
|
|
196
|
+
summary = tool.assess(call.arguments).summary if tool else f"{call.name}(?)"
|
|
197
|
+
self.ui.on_tool_call(call, summary)
|
|
198
|
+
if tool is None:
|
|
199
|
+
result = ToolResult.fail(
|
|
200
|
+
f"unknown tool '{call.name}'. Available tools: {', '.join(t.name for t in self.tools)}"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
result = await self.sentinel.guard(call, tool)
|
|
204
|
+
self.ui.on_tool_result(call, result)
|
|
205
|
+
self.messages.append(Message.tool(result.for_model(), call.id, call.name))
|
|
206
|
+
if result.stop:
|
|
207
|
+
final = result.output
|
|
208
|
+
stop = True
|
|
209
|
+
if stop:
|
|
210
|
+
break
|
|
211
|
+
if self._is_stuck():
|
|
212
|
+
logger.warning("agent seems stuck – nudging")
|
|
213
|
+
self.messages.append(Message.user(prompts.STUCK_PROMPT))
|
|
214
|
+
else:
|
|
215
|
+
# Step budget exhausted: ask for a wrap-up without tools.
|
|
216
|
+
self.messages.append(Message.user(prompts.MAX_STEPS_PROMPT))
|
|
217
|
+
context = [Message.system(system_prompt), *self.context_messages()]
|
|
218
|
+
response = await self.llm.ask(context, tools=None, on_delta=self.ui.on_text_delta)
|
|
219
|
+
self.messages.append(response.to_message())
|
|
220
|
+
self.ui.on_assistant_message(response.content, response.reasoning)
|
|
221
|
+
final = response.content or "(step limit reached)"
|
|
222
|
+
self.state = AgentState.FINISHED
|
|
223
|
+
except Exception:
|
|
224
|
+
self.state = AgentState.ERROR
|
|
225
|
+
raise
|
|
226
|
+
finally:
|
|
227
|
+
self._save_session()
|
|
228
|
+
return final or ""
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------ session persistence
|
|
231
|
+
def reset(self) -> None:
|
|
232
|
+
self.messages.clear()
|
|
233
|
+
self.sentinel.tainted = False
|
|
234
|
+
self.state = AgentState.IDLE
|
|
235
|
+
|
|
236
|
+
def _save_session(self) -> None:
|
|
237
|
+
if not self.session_file:
|
|
238
|
+
return
|
|
239
|
+
try:
|
|
240
|
+
self.session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
data: dict[str, Any] = {
|
|
242
|
+
"saved_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
243
|
+
"turns": self.turns,
|
|
244
|
+
"messages": [m.model_dump(mode="json") for m in self.messages],
|
|
245
|
+
}
|
|
246
|
+
self.session_file.write_text(json.dumps(data, ensure_ascii=False, indent=1), "utf-8")
|
|
247
|
+
except OSError as exc: # pragma: no cover
|
|
248
|
+
logger.warning("could not save session: {}", exc)
|
|
249
|
+
|
|
250
|
+
def load_session(self, path: Path) -> int:
|
|
251
|
+
data = json.loads(Path(path).read_text("utf-8"))
|
|
252
|
+
self.messages = [Message.model_validate(m) for m in data.get("messages", [])]
|
|
253
|
+
self.turns = int(data.get("turns", 0))
|
|
254
|
+
return len(self.messages)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
__all__ = ["MuseAgent"]
|
openmuse/app.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Composition root: wires settings → stores, vault, Sentinel, tools, LLM, agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from openmuse import prompts
|
|
10
|
+
from openmuse.agent import MuseAgent
|
|
11
|
+
from openmuse.config import Settings
|
|
12
|
+
from openmuse.goals import GoalStore
|
|
13
|
+
from openmuse.llm import BaseLLM, create_llm
|
|
14
|
+
from openmuse.logger import logger, setup_logging
|
|
15
|
+
from openmuse.memory import MemoryStore
|
|
16
|
+
from openmuse.sentinel import AuditLog, Sentinel
|
|
17
|
+
from openmuse.tools import (
|
|
18
|
+
AskUser,
|
|
19
|
+
Browser,
|
|
20
|
+
Files,
|
|
21
|
+
Forget,
|
|
22
|
+
Goals,
|
|
23
|
+
MCPManager,
|
|
24
|
+
PythonExecute,
|
|
25
|
+
ReadEmails,
|
|
26
|
+
Recall,
|
|
27
|
+
Remember,
|
|
28
|
+
SendEmail,
|
|
29
|
+
Shell,
|
|
30
|
+
Terminate,
|
|
31
|
+
ToolCollection,
|
|
32
|
+
WebFetch,
|
|
33
|
+
WebSearch,
|
|
34
|
+
playwright_available,
|
|
35
|
+
)
|
|
36
|
+
from openmuse.ui import UI
|
|
37
|
+
from openmuse.vault import CredentialVault
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenMuseApp:
|
|
41
|
+
def __init__(
|
|
42
|
+
self, settings: Settings, ui: UI, llm: BaseLLM | None = None, session_id: str | None = None
|
|
43
|
+
):
|
|
44
|
+
self.settings = settings
|
|
45
|
+
self.ui = ui
|
|
46
|
+
setup_logging(settings.log_level, settings.data_dir / "logs")
|
|
47
|
+
settings.ensure_dirs()
|
|
48
|
+
self.session_id = (
|
|
49
|
+
session_id or datetime.now().strftime("%Y%m%d-%H%M%S-") + uuid.uuid4().hex[:4]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.vault = CredentialVault(settings.vault_file, settings.vault_key_file)
|
|
53
|
+
self.memory = MemoryStore(settings.memory_db) if settings.memory.enabled else None
|
|
54
|
+
self.goals = GoalStore(settings.goals_db)
|
|
55
|
+
self.audit = AuditLog(settings.audit_file, session_id=self.session_id)
|
|
56
|
+
self.sentinel = Sentinel(
|
|
57
|
+
settings.sentinel,
|
|
58
|
+
audit=self.audit,
|
|
59
|
+
ui=ui,
|
|
60
|
+
vault=self.vault,
|
|
61
|
+
persistent_approvals_file=settings.data_dir / "approvals.json",
|
|
62
|
+
)
|
|
63
|
+
self.llm = llm or create_llm(settings.llm)
|
|
64
|
+
self.mcp = MCPManager(settings.mcp.servers) if settings.mcp.servers else None
|
|
65
|
+
self.tools = self._build_tools()
|
|
66
|
+
self.agent = MuseAgent(
|
|
67
|
+
settings=settings,
|
|
68
|
+
llm=self.llm,
|
|
69
|
+
tools=self.tools,
|
|
70
|
+
sentinel=self.sentinel,
|
|
71
|
+
ui=ui,
|
|
72
|
+
audit=self.audit,
|
|
73
|
+
memory=self.memory,
|
|
74
|
+
goals=self.goals,
|
|
75
|
+
session_file=settings.data_dir / "sessions" / f"{self.session_id}.json",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------ tools
|
|
79
|
+
def _build_tools(self) -> ToolCollection:
|
|
80
|
+
s = self.settings
|
|
81
|
+
ws: Path = s.agent.workspace
|
|
82
|
+
tools = ToolCollection(
|
|
83
|
+
Terminate(),
|
|
84
|
+
AskUser(ui=self.ui),
|
|
85
|
+
Files(workspace=ws),
|
|
86
|
+
Shell(workspace=ws),
|
|
87
|
+
PythonExecute(workspace=ws),
|
|
88
|
+
WebSearch(),
|
|
89
|
+
WebFetch(),
|
|
90
|
+
Goals(store=self.goals),
|
|
91
|
+
)
|
|
92
|
+
if self.memory is not None:
|
|
93
|
+
tools.add(
|
|
94
|
+
Remember(store=self.memory), Recall(store=self.memory), Forget(store=self.memory)
|
|
95
|
+
)
|
|
96
|
+
if s.connectors.email.enabled:
|
|
97
|
+
tools.add(
|
|
98
|
+
ReadEmails(settings=s.connectors.email, vault=self.vault),
|
|
99
|
+
SendEmail(settings=s.connectors.email, vault=self.vault),
|
|
100
|
+
)
|
|
101
|
+
if s.browser.enabled:
|
|
102
|
+
if playwright_available():
|
|
103
|
+
tools.add(
|
|
104
|
+
Browser(
|
|
105
|
+
headless=s.browser.headless, timeout_ms=s.browser.timeout_ms, workspace=ws
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
logger.warning(
|
|
110
|
+
"browser.enabled=true but playwright is missing: pip install 'openmuse[browser]'"
|
|
111
|
+
)
|
|
112
|
+
return tools
|
|
113
|
+
|
|
114
|
+
async def start(self) -> OpenMuseApp:
|
|
115
|
+
"""Connect optional MCP servers. Call once before using the agent."""
|
|
116
|
+
if self.mcp is not None:
|
|
117
|
+
for tool in await self.mcp.connect():
|
|
118
|
+
self.tools.add(tool)
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
async def close(self) -> None:
|
|
122
|
+
await self.tools.cleanup()
|
|
123
|
+
if self.mcp is not None:
|
|
124
|
+
await self.mcp.close()
|
|
125
|
+
await self.llm.close()
|
|
126
|
+
if self.memory is not None:
|
|
127
|
+
self.memory.close()
|
|
128
|
+
self.goals.close()
|
|
129
|
+
|
|
130
|
+
async def __aenter__(self) -> OpenMuseApp:
|
|
131
|
+
return await self.start()
|
|
132
|
+
|
|
133
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
134
|
+
await self.close()
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------ high-level ops
|
|
137
|
+
async def run(self, task: str) -> str:
|
|
138
|
+
return await self.agent.run(task)
|
|
139
|
+
|
|
140
|
+
async def advance_goal(self, goal_id: str) -> str:
|
|
141
|
+
goal = self.goals.get(goal_id)
|
|
142
|
+
if goal is None:
|
|
143
|
+
raise ValueError(f"no goal {goal_id}")
|
|
144
|
+
if goal.status != "active":
|
|
145
|
+
raise ValueError(f"goal {goal_id} is {goal.status}")
|
|
146
|
+
self.agent.reset()
|
|
147
|
+
return await self.agent.run(prompts.ADVANCE_GOAL_PROMPT.format(goal=goal.render()))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = ["OpenMuseApp"]
|