mforege 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.
- app/__init__.py +3 -0
- app/agent/__init__.py +10 -0
- app/agent/agent.py +431 -0
- app/agent/long_term_memory.py +261 -0
- app/agent/memory.py +108 -0
- app/agent/tools.py +264 -0
- app/llm/__init__.py +6 -0
- app/llm/client.py +149 -0
- app/tools/__init__.py +28 -0
- app/tools/system_tools.py +972 -0
- app/tools/web_search.py +107 -0
- app/ui.py +237 -0
- main.py +334 -0
- mforege-0.1.0.dist-info/METADATA +265 -0
- mforege-0.1.0.dist-info/RECORD +19 -0
- mforege-0.1.0.dist-info/WHEEL +5 -0
- mforege-0.1.0.dist-info/entry_points.txt +2 -0
- mforege-0.1.0.dist-info/licenses/LICENSE +21 -0
- mforege-0.1.0.dist-info/top_level.txt +2 -0
app/__init__.py
ADDED
app/agent/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Agent Package
|
|
3
|
+
"""
|
|
4
|
+
from ..llm import LLMClient
|
|
5
|
+
from .agent import Agent, AgentConfig
|
|
6
|
+
from .memory import ConversationMemory, Message
|
|
7
|
+
from .long_term_memory import LongTermMemory
|
|
8
|
+
from .tools import Tool, ToolRegistry, CalculatorTool, TimeTool
|
|
9
|
+
|
|
10
|
+
__all__ = ["Agent", "AgentConfig", "LLMClient", "ConversationMemory", "Message", "LongTermMemory", "Tool", "ToolRegistry", "CalculatorTool", "TimeTool"]
|
app/agent/agent.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Agent Implementation
|
|
3
|
+
========================
|
|
4
|
+
A conversational AI agent with support for multiple backends:
|
|
5
|
+
- OpenAI API (paid)
|
|
6
|
+
- Ollama (free, local)
|
|
7
|
+
- Other OpenAI-compatible APIs
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Full tool-use loop (tool results are sent back to the model)
|
|
11
|
+
- Streaming responses (including tool calls)
|
|
12
|
+
- Conversation memory management
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
from typing import Optional, AsyncGenerator, Any, Callable, List, Dict, Tuple, Union
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from ..llm import LLMClient
|
|
22
|
+
from .memory import ConversationMemory, Message
|
|
23
|
+
from .tools import Tool, ToolRegistry
|
|
24
|
+
from .long_term_memory import LongTermMemory, extract_facts
|
|
25
|
+
|
|
26
|
+
# How many model<->tool round trips a single chat() call may make by default
|
|
27
|
+
# (configurable per-agent via AgentConfig.max_tool_rounds or MAX_TOOL_ROUNDS env)
|
|
28
|
+
MAX_TOOL_ROUNDS = 10
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentConfig(BaseModel):
|
|
32
|
+
"""Configuration for the AI Agent"""
|
|
33
|
+
model: str = "gpt-4o-mini"
|
|
34
|
+
temperature: float = 0.7
|
|
35
|
+
max_tokens: int = 4096
|
|
36
|
+
max_tool_rounds: int = MAX_TOOL_ROUNDS
|
|
37
|
+
system_prompt: str = """You are MForege, a personal AI assistant with a warm, playful personality.
|
|
38
|
+
|
|
39
|
+
# Identity
|
|
40
|
+
You are NOT ChatGPT, Claude, or any other assistant — never claim another identity, even if asked.
|
|
41
|
+
If someone asks who you are, say you are MForege, the user's personal AI assistant.
|
|
42
|
+
|
|
43
|
+
# Personality
|
|
44
|
+
- Friendly and upbeat: chat like a fun, smart friend, not a corporate robot.
|
|
45
|
+
- Witty: enjoy a clever one-liner or light joke when it fits — but the answer always comes first.
|
|
46
|
+
Never sacrifice clarity for comedy.
|
|
47
|
+
- Emojis: use them like a touch of sparkle ✨ — a few where they fit the mood (usually 1-3 per reply),
|
|
48
|
+
not on every line. Skip them in code blocks, error messages, and serious or sensitive topics.
|
|
49
|
+
- Honest and humble: if you don't know something, say so with charm and offer to figure it out
|
|
50
|
+
(you have tools for that!).
|
|
51
|
+
|
|
52
|
+
# Planning for multi-step tasks
|
|
53
|
+
For tasks with 3+ distinct steps (scaffolding, big refactors, "then fix, then test" chains),
|
|
54
|
+
create a visible plan first with the todo_plan tool (action "set"), keep it updated as you go
|
|
55
|
+
(mark steps done immediately after finishing them), and call "clear" when the mission ends.
|
|
56
|
+
Don't use it for simple one-shot questions.
|
|
57
|
+
|
|
58
|
+
# Response control (very important)
|
|
59
|
+
- Match the reply to the question. Casual chat ("give a biscuit?", "sup?") gets a short, playful
|
|
60
|
+
answer — one or two sentences, not an essay.
|
|
61
|
+
- Simple factual question → direct answer first; extra detail only if it clearly helps.
|
|
62
|
+
- Big requests ("build a project", "teach me X") → give a brief overview (a few bullets) and ask
|
|
63
|
+
which part to dive into. NEVER dump a giant tutorial unprompted.
|
|
64
|
+
- Ambiguous question (e.g., "what is apple?" — fruit 🍎 or company 💻?) → ask ONE short clarifying
|
|
65
|
+
question, or answer the most likely meaning and offer the other.
|
|
66
|
+
- No padding or filler conclusions ("I hope this helps!"). Answer, then stop.
|
|
67
|
+
|
|
68
|
+
# Acting on the user's machine (agentic tools)
|
|
69
|
+
You have tools to inspect and modify the user's project: list_files, read_file, run_command,
|
|
70
|
+
create_file, edit_file, search_code, glob_files.
|
|
71
|
+
- Search-first workflow: before touching anything, understand the project. Use glob_files to
|
|
72
|
+
discover files (e.g., '**/*.py'), search_code to find where things are defined or where an
|
|
73
|
+
error string originates, and read_file (with offset/limit for big files) to study the code.
|
|
74
|
+
- You operate inside ONE workspace folder, chosen by the user at startup (shown in the header).
|
|
75
|
+
All tool paths are relative to it. If the user names a folder outside this workspace, DO NOT
|
|
76
|
+
say you "can't" or call it a sandbox limitation — instead say: "restart me with
|
|
77
|
+
--workspace <that folder> and I'll work there", then offer to proceed in the current
|
|
78
|
+
workspace meanwhile.
|
|
79
|
+
- Relative paths in conversation (e.g., "in the town folder") are fine — tools resolve them
|
|
80
|
+
inside the workspace. Absolute paths outside it are refused by design.
|
|
81
|
+
- Before creating anything the user didn't fully specify (project name, location, stack), ask ONE
|
|
82
|
+
short clarifying question or propose sensible defaults and ask "shall I proceed?". NEVER invent
|
|
83
|
+
details and act on them silently.
|
|
84
|
+
- Prefer list_files/read_file to ground answers in the actual project before guessing.
|
|
85
|
+
- Use create_file for new files (small plan first for big scaffolds); use edit_file for existing
|
|
86
|
+
ones — read the file first, then replace an exact unique snippet.
|
|
87
|
+
- Every write/edit asks the user for confirmation — that's by design, don't try to bypass it
|
|
88
|
+
(e.g., don't echo file content through run_command to write files).
|
|
89
|
+
- When running commands, explain briefly what you're about to run and why. If the user cancels,
|
|
90
|
+
accept it gracefully and ask what they'd prefer.
|
|
91
|
+
- Long build/install commands may take a while — warn the user before starting them.
|
|
92
|
+
|
|
93
|
+
# Tools
|
|
94
|
+
- Prefer tools when they make the answer accurate: web_search for anything current or uncertain,
|
|
95
|
+
calculator for math, get_current_time for dates/times.
|
|
96
|
+
- When using a tool, briefly say what you're doing (e.g., "Crunching the numbers... 🧮").
|
|
97
|
+
- If a needed tool is unavailable (e.g., search not configured), say so honestly instead of guessing.
|
|
98
|
+
- Maintain context across the conversation and offer helpful follow-ups — briefly."""
|
|
99
|
+
streaming: bool = True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Agent:
|
|
103
|
+
"""
|
|
104
|
+
AI Agent with tool use capabilities.
|
|
105
|
+
|
|
106
|
+
Supports multiple LLM backends:
|
|
107
|
+
- openai: OpenAI API (requires API key)
|
|
108
|
+
- ollama: Local Ollama (free, no API key needed)
|
|
109
|
+
- custom: Any OpenAI-compatible API
|
|
110
|
+
|
|
111
|
+
Features:
|
|
112
|
+
- Full tool loop: when the model requests a tool, the tool runs and its
|
|
113
|
+
result is sent back so the model can incorporate it into the answer
|
|
114
|
+
- Supports streaming responses (tool calls included)
|
|
115
|
+
- Maintains conversation memory
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, config: Optional[AgentConfig] = None, api_key: Optional[str] = None,
|
|
119
|
+
backend: str = "openai", base_url: Optional[str] = None,
|
|
120
|
+
memory_path: Optional[str] = None):
|
|
121
|
+
self.config = config or AgentConfig()
|
|
122
|
+
self.llm = LLMClient(
|
|
123
|
+
backend=backend,
|
|
124
|
+
api_key=api_key,
|
|
125
|
+
base_url=base_url,
|
|
126
|
+
model=self.config.model
|
|
127
|
+
)
|
|
128
|
+
self.memory = ConversationMemory()
|
|
129
|
+
self.tools = ToolRegistry()
|
|
130
|
+
self.long_term_memory = LongTermMemory(path=memory_path)
|
|
131
|
+
self._pending_exchange: Optional[Tuple[str, str]] = None # (user msg, reply) awaiting memory extraction
|
|
132
|
+
self.on_activity: Optional[Callable[[str, str], None]] = None # (event, detail) -> UI hook
|
|
133
|
+
|
|
134
|
+
def _emit(self, event: str, detail: str = "") -> None:
|
|
135
|
+
"""Notify the UI of agent activity (tool calls, rounds). Never raises."""
|
|
136
|
+
if self.on_activity is None:
|
|
137
|
+
return
|
|
138
|
+
try:
|
|
139
|
+
self.on_activity(event, detail)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def add_tool(self, tool: Tool) -> None:
|
|
144
|
+
"""Register a tool for the agent to use"""
|
|
145
|
+
self.tools.register(tool)
|
|
146
|
+
|
|
147
|
+
def register_tools(self, *tools: Tool) -> None:
|
|
148
|
+
"""Register multiple tools at once"""
|
|
149
|
+
for tool in tools:
|
|
150
|
+
self.add_tool(tool)
|
|
151
|
+
|
|
152
|
+
async def chat(self, message: str, stream: Optional[bool] = None) -> Union[str, AsyncGenerator[str, None]]:
|
|
153
|
+
"""
|
|
154
|
+
Send a message to the agent and get a response.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
message: User's message
|
|
158
|
+
stream: Whether to stream the response (default: use config setting)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
If streaming: async generator yielding text chunks
|
|
162
|
+
If not streaming: complete response string
|
|
163
|
+
"""
|
|
164
|
+
# Add user message to memory
|
|
165
|
+
# First, remember anything durable from the previous exchange
|
|
166
|
+
await self._process_pending_memory()
|
|
167
|
+
|
|
168
|
+
self.memory.add(Message(role="user", content=message))
|
|
169
|
+
|
|
170
|
+
stream_mode = stream if stream is not None else self.config.streaming
|
|
171
|
+
|
|
172
|
+
if stream_mode:
|
|
173
|
+
return self._stream_response(user_message=message)
|
|
174
|
+
|
|
175
|
+
reply = await self._get_response()
|
|
176
|
+
self._pending_exchange = (message, reply)
|
|
177
|
+
await self._process_pending_memory() # remember this exchange immediately
|
|
178
|
+
return reply
|
|
179
|
+
|
|
180
|
+
async def flush_memory(self) -> None:
|
|
181
|
+
"""Force extraction of any pending exchange (call before shutdown)"""
|
|
182
|
+
await self._process_pending_memory()
|
|
183
|
+
|
|
184
|
+
async def _process_pending_memory(self) -> None:
|
|
185
|
+
"""Extract durable facts from the pending exchange (best-effort)"""
|
|
186
|
+
if not self._pending_exchange:
|
|
187
|
+
return
|
|
188
|
+
user_msg, reply = self._pending_exchange
|
|
189
|
+
self._pending_exchange = None
|
|
190
|
+
try:
|
|
191
|
+
new_facts = await extract_facts(
|
|
192
|
+
self.llm, self.config.model, user_msg, reply,
|
|
193
|
+
self.long_term_memory.all(),
|
|
194
|
+
)
|
|
195
|
+
except Exception:
|
|
196
|
+
return # memory is best-effort; never disrupt chatting
|
|
197
|
+
for fact in new_facts:
|
|
198
|
+
self.long_term_memory.add(fact)
|
|
199
|
+
|
|
200
|
+
def _build_api_messages(self) -> List[Dict]:
|
|
201
|
+
"""Build the message list sent to the API (system prompt + plan + memories + history)"""
|
|
202
|
+
system_prompt = self.long_term_memory.inject_into(self.config.system_prompt)
|
|
203
|
+
plan = getattr(self, "plan_state", None)
|
|
204
|
+
if plan is not None and plan.steps:
|
|
205
|
+
system_prompt += (
|
|
206
|
+
"\n\n# Current mission plan (use todo_plan to update it as you progress)\n"
|
|
207
|
+
+ plan.render()
|
|
208
|
+
)
|
|
209
|
+
return [
|
|
210
|
+
{"role": "system", "content": system_prompt},
|
|
211
|
+
*self.memory.to_openai_format(),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
def _tools_schema(self) -> Optional[List[Dict]]:
|
|
215
|
+
return self.tools.to_openai_schema() if self.tools.tools else None
|
|
216
|
+
|
|
217
|
+
# ── Non-streaming path ─────────────────────────────
|
|
218
|
+
async def _get_response(self) -> str:
|
|
219
|
+
"""Run the tool loop until the model produces a final text answer"""
|
|
220
|
+
messages = self._build_api_messages()
|
|
221
|
+
tools_schema = self._tools_schema()
|
|
222
|
+
|
|
223
|
+
for round_num in range(1, self.config.max_tool_rounds + 1):
|
|
224
|
+
if round_num > 1:
|
|
225
|
+
self._emit("round", f"{round_num}/{self.config.max_tool_rounds}")
|
|
226
|
+
response = await self.llm.create_chat_completion(
|
|
227
|
+
model=self.config.model,
|
|
228
|
+
messages=messages,
|
|
229
|
+
temperature=self.config.temperature,
|
|
230
|
+
max_tokens=self.config.max_tokens,
|
|
231
|
+
tools=tools_schema,
|
|
232
|
+
tool_choice="auto" if tools_schema else None
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
msg = response.choices[0].message
|
|
236
|
+
|
|
237
|
+
if not msg.tool_calls:
|
|
238
|
+
content = msg.content or ""
|
|
239
|
+
self.memory.add(Message(role="assistant", content=content))
|
|
240
|
+
return content
|
|
241
|
+
|
|
242
|
+
# Record the assistant's tool-call request, run the tools,
|
|
243
|
+
# then feed the results back to the model.
|
|
244
|
+
messages.append({
|
|
245
|
+
"role": "assistant",
|
|
246
|
+
"content": msg.content or "",
|
|
247
|
+
"tool_calls": [
|
|
248
|
+
{
|
|
249
|
+
"id": tc.id,
|
|
250
|
+
"type": "function",
|
|
251
|
+
"function": {
|
|
252
|
+
"name": tc.function.name,
|
|
253
|
+
"arguments": tc.function.arguments,
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
for tc in msg.tool_calls
|
|
257
|
+
],
|
|
258
|
+
})
|
|
259
|
+
tool_results = await self._run_tool_calls(msg.tool_calls)
|
|
260
|
+
messages.extend(tool_results)
|
|
261
|
+
|
|
262
|
+
# Model kept requesting tools past the limit; force a final answer
|
|
263
|
+
response = await self.llm.create_chat_completion(
|
|
264
|
+
model=self.config.model,
|
|
265
|
+
messages=messages,
|
|
266
|
+
temperature=self.config.temperature,
|
|
267
|
+
max_tokens=self.config.max_tokens,
|
|
268
|
+
)
|
|
269
|
+
content = response.choices[0].message.content or ""
|
|
270
|
+
self.memory.add(Message(role="assistant", content=content))
|
|
271
|
+
return content
|
|
272
|
+
|
|
273
|
+
# ── Streaming path ─────────────────────────────────
|
|
274
|
+
|
|
275
|
+
async def _stream_response(self, user_message: str = "") -> AsyncGenerator[str, None]:
|
|
276
|
+
"""
|
|
277
|
+
Stream a response, handling tool calls if they occur.
|
|
278
|
+
|
|
279
|
+
If the model requests tools mid-stream, the tools run and a new
|
|
280
|
+
streamed completion is started; the generator yields the final
|
|
281
|
+
text answer (streamed chunk by chunk).
|
|
282
|
+
"""
|
|
283
|
+
messages = self._build_api_messages()
|
|
284
|
+
tools_schema = self._tools_schema()
|
|
285
|
+
full_content = ""
|
|
286
|
+
saved = False
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
for round_num in range(1, self.config.max_tool_rounds + 1):
|
|
290
|
+
if round_num > 1:
|
|
291
|
+
self._emit("round", f"{round_num}/{self.config.max_tool_rounds}")
|
|
292
|
+
response = await self.llm.create_chat_completion(
|
|
293
|
+
model=self.config.model,
|
|
294
|
+
messages=messages,
|
|
295
|
+
temperature=self.config.temperature,
|
|
296
|
+
max_tokens=self.config.max_tokens,
|
|
297
|
+
stream=True,
|
|
298
|
+
tools=tools_schema,
|
|
299
|
+
tool_choice="auto" if tools_schema else None
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
round_content = ""
|
|
303
|
+
tool_calls: Dict[int, Dict[str, Any]] = {} # index -> {id, name, arguments}
|
|
304
|
+
|
|
305
|
+
async for chunk in response:
|
|
306
|
+
if not chunk.choices:
|
|
307
|
+
continue
|
|
308
|
+
delta = chunk.choices[0].delta
|
|
309
|
+
|
|
310
|
+
if delta.content:
|
|
311
|
+
round_content += delta.content
|
|
312
|
+
full_content += delta.content
|
|
313
|
+
yield delta.content
|
|
314
|
+
|
|
315
|
+
# Accumulate streamed tool-call fragments
|
|
316
|
+
if delta.tool_calls:
|
|
317
|
+
for tc_delta in delta.tool_calls:
|
|
318
|
+
entry = tool_calls.setdefault(
|
|
319
|
+
tc_delta.index, {"id": "", "name": "", "arguments": ""}
|
|
320
|
+
)
|
|
321
|
+
if tc_delta.id:
|
|
322
|
+
entry["id"] = tc_delta.id
|
|
323
|
+
if tc_delta.function:
|
|
324
|
+
if tc_delta.function.name:
|
|
325
|
+
entry["name"] += tc_delta.function.name
|
|
326
|
+
if tc_delta.function.arguments:
|
|
327
|
+
entry["arguments"] += tc_delta.function.arguments
|
|
328
|
+
|
|
329
|
+
# No tools requested -> this round is the final answer
|
|
330
|
+
if not tool_calls:
|
|
331
|
+
self.memory.add(Message(role="assistant", content=full_content))
|
|
332
|
+
saved = True
|
|
333
|
+
if user_message and full_content:
|
|
334
|
+
self._pending_exchange = (user_message, full_content)
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Feed the tool-call request back and run the tools
|
|
338
|
+
messages.append({
|
|
339
|
+
"role": "assistant",
|
|
340
|
+
"content": round_content,
|
|
341
|
+
"tool_calls": [
|
|
342
|
+
{
|
|
343
|
+
"id": tc["id"] or f"call_{i}",
|
|
344
|
+
"type": "function",
|
|
345
|
+
"function": {"name": tc["name"], "arguments": tc["arguments"]},
|
|
346
|
+
}
|
|
347
|
+
for i, tc in sorted(tool_calls.items())
|
|
348
|
+
],
|
|
349
|
+
})
|
|
350
|
+
tool_results = await self._run_tool_calls_from_stream(tool_calls)
|
|
351
|
+
messages.extend(tool_results)
|
|
352
|
+
# Tool progress is surfaced below the stream, not in the text
|
|
353
|
+
finally:
|
|
354
|
+
# Persist whatever was produced, even if the consumer stops early
|
|
355
|
+
if full_content and not saved:
|
|
356
|
+
self.memory.add(Message(role="assistant", content=full_content))
|
|
357
|
+
|
|
358
|
+
# ── Tool execution ─────────────────────────────────
|
|
359
|
+
|
|
360
|
+
async def _run_tool_calls(self, tool_calls: list) -> List[Dict]:
|
|
361
|
+
"""Execute tool calls and return them as OpenAI 'tool' role messages"""
|
|
362
|
+
results = []
|
|
363
|
+
for tc in tool_calls:
|
|
364
|
+
result = await self._execute_one(tc.function.name, tc.function.arguments)
|
|
365
|
+
results.append({
|
|
366
|
+
"role": "tool",
|
|
367
|
+
"tool_call_id": tc.id,
|
|
368
|
+
"content": result,
|
|
369
|
+
})
|
|
370
|
+
return results
|
|
371
|
+
|
|
372
|
+
async def _run_tool_calls_from_stream(self, tool_calls: Dict[int, Dict[str, Any]]) -> List[Dict]:
|
|
373
|
+
"""Same as _run_tool_calls but for tool calls accumulated from a stream"""
|
|
374
|
+
results = []
|
|
375
|
+
for i, tc in sorted(tool_calls.items()):
|
|
376
|
+
result = await self._execute_one(tc["name"], tc["arguments"])
|
|
377
|
+
results.append({
|
|
378
|
+
"role": "tool",
|
|
379
|
+
"tool_call_id": tc["id"] or f"call_{i}",
|
|
380
|
+
"content": result,
|
|
381
|
+
})
|
|
382
|
+
return results
|
|
383
|
+
|
|
384
|
+
async def _execute_one(self, function_name: str, raw_arguments: str) -> str:
|
|
385
|
+
"""Execute a single tool and return its result as a string (never raises)"""
|
|
386
|
+
tool = self.tools.get(function_name)
|
|
387
|
+
if not tool:
|
|
388
|
+
return f"Error: unknown tool '{function_name}'"
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
function_args = json.loads(raw_arguments) if raw_arguments else {}
|
|
392
|
+
except json.JSONDecodeError:
|
|
393
|
+
return f"Error: could not parse arguments for {function_name}: {raw_arguments!r}"
|
|
394
|
+
|
|
395
|
+
if not isinstance(function_args, dict):
|
|
396
|
+
return f"Error: arguments for {function_name} must be an object"
|
|
397
|
+
|
|
398
|
+
# Show the user what the model is doing right now
|
|
399
|
+
brief = ", ".join(f"{k}={str(v)[:40]}" for k, v in list(function_args.items())[:2])
|
|
400
|
+
self._emit("tool_start", f"{function_name}({brief})")
|
|
401
|
+
start = time.monotonic()
|
|
402
|
+
try:
|
|
403
|
+
result = str(await tool.execute(**function_args))
|
|
404
|
+
except Exception as e:
|
|
405
|
+
result = f"Error in {function_name}: {e}"
|
|
406
|
+
elapsed = time.monotonic() - start
|
|
407
|
+
ok = not result.startswith("Error")
|
|
408
|
+
self._emit("tool_end", f"{function_name} {'✓' if ok else '✗'} ({elapsed:.1f}s)")
|
|
409
|
+
return result
|
|
410
|
+
|
|
411
|
+
# ── Utilities ──────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
def clear_memory(self) -> None:
|
|
414
|
+
"""Clear conversation history (session memory, not long-term facts)"""
|
|
415
|
+
self.memory.clear()
|
|
416
|
+
|
|
417
|
+
def clear_long_term_memory(self) -> bool:
|
|
418
|
+
"""Forget all long-term facts across sessions"""
|
|
419
|
+
return self.long_term_memory.clear()
|
|
420
|
+
|
|
421
|
+
def remember_fact(self, fact: str) -> bool:
|
|
422
|
+
"""Manually store a durable fact about the user"""
|
|
423
|
+
return self.long_term_memory.add(fact)
|
|
424
|
+
|
|
425
|
+
def get_history(self) -> List[Message]:
|
|
426
|
+
"""Get conversation history"""
|
|
427
|
+
return self.memory.messages
|
|
428
|
+
|
|
429
|
+
def set_system_prompt(self, prompt: str) -> None:
|
|
430
|
+
"""Update the system prompt (applies to the next chat call)"""
|
|
431
|
+
self.config.system_prompt = prompt
|