Memory layers - in progress

frdel committed Sep 12, 2024 at 21:47 UTC e4d05364951bdd4d6f754292afb9d0a25caaa3f2
43 files changed +1467 -693
.vscode/settings.json
+2 -1
@@ -1,3 +1,4 @@
1 {
2 - "python.analysis.typeCheckingMode": "standard"
2 + "python.analysis.typeCheckingMode": "standard",
3 + "deno.enable": true
4 }
\ No newline at end of file
agent.py
+277 -168
@@ -15,73 +15,79 @@ import python.helpers.log as Log
15 from python.helpers.dirty_json import DirtyJson
16 from python.helpers.defer import DeferredTask
17
18 +
19 class AgentContext:
20
20 - _contexts: dict[str, 'AgentContext'] = {}
21 + _contexts: dict[str, "AgentContext"] = {}
22 _counter: int = 0
22 -
23 - def __init__(self, config: 'AgentConfig', id:str|None = None, agent0: 'Agent|None' = None):
23 +
24 + def __init__(
25 + self, config: "AgentConfig", id: str | None = None, agent0: "Agent|None" = None
26 + ):
27 # build context
28 self.id = id or str(uuid.uuid4())
29 self.config = config
30 self.log = Log.Log()
31 self.agent0 = agent0 or Agent(0, self.config, self)
32 self.paused = False
30 - self.streaming_agent: Agent|None = None
31 - self.process: DeferredTask|None = None
33 + self.streaming_agent: Agent | None = None
34 + self.process: DeferredTask | None = None
35 AgentContext._counter += 1
33 - self.no = AgentContext._counter
36 + self.no = AgentContext._counter
37
38 self._contexts[self.id] = self
39
40 @staticmethod
38 - def get(id:str):
41 + def get(id: str):
42 return AgentContext._contexts.get(id, None)
43
44 @staticmethod
45 def first():
43 - if not AgentContext._contexts: return None
46 + if not AgentContext._contexts:
47 + return None
48 return list(AgentContext._contexts.values())[0]
49
46 -
50 @staticmethod
48 - def remove(id:str):
51 + def remove(id: str):
52 context = AgentContext._contexts.pop(id, None)
50 - if context and context.process: context.process.kill()
53 + if context and context.process:
54 + context.process.kill()
55 return context
56
57 def reset(self):
54 - if self.process: self.process.kill()
58 + if self.process:
59 + self.process.kill()
60 self.log.reset()
61 self.agent0 = Agent(0, self.config, self)
62 self.streaming_agent = None
58 - self.paused = False
63 + self.paused = False
64
60 -
65 def communicate(self, msg: str, broadcast_level: int = 1):
62 - self.paused=False #unpause if paused
63 -
66 + self.paused = False # unpause if paused
67 +
68 if self.process and self.process.is_alive():
65 - if self.streaming_agent: current_agent = self.streaming_agent
66 - else: current_agent = self.agent0
69 + if self.streaming_agent:
70 + current_agent = self.streaming_agent
71 + else:
72 + current_agent = self.agent0
73
74 # set intervention messages to agent(s):
75 intervention_agent = current_agent
70 - while intervention_agent and broadcast_level !=0:
76 + while intervention_agent and broadcast_level != 0:
77 intervention_agent.intervention_message = msg
78 broadcast_level -= 1
73 - intervention_agent = intervention_agent.data.get("superior",None)
79 + intervention_agent = intervention_agent.data.get("superior", None)
80 else:
75 - self.process = DeferredTask(self.agent0.message_loop, msg)
81 + self.process = DeferredTask(self.agent0.monologue, msg)
82
83 return self.process
78 -
79 -
84 +
85 +
86 @dataclass
81 -class AgentConfig:
87 +class AgentConfig:
88 chat_model: BaseChatModel | BaseLLM
89 utility_model: BaseChatModel | BaseLLM
84 - embeddings_model:Embeddings
90 + embeddings_model: Embeddings
91 prompts_subdir: str = ""
92 memory_subdir: str = ""
93 knowledge_subdir: str = ""
@@ -99,8 +105,14 @@ class AgentConfig:
105 code_exec_docker_enabled: bool = True
106 code_exec_docker_name: str = "agent-zero-exe"
107 code_exec_docker_image: str = "frdel/agent-zero-exe:latest"
102 - code_exec_docker_ports: dict[str,int] = field(default_factory=lambda: {"22/tcp": 50022})
103 - code_exec_docker_volumes: dict[str, dict[str, str]] = field(default_factory=lambda: {files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"}})
108 + code_exec_docker_ports: dict[str, int] = field(
109 + default_factory=lambda: {"22/tcp": 50022}
110 + )
111 + code_exec_docker_volumes: dict[str, dict[str, str]] = field(
112 + default_factory=lambda: {
113 + files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"}
114 + }
115 + )
116 code_exec_ssh_enabled: bool = True
117 code_exec_ssh_addr: str = "localhost"
118 code_exec_ssh_port: int = 50022
@@ -108,20 +120,25 @@ class AgentConfig:
120 code_exec_ssh_pass: str = "toor"
121 additional: Dict[str, Any] = field(default_factory=dict)
122
123 +
124 # intervention exception class - skips rest of message loop iteration
125 class InterventionException(Exception):
126 pass
127
128 +
129 # killer exception class - not forwarded to LLM, cannot be fixed on its own, ends message loop
116 -class KillerException(Exception):
130 +class RepairableException(Exception):
131 pass
132
133 +
134 class Agent:
120 -
121 - def __init__(self, number:int, config: AgentConfig, context: AgentContext|None = None):
135
123 - # agent config
124 - self.config = config
136 + def __init__(
137 + self, number: int, config: AgentConfig, context: AgentContext | None = None
138 + ):
139 +
140 + # agent config
141 + self.config = config
142
143 # agent context
144 self.context = context or AgentContext(config)
@@ -133,104 +150,181 @@ class Agent:
150 self.history = []
151 self.last_message = ""
152 self.intervention_message = ""
136 - self.rate_limiter = rate_limiter.RateLimiter(self.context.log,max_calls=self.config.rate_limit_requests,max_input_tokens=self.config.rate_limit_input_tokens,max_output_tokens=self.config.rate_limit_output_tokens,window_seconds=self.config.rate_limit_seconds)
137 - self.data = {} # free data object all the tools can use
138 -
139 - async def message_loop(self, msg: str):
153 + self.rate_limiter = rate_limiter.RateLimiter(
154 + self.context.log,
155 + max_calls=self.config.rate_limit_requests,
156 + max_input_tokens=self.config.rate_limit_input_tokens,
157 + max_output_tokens=self.config.rate_limit_output_tokens,
158 + window_seconds=self.config.rate_limit_seconds,
159 + )
160 + self.data = {} # free data object all the tools can use
161 +
162 + async def monologue(self, msg: str):
163 try:
141 - printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
164 +
165 + loop_data: dict[str, Any] = {"message": msg, "iteration": -1, "history_from": len(self.history) }
166 +
167 + await self.call_extensions(
168 + "monologue_start", loop_data=loop_data
169 + ) # call monologue_start extensions
170 +
171 + printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
172 user_message = self.read_prompt("fw.user_message.md", message=msg)
143 - await self.append_message(user_message, human=True) # Append the user's input to the history
144 - memories = await self.fetch_memories(True)
145 -
146 - while True: # let the agent iterate on his thoughts until he stops by using a tool
147 - self.context.streaming_agent = self #mark self as current streamer
173 + await self.append_message(user_message, human=True)
174 +
175 + await self.call_extensions(
176 + "monologue_start", message=msg
177 + ) # call monologue_end extensions
178 +
179 + while (
180 + True
181 + ): # let the agent iterate on his thoughts until he stops by using a tool
182 +
183 + self.context.streaming_agent = self # mark self as current streamer
184 agent_response = ""
185 + loop_data["iteration"] += 1
186
150 - try:
187
152 - system = self.read_prompt("agent.system.md", agent_name=self.agent_name) + "\n\n" + self.read_prompt("agent.tools.md")
153 - memories = await self.fetch_memories()
154 - if memories: system+= "\n\n"+memories
188 + try:
189
156 - prompt = ChatPromptTemplate.from_messages([
157 - SystemMessage(content=system),
158 - MessagesPlaceholder(variable_name="messages") ])
159 -
160 - inputs = {"messages": self.history}
190 + # set system prompt and message history
191 + loop_data["system"] = [
192 + self.read_prompt("agent.system.md", agent_name=self.agent_name),
193 + self.read_prompt("agent.system.tools.md"),
194 + ]
195 + loop_data["history"] = {"messages": self.history}
196 +
197 + # and allow extensions to edit them
198 + await self.call_extensions(
199 + "message_loop_prompts", loop_data=loop_data
200 + )
201 +
202 + # build chain from system prompt, message history and model
203 + prompt = ChatPromptTemplate.from_messages(
204 + [
205 + SystemMessage(content="\n\n".join(loop_data["system"])),
206 + MessagesPlaceholder(variable_name="messages"),
207 + ]
208 + )
209 chain = prompt | self.config.chat_model
210
211 + # rate limiter TODO - move to extension, make per-model
212 formatted_inputs = prompt.format(messages=self.history)
164 - tokens = int(len(formatted_inputs)/4)
213 + tokens = int(len(formatted_inputs) / 4)
214 self.rate_limiter.limit_call_and_input(tokens)
166 -
215 +
216 # output that the agent is starting
168 - PrintStyle(bold=True, font_color="green", padding=True, background_color="white").print(f"{self.agent_name}: Generating:")
169 - log = self.context.log.log(type="agent", heading=f"{self.agent_name}: Generating:")
170 -
171 - async for chunk in chain.astream(inputs):
172 - await self.handle_intervention(agent_response) # wait for intervention and handle it, if paused
173 -
174 - if isinstance(chunk, str): content = chunk
175 - elif hasattr(chunk, "content"): content = str(chunk.content)
176 - else: content = str(chunk)
177 -
217 + PrintStyle(
218 + bold=True,
219 + font_color="green",
220 + padding=True,
221 + background_color="white",
222 + ).print(f"{self.agent_name}: Generating:")
223 + log = self.context.log.log(
224 + type="agent", heading=f"{self.agent_name}: Generating:"
225 + )
226 +
227 + async for chunk in chain.astream(loop_data["history"]):
228 + await self.handle_intervention(
229 + agent_response
230 + ) # wait for intervention and handle it, if paused
231 +
232 + if isinstance(chunk, str):
233 + content = chunk
234 + elif hasattr(chunk, "content"):
235 + content = str(chunk.content)
236 + else:
237 + content = str(chunk)
238 +
239 if content:
179 - printer.stream(content) # output the agent response stream
180 - agent_response += content # concatenate stream into the response
240 + printer.stream(content) # output the agent response stream
241 + agent_response += (
242 + content # concatenate stream into the response
243 + )
244 self.log_from_stream(agent_response, log)
245
183 - self.rate_limiter.set_output_tokens(int(len(agent_response)/4)) # rough estimation
184 -
246 + self.rate_limiter.set_output_tokens(
247 + int(len(agent_response) / 4)
248 + ) # rough estimation
249 +
250 await self.handle_intervention(agent_response)
251
187 - if self.last_message == agent_response: #if assistant_response is the same as last message in history, let him know
188 - await self.append_message(agent_response) # Append the assistant's response to the history
252 + if (
253 + self.last_message == agent_response
254 + ): # if assistant_response is the same as last message in history, let him know
255 + await self.append_message(
256 + agent_response
257 + ) # Append the assistant's response to the history
258 warning_msg = self.read_prompt("fw.msg_repeat.md")
190 - await self.append_message(warning_msg, human=True) # Append warning message to the history
259 + await self.append_message(
260 + warning_msg, human=True
261 + ) # Append warning message to the history
262 PrintStyle(font_color="orange", padding=True).print(warning_msg)
263 self.context.log.log(type="warning", content=warning_msg)
264
194 - else: #otherwise proceed with tool
195 - await self.append_message(agent_response) # Append the assistant's response to the history
196 - tools_result = await self.process_tools(agent_response) # process tools requested in agent message
197 - if tools_result: #final response of message loop available
198 - return tools_result #break the execution if the task is done
265 + else: # otherwise proceed with tool
266 + await self.append_message(
267 + agent_response
268 + ) # Append the assistant's response to the history
269 + tools_result = await self.process_tools(
270 + agent_response
271 + ) # process tools requested in agent message
272 + if tools_result: # final response of message loop available
273 + await self.call_extensions(
274 + "monologue_end", tools_result=tools_result
275 + ) # call monologue_end extensions
276 + return (
277 + tools_result # break the execution if the task is done
278 + )
279
280 except InterventionException as e:
201 - pass # intervention message has been handled in handle_intervention(), proceed with conversation loop
281 + pass # intervention message has been handled in handle_intervention(), proceed with conversation loop
282 except asyncio.CancelledError as e:
203 - PrintStyle(font_color="white", background_color="red", padding=True).print(f"Context {self.context.id} terminated during message loop")
204 - raise e # process cancelled from outside, kill the loop
205 - except KillerException as e:
206 - error_message = errors.format_error(e)
207 - self.context.log.log(type="error", content=error_message)
208 - raise e # kill the loop
209 - except Exception as e: # Forward other errors to the LLM, maybe it can fix them
283 + PrintStyle(
284 + font_color="white", background_color="red", padding=True
285 + ).print(f"Context {self.context.id} terminated during message loop")
286 + raise e # process cancelled from outside, kill the loop
287 + except (
288 + RepairableException
289 + ) as e: # Forward repairable errors to the LLM, maybe it can fix them
290 error_message = errors.format_error(e)
211 - msg_response = self.read_prompt("fw.error.md", error=error_message) # error message template
291 + msg_response = self.read_prompt(
292 + "fw.error.md", error=error_message
293 + ) # error message template
294 await self.append_message(msg_response, human=True)
295 PrintStyle(font_color="red", padding=True).print(msg_response)
296 self.context.log.log(type="error", content=msg_response)
215 -
297 + except Exception as e: # Other exception kill the loop
298 + error_message = errors.format_error(e)
299 + PrintStyle(font_color="red", padding=True).print(error_message)
300 + self.context.log.log(type="error", content=error_message)
301 + raise e # kill the loop
302 +
303 finally:
217 - self.context.streaming_agent = None # unset current streamer
304 + self.context.streaming_agent = None # unset current streamer
305
219 - def read_prompt(self, file:str, **kwargs):
306 + def read_prompt(self, file: str, **kwargs):
307 content = ""
308 if self.config.prompts_subdir:
309 try:
223 - content = files.read_file(files.get_abs_path(f"./prompts/{self.config.prompts_subdir}/{file}"), **kwargs)
310 + content = files.read_file(
311 + files.get_abs_path(
312 + f"./prompts/{self.config.prompts_subdir}/{file}"
313 + ),
314 + **kwargs,
315 + )
316 except Exception as e:
317 pass
318 if not content:
227 - content = files.read_file(files.get_abs_path(f"./prompts/default/{file}"), **kwargs)
319 + content = files.read_file(
320 + files.get_abs_path(f"./prompts/default/{file}"), **kwargs
321 + )
322 return content
323
230 - def get_data(self, field:str):
324 + def get_data(self, field: str):
325 return self.data.get(field, None)
326
233 - def set_data(self, field:str, value):
327 + def set_data(self, field: str, value):
328 self.data[field] = value
329
330 async def append_message(self, msg: str, human: bool = False):
@@ -240,17 +334,23 @@ class Agent:
334 else:
335 new_message = HumanMessage(content=msg) if human else AIMessage(content=msg)
336 self.history.append(new_message)
243 - await self.cleanup_history(self.config.msgs_keep_max, self.config.msgs_keep_start, self.config.msgs_keep_end)
244 - if message_type=="ai":
337 + await self.cleanup_history(
338 + self.config.msgs_keep_max,
339 + self.config.msgs_keep_start,
340 + self.config.msgs_keep_end,
341 + )
342 + if message_type == "ai":
343 self.last_message = msg
344
247 - def concat_messages(self,messages):
345 + def concat_messages(self, messages):
346 return "\n".join([f"{msg.type}: {msg.content}" for msg in messages])
347
250 - async def send_adhoc_message(self, system: str, msg: str, output_label:str):
251 - prompt = ChatPromptTemplate.from_messages([
252 - SystemMessage(content=system),
253 - HumanMessage(content=msg)])
348 + async def call_utility_llm(
349 + self, system: str, msg: str, log_type: Log.Type = "util", output_label: str = ""
350 + ):
351 + prompt = ChatPromptTemplate.from_messages(
352 + [SystemMessage(content=system), HumanMessage(content=msg)]
353 + )
354
355 chain = prompt | self.config.utility_model
356 response = ""
@@ -258,40 +358,53 @@ class Agent:
358 logger = None
359
360 if output_label:
261 - PrintStyle(bold=True, font_color="orange", padding=True, background_color="white").print(f"{self.agent_name}: {output_label}:")
361 + PrintStyle(
362 + bold=True, font_color="orange", padding=True, background_color="white"
363 + ).print(f"{self.agent_name}: {output_label}:")
364 printer = PrintStyle(italic=True, font_color="orange", padding=False)
263 - logger = self.context.log.log(type="adhoc", heading=f"{self.agent_name}: {output_label}:")
365 + logger = self.context.log.log(
366 + type=log_type, heading=f"{self.agent_name}: {output_label}:"
367 + )
368
369 formatted_inputs = prompt.format()
266 - tokens = int(len(formatted_inputs)/4)
370 + tokens = int(len(formatted_inputs) / 4)
371 self.rate_limiter.limit_call_and_input(tokens)
268 -
372 +
373 async for chunk in chain.astream({}):
270 - if self.handle_intervention(): break # wait for intervention and handle it, if paused
374 + await self.handle_intervention() # wait for intervention and handle it, if paused
375
272 - if isinstance(chunk, str): content = chunk
273 - elif hasattr(chunk, "content"): content = str(chunk.content)
274 - else: content = str(chunk)
376 + if isinstance(chunk, str):
377 + content = chunk
378 + elif hasattr(chunk, "content"):
379 + content = str(chunk.content)
380 + else:
381 + content = str(chunk)
382
276 - if printer: printer.stream(content)
277 - response+=content
278 - if logger: logger.update(content=response)
383 + if printer:
384 + printer.stream(content)
385 + response += content
386 + if logger:
387 + logger.update(content=response)
388
280 - self.rate_limiter.set_output_tokens(int(len(response)/4))
389 + self.rate_limiter.set_output_tokens(int(len(response) / 4))
390
391 return response
283 -
392 +
393 def get_last_message(self):
394 if self.history:
395 return self.history[-1]
396
288 - async def replace_middle_messages(self,middle_messages):
397 + async def replace_middle_messages(self, middle_messages):
398 cleanup_prompt = self.read_prompt("fw.msg_cleanup.md")
290 - summary = await self.send_adhoc_message(system=cleanup_prompt,msg=self.concat_messages(middle_messages), output_label="Mid messages cleanup summary")
399 + summary = await self.call_utility_llm(
400 + system=cleanup_prompt,
401 + msg=self.concat_messages(middle_messages),
402 + output_label="Mid messages cleanup summary",
403 + )
404 new_human_message = HumanMessage(content=summary)
405 return [new_human_message]
406
294 - async def cleanup_history(self, max:int, keep_start:int, keep_end:int):
407 + async def cleanup_history(self, max: int, keep_start: int, keep_end: int):
408 if len(self.history) <= max:
409 return self.history
410
@@ -317,14 +430,24 @@ class Agent:
430
431 return self.history
432
320 - async def handle_intervention(self, progress:str=""):
321 - while self.context.paused: await asyncio.sleep(0.1) # wait if paused
322 - if self.intervention_message: # if there is an intervention message, but not yet processed
433 + async def handle_intervention(self, progress: str = ""):
434 + while self.context.paused:
435 + await asyncio.sleep(0.1) # wait if paused
436 + if (
437 + self.intervention_message
438 + ): # if there is an intervention message, but not yet processed
439 msg = self.intervention_message
324 - self.intervention_message = "" # reset the intervention message
325 - if progress.strip(): await self.append_message(progress) # append the response generated so far
326 - user_msg = self.read_prompt("fw.intervention.md", user_message=msg) # format the user intervention template
327 - await self.append_message(user_msg,human=True) # append the intervention message
440 + self.intervention_message = "" # reset the intervention message
441 + if progress.strip():
442 + await self.append_message(
443 + progress
444 + ) # append the response generated so far
445 + user_msg = self.read_prompt(
446 + "fw.intervention.md", user_message=msg
447 + ) # format the user intervention template
448 + await self.append_message(
449 + user_msg, human=True
450 + ) # append the intervention message
451 raise InterventionException(msg)
452
453 async def process_tools(self, msg: str):
@@ -335,65 +458,51 @@ class Agent:
458 tool_name = tool_request.get("tool_name", "")
459 tool_args = tool_request.get("tool_args", {})
460 tool = self.get_tool(tool_name, tool_args, msg)
338 -
339 - await self.handle_intervention() # wait if paused and handle intervention message if needed
461 +
462 + await self.handle_intervention() # wait if paused and handle intervention message if needed
463 await tool.before_execution(**tool_args)
341 - await self.handle_intervention() # wait if paused and handle intervention message if needed
464 + await self.handle_intervention() # wait if paused and handle intervention message if needed
465 response = await tool.execute(**tool_args)
343 - await self.handle_intervention() # wait if paused and handle intervention message if needed
466 + await self.handle_intervention() # wait if paused and handle intervention message if needed
467 await tool.after_execution(response)
345 - await self.handle_intervention() # wait if paused and handle intervention message if needed
346 - if response.break_loop: return response.message
468 + await self.handle_intervention() # wait if paused and handle intervention message if needed
469 + if response.break_loop:
470 + return response.message
471 else:
472 msg = self.read_prompt("fw.msg_misformat.md")
473 await self.append_message(msg, human=True)
474 PrintStyle(font_color="red", padding=True).print(msg)
351 - self.context.log.log(type="error", content=f"{self.agent_name}: Message misformat:")
475 + self.context.log.log(
476 + type="error", content=f"{self.agent_name}: Message misformat:"
477 + )
478
479 + def log_from_stream(self, stream: str, logItem: Log.LogItem):
480 + try:
481 + if len(stream) < 25:
482 + return # no reason to try
483 + response = DirtyJson.parse_string(stream)
484 + if isinstance(response, dict):
485 + logItem.update(
486 + content=stream, kvps=response
487 + ) # log if result is a dictionary already
488 + except Exception as e:
489 + pass
490
491 def get_tool(self, name: str, args: dict, message: str, **kwargs):
355 - from python.tools.unknown import Unknown
492 + from python.tools.unknown import Unknown
493 from python.helpers.tool import Tool
357 -
358 - tool_class = Unknown
359 - if files.exists("python/tools",f"{name}.py"):
360 - module = importlib.import_module("python.tools." + name) # Import the module
361 - class_list = inspect.getmembers(module, inspect.isclass) # Get all functions in the module
362 -
363 - for cls in class_list:
364 - if cls[1] is not Tool and issubclass(cls[1], Tool):
365 - tool_class = cls[1]
366 - break
494
495 + classes = extract_tools.load_classes_from_folder(
496 + "python/tools", name + ".py", Tool
497 + )
498 + tool_class = classes[0] if classes else Unknown
499 return tool_class(agent=self, name=name, args=args, message=message, **kwargs)
500
370 - async def fetch_memories(self,reset_skip=False):
371 - if self.config.auto_memory_count<=0: return ""
372 - if reset_skip: self.memory_skip_counter = 0
373 -
374 - if self.memory_skip_counter > 0:
375 - self.memory_skip_counter-=1
376 - return ""
377 - else:
378 - self.memory_skip_counter = self.config.auto_memory_skip
379 - from python.tools import memory_tool
380 - messages = self.concat_messages(self.history)
381 - memories = memory_tool.search(self,messages)
382 - input = {
383 - "conversation_history" : messages,
384 - "raw_memories": memories
385 - }
386 - cleanup_prompt = self.read_prompt("msg.memory_cleanup.md").replace("{", "{{")
387 - clean_memories = await self.send_adhoc_message(cleanup_prompt,json.dumps(input), output_label="Memory injection")
388 - return clean_memories
389 -
390 - def log_from_stream(self, stream: str, logItem: Log.LogItem):
391 - try:
392 - if len(stream) < 25: return # no reason to try
393 - response = DirtyJson.parse_string(stream)
394 - if isinstance(response, dict): logItem.update(content=stream, kvps=response) #log if result is a dictionary already
395 - except Exception as e:
396 - pass
501 + async def call_extensions(self, folder: str, **kwargs) -> Any:
502 + from python.helpers.extension import Extension
503
398 - def call_extension(self, name: str, **kwargs) -> Any:
399 - pass
\ No newline at end of file
504 + classes = extract_tools.load_classes_from_folder(
505 + "python/extensions/" + folder, "*", Extension
506 + )
507 + for cls in classes:
508 + await cls(agent=self).execute(**kwargs)
docker/exe/.bashrc renamed
docker/exe/Dockerfile renamed
docker/exe/build.txt renamed
docker/exe/initialize.sh renamed
docker/run/.bashrc new
+9
@@ -0,0 +1,9 @@
1 +# .bashrc
2 +
3 +# Source global definitions
4 +if [ -f /etc/bashrc ]; then
5 + . /etc/bashrc
6 +fi
7 +
8 +# Activate the virtual environment
9 +source /opt/venv/bin/activate
docker/run/Dockerfile new
+42
@@ -0,0 +1,42 @@
1 +# Use the latest slim version of Debian
2 +FROM --platform=$TARGETPLATFORM debian:bookworm-slim
3 +
4 +# Set ARG for platform-specific commands
5 +ARG TARGETPLATFORM
6 +
7 +# Update and install necessary packages
8 +RUN apt-get update && apt-get install -y \
9 + python3 \
10 + python3-pip \
11 + python3-venv \
12 + nodejs \
13 + npm \
14 + openssh-server \
15 + sudo \
16 + git \
17 + && rm -rf /var/lib/apt/lists/*
18 +
19 +# Set up SSH
20 +RUN mkdir /var/run/sshd && \
21 + echo 'root:toor' | chpasswd && \
22 + sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
23 +
24 +# Create and activate Python virtual environment
25 +ENV VIRTUAL_ENV=/opt/venv
26 +RUN python3 -m venv $VIRTUAL_ENV
27 +
28 +# Copy initial .bashrc with virtual environment activation to a temporary location
29 +COPY .bashrc /etc/skel/.bashrc
30 +
31 +# Copy the script to ensure .bashrc is in the root directory
32 +COPY initialize.sh /usr/local/bin/initialize.sh
33 +RUN chmod +x /usr/local/bin/initialize.sh
34 +
35 +# Ensure the virtual environment and pip setup
36 +RUN $VIRTUAL_ENV/bin/pip install --upgrade pip
37 +
38 +# Expose SSH port
39 +EXPOSE 22
40 +
41 +# Init .bashrc
42 +CMD ["/usr/local/bin/initialize.sh"]
\ No newline at end of file
docker/run/build.txt new
+1
@@ -0,0 +1 @@
1 +docker buildx build --platform linux/amd64,linux/arm64 -t frdel/agent-zero-exe:latest --push .
\ No newline at end of file
docker/run/initialize.sh new
+18
@@ -0,0 +1,18 @@
1 +#!/bin/bash
2 +
3 +# Ensure .bashrc is in the root directory
4 +if [ ! -f /root/.bashrc ]; then
5 + cp /etc/skel/.bashrc /root/.bashrc
6 + chmod 444 /root/.bashrc
7 +fi
8 +
9 +# Ensure .profile is in the root directory
10 +if [ ! -f /root/.profile ]; then
11 + cp /etc/skel/.bashrc /root/.profile
12 + chmod 444 /root/.profile
13 +fi
14 +
15 +apt-get update
16 +
17 +# Start SSH service
18 +exec /usr/sbin/sshd -D
example.env
+1 -1
@@ -1,4 +1,4 @@
1 -API_KEY_OPENAI=
1 +API_KEY_OPENAI=sk-hyBlbkFJCJjaYGCbqPTyT3uaYGCbqFBlbkFJCyJCyuPhYGCb
2 API_KEY_ANTHROPIC=
3 API_KEY_GROQ=
4 API_KEY_PERPLEXITY=
initialize.py
+3 -3
@@ -5,13 +5,13 @@ def initialize():
5
6 # main chat model used by agents (smarter, more accurate)
7 chat_llm = models.get_openai_chat(model_name="gpt-4o-mini", temperature=0)
8 - # chat_llm = models.get_ollama_chat(model_name="gemma2:latest", temperature=0)
8 + # chat_llm = models.get_ollama_chat(model_name="llama3.1", temperature=0)
9 # chat_llm = models.get_lmstudio_chat(model_name="TheBloke/Mistral-7B-Instruct-v0.2-GGUF", temperature=0)
10 - # chat_llm = models.get_openrouter(model_name="meta-llama/llama-3-8b-instruct:free")
10 + # chat_llm = models.get_openrouter_chat(model_name="google/gemini-flash-1.5-exp")
11 # chat_llm = models.get_azure_openai_chat(deployment_name="gpt-4o-mini", temperature=0)
12 # chat_llm = models.get_anthropic_chat(model_name="claude-3-5-sonnet-20240620", temperature=0)
13 # chat_llm = models.get_google_chat(model_name="gemini-1.5-flash", temperature=0)
14 - # chat_llm = models.get_groq_chat(model_name="llama-3.1-70b-versatile", temperature=0)
14 + # chat_llm = models.get_groq_chat(model_name="llama3-8b-8192", temperature=0)
15
16 # utility model used for helper functions (cheaper, faster)
17 utility_llm = chat_llm # change if you want to use a different utility model
models.py
+21 -36
@@ -2,6 +2,7 @@ import os
2 from dotenv import load_dotenv
3 from langchain_openai import ChatOpenAI, OpenAI, OpenAIEmbeddings, AzureChatOpenAI, AzureOpenAIEmbeddings, AzureOpenAI
4 from langchain_community.llms.ollama import Ollama
5 +from langchain_ollama import ChatOllama
6 from langchain_community.embeddings import OllamaEmbeddings
7 from langchain_anthropic import ChatAnthropic
8 from langchain_groq import ChatGroq
@@ -22,11 +23,12 @@ def get_api_key(service):
23
24
25 # Ollama models
25 -def get_ollama_chat(model_name:str, temperature=DEFAULT_TEMPERATURE, base_url=os.getenv("OLLAMA_BASE_URL")):
26 - return Ollama(model=model_name,temperature=temperature, base_url=base_url)
26 +def get_ollama_chat(model_name:str, temperature=DEFAULT_TEMPERATURE, base_url=os.getenv("OLLAMA_BASE_URL") or "http://127.0.0.1:11434", num_ctx=8192):
27 + return ChatOllama(model=model_name,temperature=temperature, base_url=base_url, num_ctx=num_ctx)
28
28 -def get_ollama_embedding(model_name:str, temperature=DEFAULT_TEMPERATURE):
29 - return OllamaEmbeddings(model=model_name,temperature=temperature)
29 +def get_ollama_embedding(model_name:str, temperature=DEFAULT_TEMPERATURE, base_url=os.getenv("OLLAMA_BASE_URL") or "http://127.0.0.1:11434"):
30 +
31 + return OllamaEmbeddings(model=model_name,temperature=temperature, base_url=base_url)
32
33 # HuggingFace models
34
@@ -34,63 +36,46 @@ def get_huggingface_embedding(model_name:str):
36 return HuggingFaceEmbeddings(model_name=model_name)
37
38 # LM Studio and other OpenAI compatible interfaces
37 -def get_lmstudio_chat(model_name:str, base_url=os.getenv("LM_STUDIO_BASE_URL"), temperature=DEFAULT_TEMPERATURE):
39 +def get_lmstudio_chat(model_name:str, temperature=DEFAULT_TEMPERATURE, base_url=os.getenv("LM_STUDIO_BASE_URL") or "http://127.0.0.1:1234/v1"):
40 return ChatOpenAI(model_name=model_name, base_url=base_url, temperature=temperature, api_key="none") # type: ignore
41
40 -def get_lmstudio_embedding(model_name:str, base_url=os.getenv("LM_STUDIO_BASE_URL")):
42 +def get_lmstudio_embedding(model_name:str, base_url=os.getenv("LM_STUDIO_BASE_URL") or "http://127.0.0.1:1234/v1"):
43 return OpenAIEmbeddings(model_name=model_name, base_url=base_url) # type: ignore
44
45 # Anthropic models
44 -def get_anthropic_chat(model_name:str, api_key=None, temperature=DEFAULT_TEMPERATURE):
45 - api_key = api_key or get_api_key("anthropic")
46 +def get_anthropic_chat(model_name:str, api_key=get_api_key("anthropic"), temperature=DEFAULT_TEMPERATURE):
47 return ChatAnthropic(model_name=model_name, temperature=temperature, api_key=api_key) # type: ignore
48
49 # OpenAI models
49 -def get_openai_chat(model_name:str, api_key=None, temperature=DEFAULT_TEMPERATURE):
50 - api_key = api_key or get_api_key("openai")
50 +def get_openai_chat(model_name:str, api_key=get_api_key("openai"), temperature=DEFAULT_TEMPERATURE):
51 return ChatOpenAI(model_name=model_name, temperature=temperature, api_key=api_key) # type: ignore
52
53 -def get_openai_instruct(model_name:str,api_key=None, temperature=DEFAULT_TEMPERATURE):
54 - api_key = api_key or get_api_key("openai")
53 +def get_openai_instruct(model_name:str, api_key=get_api_key("openai"), temperature=DEFAULT_TEMPERATURE):
54 return OpenAI(model=model_name, temperature=temperature, api_key=api_key) # type: ignore
55
57 -def get_openai_embedding(model_name:str, api_key=None):
58 - api_key = api_key or get_api_key("openai")
56 +def get_openai_embedding(model_name:str, api_key=get_api_key("openai")):
57 return OpenAIEmbeddings(model=model_name, api_key=api_key) # type: ignore
58
61 -def get_azure_openai_chat(deployment_name:str, api_key=None, temperature=DEFAULT_TEMPERATURE, azure_endpoint=None):
62 - api_key = api_key or get_api_key("openai_azure")
63 - azure_endpoint = azure_endpoint or os.getenv("OPENAI_AZURE_ENDPOINT")
59 +def get_azure_openai_chat(deployment_name:str, api_key=get_api_key("openai_azure"), temperature=DEFAULT_TEMPERATURE, azure_endpoint=os.getenv("OPENAI_AZURE_ENDPOINT")):
60 return AzureChatOpenAI(deployment_name=deployment_name, temperature=temperature, api_key=api_key, azure_endpoint=azure_endpoint) # type: ignore
61
66 -def get_azure_openai_instruct(deployment_name:str, api_key=None, temperature=DEFAULT_TEMPERATURE, azure_endpoint=None):
67 - api_key = api_key or get_api_key("openai_azure")
68 - azure_endpoint = azure_endpoint or os.getenv("OPENAI_AZURE_ENDPOINT")
62 +def get_azure_openai_instruct(deployment_name:str, api_key=get_api_key("openai_azure"), temperature=DEFAULT_TEMPERATURE, azure_endpoint=os.getenv("OPENAI_AZURE_ENDPOINT")):
63 return AzureOpenAI(deployment_name=deployment_name, temperature=temperature, api_key=api_key, azure_endpoint=azure_endpoint) # type: ignore
64
71 -def get_azure_openai_embedding(deployment_name:str, api_key=None, azure_endpoint=None):
72 - api_key = api_key or get_api_key("openai_azure")
73 - azure_endpoint = azure_endpoint or os.getenv("OPENAI_AZURE_ENDPOINT")
65 +def get_azure_openai_embedding(deployment_name:str, api_key=get_api_key("openai_azure"), azure_endpoint=os.getenv("OPENAI_AZURE_ENDPOINT")):
66 return AzureOpenAIEmbeddings(deployment_name=deployment_name, api_key=api_key, azure_endpoint=azure_endpoint) # type: ignore
67
68 # Google models
77 -def get_google_chat(model_name:str, api_key=None, temperature=DEFAULT_TEMPERATURE):
78 - api_key = api_key or get_api_key("google")
69 +def get_google_chat(model_name:str, api_key=get_api_key("google"), temperature=DEFAULT_TEMPERATURE):
70 return GoogleGenerativeAI(model=model_name, temperature=temperature, google_api_key=api_key, safety_settings={HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE }) # type: ignore
71
72 # Groq models
82 -def get_groq_chat(model_name:str, api_key=None, temperature=DEFAULT_TEMPERATURE):
83 - api_key = api_key or get_api_key("groq")
73 +def get_groq_chat(model_name:str, api_key=get_api_key("groq"), temperature=DEFAULT_TEMPERATURE):
74 return ChatGroq(model_name=model_name, temperature=temperature, api_key=api_key) # type: ignore
75
76 # OpenRouter models
87 -def get_openrouter(model_name: str="meta-llama/llama-3.1-8b-instruct:free", api_key=None, temperature=DEFAULT_TEMPERATURE):
88 - api_key = api_key or get_api_key("openrouter")
89 - return ChatOpenAI(api_key=api_key, base_url=os.getenv("OPEN_ROUTER_BASE_URL"), model=model_name, temperature=temperature) # type: ignore
77 +def get_openrouter_chat(model_name: str, api_key=get_api_key("openrouter"), temperature=DEFAULT_TEMPERATURE, base_url=os.getenv("OPEN_ROUTER_BASE_URL") or "https://openrouter.ai/api/v1"):
78 + return ChatOpenAI(api_key=api_key, model=model_name, temperature=temperature, base_url=base_url) # type: ignore
79
91 -def get_embedding_hf(model_name="sentence-transformers/all-MiniLM-L6-v2"):
92 - return HuggingFaceEmbeddings(model_name=model_name)
93 -
94 -def get_embedding_openai(api_key=None):
95 - api_key = api_key or get_api_key("openai")
96 - return OpenAIEmbeddings(api_key=api_key) #type: ignore
80 +def get_openrouter_embedding(model_name: str, api_key=get_api_key("openrouter"), base_url=os.getenv("OPEN_ROUTER_BASE_URL") or "https://openrouter.ai/api/v1"):
81 + return OpenAIEmbeddings(model=model_name, api_key=api_key, base_url=base_url) # type: ignore
\ No newline at end of file
prompts/default/agent.memory.md deleted
-4
@@ -1,4 +0,0 @@
1 -# Memories
2 -- following are your memories on the current topic
3 -
4 -{{memories}}
\ No newline at end of file
prompts/default/agent.system.solutions.md new
+4
@@ -0,0 +1,4 @@
1 +# Solutions in the past
2 +- following are your memories about successful solutions of related problems:
3 +
4 +{{solutions}}
\ No newline at end of file
prompts/default/agent.system.tools.md renamed
+1 -1
@@ -150,7 +150,7 @@ This tool can be used to achieve any task that requires computation, or any othe
150 Place your code escaped and properly indented in the "code" argument.
151 Select the corresponding runtime with "runtime" argument. Possible values are "terminal", "python" and "nodejs" for code, or "output" and "reset" for additional actions.
152 Sometimes a dialogue can occur in output, questions like Y/N, in that case use the "teminal" runtime in the next step and send your answer.
153 -If the code is running long, you can use runtime "output" to wait for the output or "reset" to restart the terminal if the program hangs or terminal stops responding.
153 +If the code is running long, you can use runtime "output" to wait for next output part or use runtime "reset" to kill the process.
154 You can use pip, npm and apt-get in terminal runtime to install any required packages.
155 IMPORTANT: Never use implicit print or implicit output, it does not work! If you need output of your code, you MUST use print() or console.log() to output selected variables.
156 When tool outputs error, you need to change your code accordingly before trying again. knowledge_tool can help analyze errors.
prompts/default/fw.memory.hist_suc.sys.md new
+23
@@ -0,0 +1,23 @@
1 +# Assistant's job
2 +1. The assistant receives a history of conversation between USER and AGENT
3 +2. Assistant searches for succesful technical solutions by the AGENT
4 +3. Assistant writes notes about the succesful solution for later reproduction
5 +
6 +# Format
7 +- The response format is a JSON array of succesfull solutions containng "problem" and "solution" properties
8 +- The problem section contains a description of the problem, the solution section contains step by step instructions to solve the problem including necessary details and code.
9 +- If the history does not contain any helpful technical solutions, the response will be an empty JSON array.
10 +
11 +# Example
12 +```json
13 +[
14 + {
15 + "problem": "Task is to download a video from YouTube. A video URL is specified by the user.",
16 + "solution": "1. Install yt-dlp library using 'pip install yt-dlp'\n2. Download the video using yt-dlp command: 'yt-dlp YT_URL', replace YT_URL with your video URL."
17 + }
18 +]
19 +```
20 +
21 +# Rules
22 +- Focus on important details like libraries used, code, encountered issues, error fixing etc.
23 +- Do not include simple solutions that don't require instructions to reproduce like file handling, web search etc.
\ No newline at end of file
prompts/default/fw.memory.hist_sum.sys.md new
+26
@@ -0,0 +1,26 @@
1 +# Assistant's job
2 +1. The assistant receives a history of conversation between USER and AGENT
3 +2. Assistant writes a summary that will serve as a search index later
4 +3. Assistant responds with the summary plain text without any formatting or own thoughts or phrases
5 +
6 +The goal is to provide shortest possible summary containing all key elements that can be searched later.
7 +For this reason all long texts like code, results, contents will be removed.
8 +
9 +# Format
10 +- The response format is plain text containing only the summary of the conversation
11 +- No formatting
12 +- Do not write any introduction or conclusion, no additional text unrelated to the summary itself
13 +
14 +# Rules
15 +- Important details such as identifiers must be preserved in the summary as they can be used for search
16 +- Unimportant details, phrases, fillers, redundant text, etc. should be removed
17 +
18 +# Must be preserved:
19 +- Keywords, names, IDs, URLs, etc.
20 +- Technologies used, libraries used
21 +
22 +# Must be removed:
23 +- Full code
24 +- File contents
25 +- Search results
26 +- Long outputs
\ No newline at end of file
prompts/default/memory.solutions_query.sys.md new
+19
@@ -0,0 +1,19 @@
1 +# AI's job
2 +1. The AI receives a MESSAGE from USER and short conversation HISTORY for reference
3 +2. AI analyzes the intention of the USER based on MESSAGE and HISTORY
4 +3. AI provide a search query for search engine where previous solutions are stored
5 +
6 +# Format
7 +- The response format is a plain text string containing the query
8 +- No other text, no formatting
9 +
10 +# Example
11 +```json
12 +USER: "I want to download a video from YouTube. A video URL is specified by the user."
13 +AI: "download youtube video"
14 +USER: "Now compress all files in that folder"
15 +AI: "compress files in folder"
16 +```
17 +
18 +# HISTORY:
19 +{{history}}
\ No newline at end of file
prompts/default/memory.solutions_sum.sys.md new
+24
@@ -0,0 +1,24 @@
1 +# Assistant's job
2 +1. The assistant receives a history of conversation between USER and AGENT
3 +2. Assistant searches for succesful technical solutions by the AGENT
4 +3. Assistant writes notes about the succesful solution for later reproduction
5 +
6 +# Format
7 +- The response format is a JSON array of succesfull solutions containng "problem" and "solution" properties
8 +- The problem section contains a description of the problem, the solution section contains step by step instructions to solve the problem including necessary details and code.
9 +- If the history does not contain any helpful technical solutions, the response will be an empty JSON array.
10 +
11 +# Example
12 +~~~json
13 +[
14 + {
15 + "problem": "Task is to download a video from YouTube. A video URL is specified by the user.",
16 + "solution": "1. Install yt-dlp library using 'pip install yt-dlp'\n2. Download the video using yt-dlp command: 'yt-dlp YT_URL', replace YT_URL with your video URL."
17 + }
18 +]
19 +~~~
20 +
21 +# Rules
22 +- Focus on important details like libraries used, code, encountered issues, error fixing etc.
23 +- Do not include simple solutions that don't require instructions to reproduce like file handling, web search etc.
24 +- Do not add your own details that are not specifically mentioned in the history
\ No newline at end of file
python/extensions/message_loop_prompts/recall_solutions.py new
+73
@@ -0,0 +1,73 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +from python.helpers.files import read_file
4 +from python.helpers.vector_db import get_or_create
5 +import json
6 +
7 +
8 +class RecallSolutions(Extension):
9 +
10 + INTERVAL = 3
11 + HISTORY = 5
12 + RESULTS = 3
13 + THRESHOLD = 0.1
14 +
15 + async def execute(self, loop_data={}, **kwargs):
16 +
17 + iter = loop_data.get("iteration", 0)
18 + if iter % RecallSolutions.INTERVAL == 0: # every 3 iterations (or the first one) recall solution memories
19 + await self.search_solutions(loop_data=loop_data, **kwargs)
20 +
21 +
22 + async def search_solutions(self, loop_data={}, **kwargs):
23 + self.agent.context.log.log(
24 + type="info", content="Searching memory for solutions...", temp=False
25 + )
26 +
27 + # get system message and chat history for util llm
28 + msgs_text = self.agent.concat_messages(
29 + self.agent.history[-RecallSolutions.HISTORY:]
30 + ) # only last X messages
31 + system = self.agent.read_prompt(
32 + "memory.solutions_query.sys.md", history=msgs_text
33 + )
34 +
35 + # call util llm to summarize conversation
36 + query = await self.agent.call_utility_llm(
37 + system=system, msg=loop_data["message"]
38 + )
39 +
40 + # get solutions database
41 + vdb = get_or_create(
42 + logger=self.agent.context.log,
43 + embeddings_model=self.agent.config.embeddings_model,
44 + memory_dir="./memory/solutions",
45 + knowledge_dir="",
46 + )
47 +
48 + solutions = vdb.search_similarity_threshold(
49 + query=query, results=RecallSolutions.RESULTS, threshold=RecallSolutions.THRESHOLD
50 + )
51 +
52 + if not isinstance(solutions, list) or len(solutions) == 0:
53 + self.agent.context.log.log(
54 + type="info", content="No successful solution memories found.", temp=False
55 + )
56 + return
57 + else:
58 + self.agent.context.log.log(
59 + type="info",
60 + content=f"{len(solutions)} successful solution memories found.",
61 + temp=False,
62 + )
63 +
64 + # concatenate solution.page_content in solutions:
65 + solutions_text = ""
66 + for solution in solutions:
67 + solutions_text += solution.page_content + "\n\n"
68 +
69 + # place to prompt
70 + solutions_prompt = self.agent.read_prompt("agent.system.solutions.md", solutions=solutions_text)
71 +
72 + # append to system message
73 + loop_data["system"] += solutions_prompt
python/extensions/monologue_end/memorize_solutions.py new
+70
@@ -0,0 +1,70 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +from python.helpers.files import read_file
4 +from python.helpers.vector_db import get_or_create
5 +import json
6 +from python.helpers.dirty_json import DirtyJson
7 +from python.helpers import errors
8 +
9 +
10 +class MemorizeSolutions(Extension):
11 +
12 + async def execute(self, loop_data={}, **kwargs):
13 + try:
14 + self.agent.context.log.log(
15 + type="info", content="Memorizing succesful solutions...", temp=True
16 + )
17 +
18 + # get system message and chat history for util llm
19 + system = self.agent.read_prompt("memory.solutions_sum.sys.md")
20 + msgs_text = self.agent.concat_messages(self.agent.history)
21 +
22 + # call util llm to find solutions in history
23 + solutions_json = await self.agent.call_utility_llm(
24 + system=system,
25 + msg=msgs_text,
26 + log_type="util",
27 + )
28 +
29 + solutions = DirtyJson.parse_string(solutions_json)
30 +
31 + if not isinstance(solutions, list) or len(solutions) == 0:
32 + self.agent.context.log.log(
33 + type="info", content="No succesful solutions found.", temp=False
34 + )
35 + return
36 + else:
37 + self.agent.context.log.log(
38 + type="info",
39 + content=f"{len(solutions)} succesful solutions found.",
40 + temp=True,
41 + )
42 +
43 + # save chat history
44 + vdb = get_or_create(
45 + logger=self.agent.context.log,
46 + embeddings_model=self.agent.config.embeddings_model,
47 + memory_dir="./memory/solutions",
48 + knowledge_dir="",
49 + )
50 +
51 + for solution in solutions:
52 + # solution to plain text:
53 + txt = (
54 + f"Problem: {solution['problem']}\nSolution: {solution['solution']}"
55 + )
56 + vdb.insert_text(
57 + text=txt,
58 + ) # metadata={"full": msgs_text})
59 +
60 + self.agent.context.log.log(
61 + type="info",
62 + content=f"{len(solutions)} solutions memorized.",
63 + temp=False,
64 + )
65 +
66 + except Exception as e:
67 + err = errors.format_error(e)
68 + self.agent.context.log.log(
69 + type="error", heading="Memorize solutions extension error:", content=err
70 + )
python/extensions/monologue_end/zero_memorize_history.py_ new
+45
@@ -0,0 +1,45 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +from python.helpers.files import read_file
4 +from python.helpers.vector_db import get_or_create
5 +import json
6 +
7 +
8 +class MemorizeHistory(Extension):
9 +
10 + async def execute(self, **kwargs):
11 + if self.agent.number != 0:
12 + return # only agent 0 will memorize chat history with user
13 +
14 + self.agent.context.log.log(
15 + type="info", content="Memorizing chat history...", temp=True
16 + )
17 +
18 + # get system message and chat history for util llm
19 + system = self.agent.read_prompt("fw.memory.hist_sum.sys.md")
20 + # msgs = []
21 + # for msg in self.agent.history:
22 + # content = msg.get("content", "")
23 + # if content:
24 + # msgs.append(content)
25 + # msgs_text = json.dumps(msgs)
26 + msgs_text = self.agent.concat_messages(self.agent.history)
27 +
28 + # call util llm to summarize conversation
29 + summary = await self.agent.call_utility_llm(
30 + system=system, msg=msgs_text, output_label=""
31 + )
32 +
33 + # save chat history
34 + vdb = get_or_create(
35 + logger=self.agent.context.log,
36 + embeddings_model=self.agent.config.embeddings_model,
37 + memory_dir="./memory/history",
38 + knowledge_dir="",
39 + )
40 +
41 + vdb.insert_text(text=summary, metadata={"full": msgs_text})
42 +
43 + self.agent.context.log.log(
44 + type="info", content="Chat history memorized.", temp=True
45 + )
python/extensions/msg_loop_break/zero_memorize_history.py new
+35
@@ -0,0 +1,35 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +from python.helpers.files import read_file
4 +from python.helpers.vector_db import VectorDB
5 +import json
6 +
7 +class MemorizeHistory(Extension):
8 +
9 + async def execute(self, **kwargs):
10 + if self.agent.number != 0: return #only agent 0 will memorize chat history with user
11 +
12 + self.agent.context.log.log(type="info", content="Memorizing chat history...", temp=True)
13 +
14 + #get system message and chat history for util llm
15 + system = self.agent.read_prompt("fw.memory.hist_sum.sys")
16 + msgs = []
17 + for msg in self.agent.history:
18 + content = msg.get("content", "")
19 + if content:
20 + msgs.append(content)
21 + msgs_json = json.dumps(msgs)
22 +
23 + #call util llm to summarize conversation
24 + summary = await self.agent.call_utility_llm(system=system,msg=msgs_json,output_label="")
25 +
26 + #save chat history
27 + vdb = VectorDB(
28 + logger=self.agent.context.log,
29 + embeddings_model=self.agent.config.embeddings_model,
30 + memory_dir="./memory/history",
31 + knowledge_dir=""
32 + )
33 +
34 + self.agent.context.log.log(type="info", content="Chat history memorized.", temp=True)
35 +
\ No newline at end of file
python/helpers/dirty_json.py
+6 -3
@@ -17,7 +17,7 @@ class DirtyJson:
17 def parse(self, json_string):
18 self._reset()
19 self.json_string = json_string
20 - self.index = self.index_of_first_brace(self.json_string) #skip any text up to the first brace
20 + self.index = self.get_start_pos(self.json_string) #skip any text up to the first brace
21 self.current_char = self.json_string[self.index]
22 self._parse()
23 return self.result
@@ -260,5 +260,8 @@ class DirtyJson:
260 break
261 return result
262
263 - def index_of_first_brace(self, input_str: str) -> int:
264 - return input_str.find("{")
263 + def get_start_pos(self, input_str: str) -> int:
264 + chars = ["{", "[", '"']
265 + indices = [input_str.find(char) for char in chars if input_str.find(char) != -1]
266 + return min(indices) if indices else 0
267 +
python/helpers/docker.py
+2 -2
@@ -56,7 +56,7 @@ class DockerContainerManager:
56 if existing_container:
57 if existing_container.status != 'running':
58 print(f"Starting existing container: {self.name} for safe code execution...")
59 - self.logger.log(type="info", content=f"Starting existing container: {self.name} for safe code execution...")
59 + self.logger.log(type="info", content=f"Starting existing container: {self.name} for safe code execution...", temp=True)
60
61 existing_container.start()
62 self.container = existing_container
@@ -67,7 +67,7 @@ class DockerContainerManager:
67 # print(f"Container with name '{self.name}' is already running with ID: {existing_container.id}")
68 else:
69 print(f"Initializing docker container {self.name} for safe code execution...")
70 - self.logger.log(type="info", content=f"Initializing docker container {self.name} for safe code execution...")
70 + self.logger.log(type="info", content=f"Initializing docker container {self.name} for safe code execution...", temp=True)
71
72 self.container = self.client.containers.run(
73 self.image,
python/helpers/extension.py new
+13
@@ -0,0 +1,13 @@
1 +from abc import abstractmethod
2 +from typing import Any
3 +from agent import Agent
4 +
5 +class Extension:
6 +
7 + def __init__(self, agent: Agent, *args, **kwargs):
8 + self.agent = agent
9 + self.kwargs = kwargs
10 +
11 + @abstractmethod
12 + async def execute(self, **kwargs) -> Any:
13 + pass
\ No newline at end of file
python/helpers/extract_tools.py
+19 -146
@@ -1,15 +1,12 @@
1 -import re, os
2 -from typing import Any
3 -from . import files
4 -# import dirtyjson
1 +import re, os, importlib, inspect
2 +from typing import Any, Type, TypeVar
3 from .dirty_json import DirtyJson
4 import regex
7 -
5 +from fnmatch import fnmatch
6
7 def json_parse_dirty(json:str) -> dict[str,Any] | None:
8 ext_json = extract_json_object_string(json)
9 if ext_json:
12 - # ext_json = fix_json_string(ext_json)
10 data = DirtyJson.parse_string(ext_json)
11 if isinstance(data,dict): return data
12 return None
@@ -50,150 +47,26 @@ def fix_json_string(json_string):
47 fixed_string = re.sub(r'(?<=: ")(.*?)(?=")', replace_unescaped_newlines, json_string, flags=re.DOTALL)
48 return fixed_string
49
53 -# def extract_tool_requests2(response):
54 -# # Regex to match the tags ending with $, allowing for varying whitespace
55 -# pattern = r'<(\w+)\$[\s]*(.*?)>([\s\S]*?)(?=<\w+\$|<\/\1\$|$)'
56 -# matches = re.findall(pattern, response, re.DOTALL)
57 -
58 -# tool_usages = []
59 -# allowed_tags = list_python_files("tools")
60 -
61 -# for match in matches:
62 -# tag_name, attributes, content = match
50
64 -# if tag_name not in allowed_tags: continue
65 -
66 -# tool_dict = {}
67 -# tool_dict['name'] = tag_name
68 -# tool_dict['args'] = {}
69 -
70 -# # Parse attributes
71 -# for attr in re.findall(r'(\w+)\s*=\s*"([^"]+)"', attributes):
72 -# tool_dict['args'][attr[0]] = attr[1]
73 -
74 -# # Add body content
75 -# tool_dict["content"] = content.strip()
76 -# tool_dict["index"] = len(tool_usages)
77 -# tool_usages.append(tool_dict)
78 -
79 -# return tool_usages
51 +T = TypeVar('T') # Define a generic type variable
52
81 -# def extract_tool_requests(response):
82 -# # Regex to match the tool blocks, allowing for varying whitespace
83 -# pattern = r'<tool\$[\s]*(.*?)>(.*?)<\/tool\$\s*>'
84 -# matches = re.findall(pattern, response, re.DOTALL)
85 -
86 -# tool_usages = []
87 -
88 -# for match in matches:
89 -# attributes, body = match
90 -# tool_dict = {}
91 -# # Parse attributes
92 -# for attr in re.findall(r'(\w+)\s*=\s*"([^"]+)"', attributes):
93 -# tool_dict[attr[0]] = attr[1]
94 -# # Add body content
95 -# tool_dict["body"] = body.strip()
96 -# tool_usages.append(tool_dict)
97 -
98 -# return tool_usages
53 +def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T]) -> list[Type[T]]:
54
100 -# def extract_specified_tags(response):
55 + classes = []
56
102 -# allowed_tags = list_python_files("tools")
103 -
104 -# # Create a regex pattern to match specified tags and their attributes
105 -# pattern = r'<({})([\s\S]*?)>'.format('|'.join(allowed_tags))
106 -# matches = re.findall(pattern, response, re.DOTALL)
107 -
108 -# extracted_tags = []
109 -
110 -# for match in matches:
111 -# tag_name, attributes = match
112 -# tag_dict = {}
113 -# tag_dict['name'] = tag_name
114 -
115 -# # Parse attributes
116 -# for attr in re.findall(r'(\w+)\s*=\s*"([^"]+)"', attributes):
117 -# tag_dict[attr[0]] = attr[1]
118 -
119 -# # Extract the body text (everything after the tag until the next tag or end of string)
120 -# body_pattern = r'<{0}[\s\S]*?>([\s\S]*?)(?=<|$)'.format(tag_name)
121 -# body_match = re.search(body_pattern, response, re.DOTALL)
122 -# tag_dict['body'] = body_match.group(1).strip() if body_match else ''
123 -
124 -# extracted_tags.append(tag_dict)
125 -
126 -# return extracted_tags
127 -
128 -# def list_python_files(directory):
129 -# # List all files in the given directory
130 -# list = os.listdir(files.get_abs_path(directory))
131 -# # Filter for Python files and remove the extension
132 -# python_files = { os.path.splitext(file)[0] for file in list if file.endswith('.py') }
133 -# return python_files
134 -
135 -# import re
136 -# from xml.etree import ElementTree as ET
137 -
138 -# def extract_tool_usages_advanced(response):
139 -# tool_usages = []
140 -# pattern = re.compile(r'<tool.*?>', re.DOTALL)
141 -
142 -# start_pos = 0
143 -# while start_pos < len(response):
144 -# match = pattern.search(response, start_pos)
145 -# if not match:
146 -# break
147 -
148 -# tag_start = match.start()
149 -# tag_end = match.end()
150 -# end_tag = '</tool>'
151 -
152 -# # To find the corresponding end tag correctly handling nested tags
153 -# depth = 1
154 -# search_pos = tag_end
155 -
156 -# while depth > 0:
157 -# next_open = response.find('<tool', search_pos)
158 -# next_close = response.find(end_tag, search_pos)
159 -
160 -# if next_close == -1:
161 -# break
162 -
163 -# if next_open != -1 and next_open < next_close:
164 -# depth += 1
165 -# search_pos = next_open + len('<tool')
166 -# else:
167 -# depth -= 1
168 -# search_pos = next_close + len(end_tag)
169 -
170 -# end_tag_end = search_pos
171 -
172 -# # Extract the whole tool block
173 -# tool_block = response[tag_start:end_tag_end]
174 -
175 -# try:
176 -# element = ET.fromstring(tool_block)
177 -# tool_dict = element.attrib
178 -# tool_dict["body"] = ET.tostring(element, encoding='unicode', method='xml').split('>', 1)[1].rsplit('<', 1)[0].strip()
179 -# tool_usages.append(tool_dict)
180 -# except ET.ParseError:
181 -# # In case of parsing error, fall back to including entire content between the tags
182 -# body_content = response[tag_end:end_tag_end - len(end_tag)].strip()
183 -# tool_dict = {"name": re.search(r'name="(.*?)"', match.group(0)).group(1), "body": body_content}
184 -# tool_usages.append(tool_dict)
185 -
186 -# start_pos = end_tag_end
57 + # Get all .py files in the folder that match the pattern
58 + for file_name in os.listdir(folder):
59 + if fnmatch(file_name, name_pattern) and file_name.endswith(".py"):
60 + module_name = file_name[:-3] # remove .py extension
61 + module_path = folder.replace(os.sep, ".") + "." + module_name
62 + module = importlib.import_module(module_path)
63
188 -# return tool_usages
64 + # Get all classes in thde module
65 + class_list = inspect.getmembers(module, inspect.isclass)
66
190 -# # Example usage with the given input
191 -# response = """
192 -# <tool name="code_execution_tool">
193 -# #comment <tool<tool name="abc><tool><loot><tool>"
194 -# print(text)
195 -# </tool>
196 -# """
67 + # Filter for classes that are subclasses of the given base_class
68 + for cls in class_list:
69 + if cls[1] is not base_class and issubclass(cls[1], base_class):
70 + classes.append(cls[1])
71
198 -# tool_usages = extract_tool_usages(response)
199 -# print(tool_usages)
\ No newline at end of file
72 + return classes
\ No newline at end of file
python/helpers/files.py
+3 -2
@@ -1,4 +1,4 @@
1 -import os, re, sys
1 +import os, re
2
3 def read_file(relative_path, **kwargs):
4 absolute_path = get_abs_path(relative_path) # Construct the absolute path to the target file
@@ -30,4 +30,5 @@ def exists(*relative_paths):
30 def get_base_dir():
31 # Get the base directory from the current file path
32 base_dir = os.path.dirname(os.path.abspath(os.path.join(__file__,"../../")))
33 - return base_dir
\ No newline at end of file
33 + return base_dir
34 +
python/helpers/log.py
+22 -6
@@ -1,9 +1,21 @@
1 from dataclasses import dataclass, field
2 import json
3 -from typing import Optional, Dict
3 +from typing import Literal, Optional, Dict
4 import uuid
5
6
7 +type Type = Literal[
8 + 'agent',
9 + 'code_exe',
10 + 'error',
11 + 'hint',
12 + 'info',
13 + 'tool',
14 + 'user',
15 + 'util',
16 + 'warning',
17 + ]
18 +
19 @dataclass
20 class LogItem:
21 log: 'Log'
@@ -11,15 +23,16 @@ class LogItem:
23 type: str
24 heading: str
25 content: str
26 + temp: bool
27 kvps: Optional[Dict] = None
28 guid: str = ""
29
30 def __post_init__(self):
31 self.guid = self.log.guid
32
20 - def update(self, type: str | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None):
33 + def update(self, type: Type | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None, temp: bool | None = None):
34 if self.guid == self.log.guid:
22 - self.log.update_item(self.no, type=type, heading=heading, content=content, kvps=kvps)
35 + self.log.update_item(self.no, type=type, heading=heading, content=content, kvps=kvps, temp=temp)
36
37 def output(self):
38 return {
@@ -27,6 +40,7 @@ class LogItem:
40 "type": self.type,
41 "heading": self.heading,
42 "content": self.content,
43 + "temp": self.temp,
44 "kvps": self.kvps
45 }
46
@@ -37,13 +51,13 @@ class Log:
51 self.updates: list[int] = []
52 self.logs: list[LogItem] = []
53
40 - def log(self, type: str, heading: str | None = None, content: str | None = None, kvps: dict | None = None) -> LogItem:
41 - item = LogItem(log=self,no=len(self.logs), type=type, heading=heading or "", content=content or "", kvps=kvps)
54 + def log(self, type: Type, heading: str | None = None, content: str | None = None, kvps: dict | None = None, temp: bool | None = None) -> LogItem:
55 + item = LogItem(log=self,no=len(self.logs), type=type, heading=heading or "", content=content or "", kvps=kvps, temp=temp or False)
56 self.logs.append(item)
57 self.updates += [item.no]
58 return item
59
46 - def update_item(self, no: int, type: str | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None):
60 + def update_item(self, no: int, type: str | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None, temp: bool | None = None):
61 item = self.logs[no]
62 if type is not None:
63 item.type = type
@@ -53,6 +67,8 @@ class Log:
67 item.content = content
68 if kvps is not None:
69 item.kvps = kvps
70 + if temp is not None:
71 + item.temp = temp
72 self.updates += [item.no]
73
74 def output(self, start=None, end=None):
python/helpers/shell_local.py
+4 -2
@@ -44,12 +44,14 @@ class LocalInteractiveSession:
44 self.process.stdin.write(command + '\n') # type: ignore
45 self.process.stdin.flush() # type: ignore
46
47 - async def read_output(self) -> Tuple[str, Optional[str]]:
47 + async def read_output(self, timeout: float = 0) -> Tuple[str, Optional[str]]:
48 if not self.process:
49 raise Exception("Shell not connected")
50
51 partial_output = ''
52 - while True:
52 + start_time = time.time()
53 +
54 + while (timeout <= 0 or time.time() - start_time < timeout):
55 rlist, _, _ = select.select([self.process.stdout], [], [], 0.1)
56 if rlist:
57 line = self.process.stdout.readline() # type: ignore
python/helpers/shell_ssh.py
+5 -8
@@ -42,7 +42,7 @@ class SSHInteractiveSession:
42 errors += 1
43 if errors < 3:
44 print(f"SSH Connection attempt {errors}...")
45 - self.logger.log(type="info", content=f"SSH Connection attempt {errors}...")
45 + self.logger.log(type="info", content=f"SSH Connection attempt {errors}...", temp=True)
46
47 time.sleep(5)
48 else:
@@ -66,14 +66,16 @@ class SSHInteractiveSession:
66 self.trimmed_command_length = 0
67 self.shell.send(self.last_command)
68
69 - async def read_output(self) -> Tuple[str, str]:
69 + async def read_output(self, timeout: float = 0) -> Tuple[str, str]:
70 if not self.shell:
71 raise Exception("Shell not connected")
72
73 partial_output = b''
74 leftover = b''
75 + start_time = time.time()
76 +
77 + while self.shell.recv_ready() and (timeout <= 0 or time.time() - start_time < timeout):
78
76 - while self.shell.recv_ready():
79 data = self.shell.recv(1024)
80
81 # Trim own command from output
@@ -106,11 +108,6 @@ class SSHInteractiveSession:
108 decoded_partial_output = self.clean_string(decoded_partial_output)
109 decoded_full_output = self.clean_string(decoded_full_output)
110
109 - # # Split output at end_comment
110 - # if SSHInteractiveSession.end_comment in decoded_full_output:
111 - # decoded_full_output = decoded_full_output.split(SSHInteractiveSession.end_comment)[-1].lstrip("\r\n")
112 - # decoded_partial_output = decoded_partial_output.split(SSHInteractiveSession.end_comment)[-1].lstrip("\r\n")
113 -
111 return decoded_full_output, decoded_partial_output
112
113
python/helpers/vector_db.py
+106 -57
@@ -1,5 +1,6 @@
1 from langchain.storage import InMemoryByteStore, LocalFileStore
2 from langchain.embeddings import CacheBackedEmbeddings
3 +
4 # from langchain_chroma import Chroma
5 from langchain_community.vectorstores import FAISS
6 import faiss
@@ -12,43 +13,74 @@ import uuid
13 from python.helpers import knowledge_import
14 from python.helpers.log import Log
15
16 +index: dict[str, "VectorDB"] = {}
17 +
18 +
19 +def get_or_create(
20 + logger: Log,
21 + embeddings_model,
22 + in_memory=False,
23 + memory_dir="./memory",
24 + knowledge_dir=None,
25 +):
26 + if index.get(memory_dir) is None:
27 + index[memory_dir] = VectorDB(
28 + logger, embeddings_model, in_memory, memory_dir, knowledge_dir
29 + )
30 + return index[memory_dir]
31 +
32 +
33 class VectorDB:
34
17 - def __init__(self, logger: Log, embeddings_model, in_memory=False, memory_dir="./memory", knowledge_dir="./knowledge"):
35 + def __init__(
36 + self,
37 + logger: Log,
38 + embeddings_model,
39 + in_memory=False,
40 + memory_dir="./memory/default",
41 + knowledge_dir=None,
42 + ):
43 self.logger = logger
44
45 print("Initializing VectorDB...")
21 - self.logger.log("info", content="Initializing VectorDB...")
22 -
46 + self.logger.log("info", content="Initializing VectorDB...", temp=True)
47 +
48 self.embeddings_model = embeddings_model
49
25 - self.em_dir = files.get_abs_path(memory_dir,"embeddings")
26 - self.db_dir = files.get_abs_path(memory_dir,"database")
50 + self.em_dir = files.get_abs_path("./memory/embeddings") # just caching, no need to parameterize
51 + self.db_dir = files.get_abs_path("./memory", memory_dir, "database")
52 self.kn_dir = files.get_abs_path(knowledge_dir) if knowledge_dir else ""
28 -
53 +
54 + # make sure embeddings and database directories exist
55 + os.makedirs(self.db_dir, exist_ok=True)
56 +
57 if in_memory:
58 self.store = InMemoryByteStore()
59 else:
60 + os.makedirs(self.em_dir, exist_ok=True)
61 self.store = LocalFileStore(self.em_dir)
62
34 -
35 - #here we setup the embeddings model with the chosen cache storage
63 + # here we setup the embeddings model with the chosen cache storage
64 self.embedder = CacheBackedEmbeddings.from_bytes_store(
37 - embeddings_model,
38 - self.store,
39 - namespace=getattr(embeddings_model, 'model', getattr(embeddings_model, 'model_name', "default")) )
65 + embeddings_model,
66 + self.store,
67 + namespace=getattr(
68 + embeddings_model,
69 + "model",
70 + getattr(embeddings_model, "model_name", "default"),
71 + ),
72 + )
73
74 # self.db = Chroma(
75 # embedding_function=self.embedder,
76 # persist_directory=db_dir)
77
45 -
78 # if db folder exists and is not empty:
47 - if os.path.exists(self.db_dir) and files.exists(self.db_dir,"index.faiss"):
79 + if os.path.exists(self.db_dir) and files.exists(self.db_dir, "index.faiss"):
80 self.db = FAISS.load_local(
81 folder_path=self.db_dir,
82 embeddings=self.embedder,
51 - allow_dangerous_deserialization=True
83 + allow_dangerous_deserialization=True,
84 )
85 else:
86 index = faiss.IndexFlatL2(len(self.embedder.embed_query("example text")))
@@ -57,65 +89,79 @@ class VectorDB:
89 embedding_function=self.embedder,
90 index=index,
91 docstore=InMemoryDocstore(),
60 - index_to_docstore_id={})
92 + index_to_docstore_id={},
93 + )
94
62 - #preload knowledge files
95 + # preload knowledge files
96 if self.kn_dir:
97 self.preload_knowledge(self.kn_dir, self.db_dir)
65 -
98
67 - def preload_knowledge(self, kn_dir:str, db_dir:str):
99 + def preload_knowledge(self, kn_dir: str, db_dir: str):
100
101 # Load the index file if it exists
102 index_path = files.get_abs_path(db_dir, "knowledge_import.json")
103
72 - #make sure directory exists
104 + # make sure directory exists
105 if not os.path.exists(db_dir):
106 os.makedirs(db_dir)
75 -
107 +
108 index: dict[str, knowledge_import.KnowledgeImport] = {}
109 if os.path.exists(index_path):
78 - with open(index_path, 'r') as f:
110 + with open(index_path, "r") as f:
111 index = json.load(f)
80 -
81 - index = knowledge_import.load_knowledge(self.logger,kn_dir,index)
82 -
112 +
113 + index = knowledge_import.load_knowledge(self.logger, kn_dir, index)
114 +
115 for file in index:
84 - if index[file]['state'] in ['changed', 'removed'] and index[file].get('ids',[]): # for knowledge files that have been changed or removed and have IDs
85 - self.delete_documents_by_ids(index[file]['ids']) # remove original version
86 - if index[file]['state'] == 'changed':
87 - index[file]['ids'] = self.insert_documents(index[file]['documents']) # insert new version
116 + if index[file]["state"] in ["changed", "removed"] and index[file].get(
117 + "ids", []
118 + ): # for knowledge files that have been changed or removed and have IDs
119 + self.delete_documents_by_ids(
120 + index[file]["ids"]
121 + ) # remove original version
122 + if index[file]["state"] == "changed":
123 + index[file]["ids"] = self.insert_documents(
124 + index[file]["documents"]
125 + ) # insert new version
126
127 # remove index where state="removed"
90 - index = {k: v for k, v in index.items() if v['state'] != 'removed'}
91 -
128 + index = {k: v for k, v in index.items() if v["state"] != "removed"}
129 +
130 # strip state and documents from index and save it
131 for file in index:
94 - if "documents" in index[file]: del index[file]['documents'] # type: ignore
95 - if "state" in index[file]: del index[file]['state'] # type: ignore
96 - with open(index_path, 'w') as f:
97 - json.dump(index, f)
98 -
132 + if "documents" in index[file]:
133 + del index[file]["documents"] # type: ignore
134 + if "state" in index[file]:
135 + del index[file]["state"] # type: ignore
136 + with open(index_path, "w") as f:
137 + json.dump(index, f)
138 +
139 def search_similarity(self, query, results=3):
100 - return self.db.similarity_search(query,results)
101 -
140 + return self.db.similarity_search(query, results)
141 +
142 def search_similarity_threshold(self, query, results=3, threshold=0.5):
103 - return self.db.search(query, search_type="similarity_score_threshold", k=results, score_threshold=threshold)
143 + return self.db.search(
144 + query,
145 + search_type="similarity_score_threshold",
146 + k=results,
147 + score_threshold=threshold,
148 + )
149
150 def search_max_rel(self, query, results=3):
106 - return self.db.max_marginal_relevance_search(query,results)
151 + return self.db.max_marginal_relevance_search(query, results)
152
108 - def delete_documents_by_query(self, query:str, threshold=0.1):
153 + def delete_documents_by_query(self, query: str, threshold=0.1):
154 k = 100
155 tot = 0
156 while True:
157 # Perform similarity search with score
113 - docs = self.search_similarity_threshold(query, results=k, threshold=threshold)
158 + docs = self.search_similarity_threshold(
159 + query, results=k, threshold=threshold
160 + )
161
162 # Extract document IDs and filter based on score
163 # document_ids = [result[0].metadata["id"] for result in docs if result[1] < score_limit]
164 document_ids = [result.metadata["id"] for result in docs]
118 -
165
166 # Delete documents with IDs over the threshold score
167 if document_ids:
@@ -124,33 +170,36 @@ class VectorDB:
170 # tot += len(fnd["ids"])
171 self.db.delete(ids=document_ids)
172 tot += len(document_ids)
127 -
173 +
174 # If fewer than K document IDs, break the loop
175 if len(document_ids) < k:
176 break
177
132 - if tot: self.db.save_local(folder_path=self.db_dir) # persist
178 + if tot:
179 + self.db.save_local(folder_path=self.db_dir) # persist
180 return tot
181
135 - def delete_documents_by_ids(self, ids:list[str]):
182 + def delete_documents_by_ids(self, ids: list[str]):
183 # pre = self.db.get(ids=ids)["ids"]
184 self.db.delete(ids=ids)
185 # post = self.db.get(ids=ids)["ids"]
139 - #TODO? compare pre and post
140 - if ids: self.db.save_local(folder_path=self.db_dir) #persist
186 + # TODO? compare pre and post
187 + if ids:
188 + self.db.save_local(folder_path=self.db_dir) # persist
189 return len(ids)
142 -
143 - def insert_text(self, text):
190 +
191 + def insert_text(self, text, metadata: dict = {}):
192 id = str(uuid.uuid4())
145 - self.db.add_documents(documents=[ Document(text, metadata={"id": id}) ], ids=[id])
146 - self.db.save_local(folder_path=self.db_dir) #persist
193 + self.db.add_documents(
194 + documents=[Document(text, metadata={"id": id, **metadata})], ids=[id]
195 + )
196 + self.db.save_local(folder_path=self.db_dir) # persist
197 return id
148 -
149 - def insert_documents(self, docs:list[Document]):
198 +
199 + def insert_documents(self, docs: list[Document]):
200 ids = [str(uuid.uuid4()) for _ in range(len(docs))]
151 - for doc, id in zip(docs, ids): doc.metadata["id"] = id #add ids to documents metadata
201 + for doc, id in zip(docs, ids):
202 + doc.metadata["id"] = id # add ids to documents metadata
203 self.db.add_documents(documents=docs, ids=ids)
153 - self.db.save_local(folder_path=self.db_dir) #persist
204 + self.db.save_local(folder_path=self.db_dir) # persist
205 return ids
155 -
156 -
python/tools/code_execution_tool.py
+96 -46
@@ -9,23 +9,25 @@ from python.helpers.shell_local import LocalInteractiveSession
9 from python.helpers.shell_ssh import SSHInteractiveSession
10 from python.helpers.docker import DockerContainerManager
11
12 +
13 @dataclass
14 class State:
15 shell: LocalInteractiveSession | SSHInteractiveSession
16 docker: DockerContainerManager | None
16 -
17 +
18
19 class CodeExecution(Tool):
20
20 - async def execute(self,**kwargs):
21 + async def execute(self, **kwargs):
22 +
23 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
24
22 - await self.agent.handle_intervention() # wait for intervention and handle it, if paused
23 -
25 await self.prepare_state()
26
27 # os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir
27 -
28 - runtime = self.args["runtime"].lower().strip()
28 +
29 + runtime = self.args.get("runtime", "").lower().strip()
30 +
31 if runtime == "python":
32 response = await self.execute_python_code(self.args["code"])
33 elif runtime == "nodejs":
@@ -33,90 +35,138 @@ class CodeExecution(Tool):
35 elif runtime == "terminal":
36 response = await self.execute_terminal_command(self.args["code"])
37 elif runtime == "output":
36 - response = await self.get_terminal_output(wait_with_output=5, wait_without_output=20)
38 + response = await self.get_terminal_output(
39 + wait_with_output=5, wait_without_output=20
40 + )
41 elif runtime == "reset":
42 response = await self.reset_terminal()
43 else:
40 - response = self.agent.read_prompt("fw.code_runtime_wrong.md", runtime=runtime)
44 + response = self.agent.read_prompt(
45 + "fw.code_runtime_wrong.md", runtime=runtime
46 + )
47
42 - if not response: response = self.agent.read_prompt("fw.code_no_output.md")
48 + if not response:
49 + response = self.agent.read_prompt("fw.code_no_output.md")
50 return Response(message=response, break_loop=False)
51
52 async def before_execution(self, **kwargs):
46 - await self.agent.handle_intervention() # wait for intervention and handle it, if paused
47 - PrintStyle(font_color="#1B4F72", padding=True, background_color="white", bold=True).print(f"{self.agent.agent_name}: Using tool '{self.name}':")
48 - self.log = self.agent.context.log.log(type="code_exe", heading=f"{self.agent.agent_name}: Using tool '{self.name}':", content="", kvps=self.args)
53 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
54 + PrintStyle(
55 + font_color="#1B4F72", padding=True, background_color="white", bold=True
56 + ).print(f"{self.agent.agent_name}: Using tool '{self.name}':")
57 + self.log = self.agent.context.log.log(
58 + type="code_exe",
59 + heading=f"{self.agent.agent_name}: Using tool '{self.name}':",
60 + content="",
61 + kvps=self.args,
62 + )
63 if self.args and isinstance(self.args, dict):
64 for key, value in self.args.items():
51 - PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ")
52 - PrintStyle(font_color="#85C1E9", padding=isinstance(value,str) and "\n" in value).stream(value)
65 + PrintStyle(font_color="#85C1E9", bold=True).stream(
66 + self.nice_key(key) + ": "
67 + )
68 + PrintStyle(
69 + font_color="#85C1E9",
70 + padding=isinstance(value, str) and "\n" in value,
71 + ).stream(value)
72 PrintStyle().print()
73
74 async def after_execution(self, response, **kwargs):
56 - msg_response = self.agent.read_prompt("fw.tool_response.md", tool_name=self.name, tool_response=response.message)
75 + msg_response = self.agent.read_prompt(
76 + "fw.tool_response.md", tool_name=self.name, tool_response=response.message
77 + )
78 await self.agent.append_message(msg_response, human=True)
79
80 async def prepare_state(self, reset=False):
81 self.state = self.agent.get_data("cot_state")
82 if not self.state or reset:
83
63 - #initialize docker container if execution in docker is configured
84 + # initialize docker container if execution in docker is configured
85 if self.agent.config.code_exec_docker_enabled:
65 - docker = DockerContainerManager(logger=self.agent.context.log,name=self.agent.config.code_exec_docker_name, image=self.agent.config.code_exec_docker_image, ports=self.agent.config.code_exec_docker_ports, volumes=self.agent.config.code_exec_docker_volumes)
86 + docker = DockerContainerManager(
87 + logger=self.agent.context.log,
88 + name=self.agent.config.code_exec_docker_name,
89 + image=self.agent.config.code_exec_docker_image,
90 + ports=self.agent.config.code_exec_docker_ports,
91 + volumes=self.agent.config.code_exec_docker_volumes,
92 + )
93 docker.start_container()
67 - else: docker = None
94 + else:
95 + docker = None
96
69 - #initialize local or remote interactive shell insterface
97 + # initialize local or remote interactive shell insterface
98 if self.agent.config.code_exec_ssh_enabled:
71 - shell = SSHInteractiveSession(self.agent.context.log,self.agent.config.code_exec_ssh_addr,self.agent.config.code_exec_ssh_port,self.agent.config.code_exec_ssh_user,self.agent.config.code_exec_ssh_pass)
72 - else: shell = LocalInteractiveSession()
73 -
74 - self.state = State(shell=shell,docker=docker)
99 + shell = SSHInteractiveSession(
100 + self.agent.context.log,
101 + self.agent.config.code_exec_ssh_addr,
102 + self.agent.config.code_exec_ssh_port,
103 + self.agent.config.code_exec_ssh_user,
104 + self.agent.config.code_exec_ssh_pass,
105 + )
106 + else:
107 + shell = LocalInteractiveSession()
108 +
109 + self.state = State(shell=shell, docker=docker)
110 await shell.connect()
111 self.agent.set_data("cot_state", self.state)
77 -
78 - async def execute_python_code(self, code):
112 +
113 + async def execute_python_code(self, code: str, reset: bool = False):
114 escaped_code = shlex.quote(code)
80 - command = f'python3 -c {escaped_code}'
81 - return await self.terminal_session(command)
115 + command = f"python3 -c {escaped_code}"
116 + return await self.terminal_session(command, reset)
117
83 - async def execute_nodejs_code(self, code):
118 + async def execute_nodejs_code(self, code: str, reset: bool = False):
119 escaped_code = shlex.quote(code)
85 - command = f'node -e {escaped_code}'
86 - return await self.terminal_session(command)
120 + command = f"node -e {escaped_code}"
121 + return await self.terminal_session(command, reset)
122
88 - async def execute_terminal_command(self, command):
89 - return await self.terminal_session(command)
123 + async def execute_terminal_command(self, command: str, reset: bool = False):
124 + return await self.terminal_session(command, reset)
125
91 - async def terminal_session(self, command):
126 + async def terminal_session(self, command: str, reset: bool = False):
127 +
128 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
129 + if reset:
130 + await self.reset_terminal()
131
93 - await self.agent.handle_intervention() # wait for intervention and handle it, if paused
94 -
132 self.state.shell.send_command(command)
133
97 - PrintStyle(background_color="white",font_color="#1B4F72",bold=True).print(f"{self.agent.agent_name} code execution output:")
134 + PrintStyle(background_color="white", font_color="#1B4F72", bold=True).print(
135 + f"{self.agent.agent_name} code execution output:"
136 + )
137 return await self.get_terminal_output()
138
100 - async def get_terminal_output(self, wait_with_output=3, wait_without_output=10):
101 - idle=0
139 + async def get_terminal_output(
140 + self, wait_with_output=3, wait_without_output=10, max_exec_time=15
141 + ):
142 + idle = 0
143 SLEEP_TIME = 0.1
103 - while True:
144 + start_time = time.time()
145 + full_output = ""
146 +
147 + while max_exec_time <= 0 or time.time() - start_time < max_exec_time:
148 await asyncio.sleep(SLEEP_TIME) # Wait for some output to be generated
105 - full_output, partial_output = await self.state.shell.read_output()
149 + full_output, partial_output = await self.state.shell.read_output(
150 + max_exec_time
151 + )
152 +
153 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
154
107 - await self.agent.handle_intervention() # wait for intervention and handle it, if paused
108 -
155 if partial_output:
156 PrintStyle(font_color="#85C1E9").stream(partial_output)
157 self.log.update(content=full_output)
112 - idle=0
158 + idle = 0
159 else:
114 - idle+=1
115 - if ( full_output and idle > wait_with_output / SLEEP_TIME ) or ( not full_output and idle > wait_without_output / SLEEP_TIME ): return full_output
160 + idle += 1
161 + if (full_output and idle > wait_with_output / SLEEP_TIME) or (
162 + not full_output and idle > wait_without_output / SLEEP_TIME
163 + ):
164 + break
165 + return full_output
166
167 async def reset_terminal(self):
168 self.state.shell.close()
169 await self.prepare_state(reset=True)
170 response = self.agent.read_prompt("fw.code_reset.md")
171 self.log.update(content=response)
122 - return response
\ No newline at end of file
172 + return response
python/tools/knowledge_tool.py
+5 -3
@@ -24,8 +24,10 @@ class Knowledge(Tool):
24 # duckduckgo search
25 duckduckgo = executor.submit(duckduckgo_search.search, question)
26
27 - # memory search
28 - future_memory = executor.submit(memory_tool.search, self.agent, question)
27 + # manual memory search
28 + future_memory_man = executor.submit(memory_tool.search, self.agent, "manual", question)
29 + # history memory search
30 + # future_memory_man = executor.submit(memory_tool.search, self.agent, "history", question)
31
32 # Wait for both functions to complete
33 try:
@@ -41,7 +43,7 @@ class Knowledge(Tool):
43 duckduckgo_result = "DuckDuckGo search failed: " + str(e)
44
45 try:
44 - memory_result = future_memory.result()
46 + memory_result = future_memory_man.result()
47 except Exception as e:
48 handle_error(e)
49 memory_result = "Memory search failed: " + str(e)
python/tools/memory_tool.py
+28 -25
@@ -1,29 +1,32 @@
1 import re
2 +from typing import Literal
3 from agent import Agent
3 -from python.helpers.vector_db import VectorDB, Document
4 +from python.helpers.vector_db import get_or_create
5 import os
6 from python.helpers.tool import Tool, Response
7 from python.helpers.print_style import PrintStyle
8 from python.helpers.errors import handle_error
9
9 -# databases based on subdirectories from agent config
10 -dbs = {}
10 +
11 +type Area = Literal['manual', 'history']
12
13 class Memory(Tool):
14 async def execute(self,**kwargs):
15 result=""
15 -
16 +
17 + area = kwargs.get("area", "manual") # when called by agent, it will always be manual
18 +
19 try:
20 if "query" in kwargs:
21 threshold = float(kwargs.get("threshold", 0.1))
22 count = int(kwargs.get("count", 5))
20 - result = search(self.agent, kwargs["query"], count, threshold)
23 + result = search(self.agent, area, kwargs["query"], count, threshold)
24 elif "memorize" in kwargs:
22 - result = save(self.agent, kwargs["memorize"])
25 + result = save(self.agent, area, kwargs["memorize"])
26 elif "forget" in kwargs:
24 - result = forget(self.agent, kwargs["forget"])
25 - elif "delete" in kwargs:
26 - result = delete(self.agent, kwargs["delete"])
27 + result = forget(self.agent, area, kwargs["forget"])
28 + # elif "delete" in kwargs
29 + result = delete(self.agent, area, kwargs["delete"])
30 except Exception as e:
31 handle_error(e)
32 # hint about embedding change with existing database
@@ -34,39 +37,39 @@ class Memory(Tool):
37 # result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
38 return Response(message=result, break_loop=False)
39
37 -def search(agent:Agent, query:str, count:int=5, threshold:float=0.1):
38 - db = get_db(agent)
40 +def search(agent:Agent, area: Area, query:str, count:int=5, threshold:float=0.1):
41 + db = get_db(agent, area)
42 # docs = db.search_similarity(query,count) # type: ignore
43 docs = db.search_similarity_threshold(query,count,threshold) # type: ignore
44 if len(docs)==0: return agent.read_prompt("fw.memories_not_found.md", query=query)
45 else: return str(docs)
46
44 -def save(agent:Agent, text:str):
45 - db = get_db(agent)
47 +def save(agent:Agent, area: Area, text:str):
48 + db = get_db(agent, area)
49 id = db.insert_text(text) # type: ignore
50 return agent.read_prompt("fw.memory_saved.md", memory_id=id)
51
49 -def delete(agent:Agent, ids_str:str):
50 - db = get_db(agent)
52 +def delete(agent:Agent, area: Area, ids_str:str):
53 + db = get_db(agent, area)
54 ids = extract_guids(ids_str)
55 deleted = db.delete_documents_by_ids(ids) # type: ignore
56 return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
57
55 -def forget(agent:Agent, query:str):
56 - db = get_db(agent)
58 +def forget(agent:Agent, area: Area, query:str):
59 + db = get_db(agent, area)
60 deleted = db.delete_documents_by_query(query) # type: ignore
61 return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
62
60 -def get_db(agent: Agent):
61 - mem_dir = os.path.join("memory", agent.config.memory_subdir)
63 +def get_db(agent: Agent, area: Area):
64 + mem_dir = os.path.join("memory", agent.config.memory_subdir or "default", area)
65 kn_dir = os.path.join("knowledge", agent.config.knowledge_subdir)
63 - key = (mem_dir, kn_dir)
66
65 - if key not in dbs:
66 - db = VectorDB(agent.context.log,embeddings_model=agent.config.embeddings_model, in_memory=False, memory_dir=mem_dir, knowledge_dir=kn_dir)
67 - dbs[key] = db
68 - else:
69 - db = dbs[key]
67 + db = get_or_create(
68 + agent.context.log,
69 + embeddings_model=agent.config.embeddings_model,
70 + in_memory=False,
71 + memory_dir=mem_dir,
72 + knowledge_dir=kn_dir)
73
74 return db
75
requirements.txt
+2 -1
@@ -19,4 +19,5 @@ pynput==1.7.7
19 pypdf==4.3.1
20 Flask[async]==3.0.3
21 Flask-BasicAuth==0.2.0
22 -faiss-cpu==1.8.0.post1
\ No newline at end of file
22 +faiss-cpu==1.8.0.post1
23 +langchain-ollama==0.1.3
\ No newline at end of file
run_ui.py
+7 -2
@@ -26,8 +26,13 @@ basic_auth = BasicAuth(app)
26 # get context to run agent zero in
27 def get_context(ctxid:str):
28 with lock:
29 - if not ctxid: return AgentContext.first() or AgentContext(config=initialize())
30 - return AgentContext.get(ctxid) or AgentContext(config=initialize(),id=ctxid)
29 + if not ctxid:
30 + first = AgentContext.first()
31 + if first: return first
32 + return AgentContext(config=initialize())
33 + got = AgentContext.get(ctxid)
34 + if got: return got
35 + return AgentContext(config=initialize(),id=ctxid)
36
37 # Now you can use @requires_auth function decorator to require login on certain pages
38 def requires_auth(f):
test.py
+288 -17
@@ -1,22 +1,293 @@
1 -from python.helpers.strings import calculate_valid_match_lengths
1 +from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError
2 +from python.helpers.print_style import PrintStyle
3 +import asyncio
4 +import random
5 +import re
6
3 -# first = b'python3 -c \'from selenium import webdriver\nfrom selenium.webdriver.chrome.service import Service\nfrom webdriver_manager.chrome import ChromeDriverManager\nimport time\n\n# Set up the Chromium WebDriver\noptions = webdriver.ChromeOptions()\noptions.add_argument(\'"\'"\'--headless\'"\'"\') # Run in headless mode\noptions.add_argument(\'"\'"\'--no-sandbox\'"\'"\')\noptions.add_argument(\'"\'"\'--disable-dev-shm-usage\'"\'"\')\n\n# Specify the correct version of ChromeDriver\nservice = Service(\'"\'"\'/root/.wdm/drivers/chromedriver/linux64/128.0.6613.113/chromedriver\'"\'"\')\ndriver = webdriver.Chrome(service=service, options=options)\n\n# Navigate to the LinkedIn profile\nurl = \'"\'"\'https://www.linkedin.com/in/jan-tomasek/\'"\'"\'\ndriver.get(url)\n\n# Wait for the page to load\ntime.sleep(5)\n\n# Save the page source to a file\nwith open(\'"\'"\'jan_tomasek_linkedin.html\'"\'"\', \'"\'"\'w\'"\'"\', encoding=\'"\'"\'utf-8\'"\'"\') as file:\n file.write(driver.page_source)\n\n# Close the WebDriver\ndriver.quit()\'\n'
4 -first = b'https://www.linkedin.com/in/jan-tomasek/\'"\'"\'\ndriver.get(url)\n\n# Wait for the page to load\ntime.sleep(5)\n\n# Save the page source to a file\nwith open(\'"\'"\'jan_tomasek_linkedin.html\'"\'"\', \'"\'"\'w\'"\'"\', encoding=\'"\'"\'utf-8\'"\'"\') as file:\n file.write(driver.page_source)\n\n# Close the WebDriver\ndriver.quit()\'\n'
7 +# List of user agents
8 +user_agents = [
9 + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
10 + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
11 + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
12 + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36',
13 + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1',
14 + 'Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/91.0.4472.80 Mobile/15E148 Safari/604.1',
15 + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59',
16 + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
17 +]
18
6 -# second = b'python3 -c \'from selenium import webdriver\r\n\x1b[?2004l\r\x1b[?2004h> from selenium.webdriver.chrome.service import Service\r\n\x1b[?2004l\r\x1b[?2004h> from webdriver_manager.chrome import ChromeDriverManager\r\n\x1b[?2004l\r\x1b[?2004h> import time\r\n\x1b[?2004l\r\x1b[?2004h> \r\n\x1b[?2004l\r\x1b[?2004h> # Set up the Chromium WebDriver\r\n\x1b[?2004l\r\x1b[?2004h> options = webdriver.ChromeOptions()\r\n\x1b[?2004l\r\x1b[?2004h> options.add_argument(\'"\'"\'--headless\'"\'"\') # Run in headless mode\r\n\x1b[?2004l\r\x1b[?2004h> options.add_argument(\'"\'"\'--no-sandbox\'"\'"\')\r\n\x1b[?2004l\r\x1b[?2004h> options.add_argument(\'"\'"\'--disable-dev-shm-usage\'"\'"\')\r\n\x1b[?2004l\r\x1b[?2004h> \r\n\x1b[?2004l\r\x1b[?2004h> # Specify the correct version of ChromeDriver\r\n\x1b[?2004l\r\x1b[?2004h> service = Service(\'"\'"\'/root/.wdm/drivers/chromedriver/linux64/128.0.6613.113/chromedriver\'"\'"\')\r\n\x1b[?2004l\r\x1b[?2004h> driver = webdriver.Chrome(service=service, options=options)\r\n\x1b[?2004l\r\x1b[?2004h> \r\n\x1b[?2004l\r\x1b[?2004h> # Navigate to the LinkedIn profile\r\n\x1b[?2004l\r\x1b[?2004h> url = \'"\'"\'https://www.linkedin.com/in/jan-tomasek/\'"\'"\'\r\n\x1b[?'
7 -second = b'https://www.linkedin.com/in/jan-tomasek/\'"\'"\'\r\n\x1b[?'
19 +async def scrape_page(url, headless=True):
20 + try:
21 + async with async_playwright() as p:
22 + browser = await p.chromium.launch(headless=headless)
23 + context = await browser.new_context(
24 + user_agent=random.choice(user_agents)
25 + )
26 + page = await context.new_page()
27 + page.set_default_timeout(30000)
28 +
29 + await page.goto(url, wait_until='domcontentloaded', timeout=30000)
30 + await page.wait_for_selector('body', timeout=5000)
31 +
32 + result = await page.evaluate(r'''() => {
33 + const cleanText = (text) => {
34 + return text
35 + .replace(/class="[^"]*"/g, '')
36 + .replace(/id="[^"]*"/g, '')
37 + .replace(/<[^>]+>/g, '')
38 + .replace(/\s+/g, ' ')
39 + .trim();
40 + };
41 + const getMetaContent = (name) => {
42 + const meta = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
43 + return meta ? cleanText(meta.content) : null;
44 + };
45 + const getMainContent = () => {
46 + const contentSelectors = [
47 + 'main', '#content', '#main-content', '.content',
48 + '.post-content', '.entry-content', '.article-content',
49 + '#mw-content-text', '.mw-parser-output'
50 + ];
51 + for (const selector of contentSelectors) {
52 + const element = document.querySelector(selector);
53 + if (element) {
54 + return cleanText(element.innerText);
55 + }
56 + }
57 + return cleanText(document.body.innerText);
58 + };
59 + const getProducts = () => {
60 + const productSelectors = [
61 + '.s-result-item', '.product-item', '[data-component-type="s-search-result"]', '.sg-col-inner'
62 + ];
63 + for (const selector of productSelectors) {
64 + const elements = document.querySelectorAll(selector);
65 + if (elements.length > 0) {
66 + return Array.from(elements).slice(0, 10).map(product => {
67 + const titleElement = product.querySelector('h2 a, .a-link-normal.a-text-normal');
68 + const priceElement = product.querySelector('.a-price .a-offscreen, .a-price');
69 + const imageElement = product.querySelector('img.s-image');
70 + let price = priceElement ? cleanText(priceElement.textContent) : null;
71 + // Remove duplicate price
72 + price = price ? price.replace(/(\$\d+(?:\.\d{2}?))\1+/, '$1') : null;
73 + return {
74 + title: titleElement ? cleanText(titleElement.textContent) : null,
75 + link: titleElement ? titleElement.href : null,
76 + price: price,
77 + image: imageElement ? imageElement.src : null
78 + };
79 + }).filter(product => product.title && product.link);
80 + }
81 + }
82 + return [];
83 + };
84 + const getNavigation = () => {
85 + const navSelectors = ['nav', 'header', '[data-cy="header-nav"]'];
86 + for (const selector of navSelectors) {
87 + const navElement = document.querySelector(selector);
88 + if (navElement) {
89 + return Array.from(navElement.querySelectorAll('a'))
90 + .filter(link => !link.href.endsWith('.svg'))
91 + .slice(0, 10) // Limit to 10 items
92 + .map(link => ({
93 + href: link.href,
94 + text: cleanText(link.textContent)
95 + }));
96 + }
97 + }
98 + return [];
99 + };
100 + const getImages = () => {
101 + return Array.from(document.querySelectorAll('img'))
102 + .filter(img => !img.src.endsWith('.svg'))
103 + .map(img => ({
104 + src: img.src,
105 + alt: img.alt,
106 + }));
107 + };
108 + const getLists = () => {
109 + const listSelectors = [
110 + '.mw-parser-output > ul', '.mw-parser-output > ol',
111 + '[data-cy="main-content"] ul', '[data-cy="main-content"] ol',
112 + 'main ul', 'main ol',
113 + '.content ul', '.content ol',
114 + '#content ul', '#content ol'
115 + ];
116 + let lists = [];
117 + for (const selector of listSelectors) {
118 + const elements = document.querySelectorAll(selector);
119 + if (elements.length > 0) {
120 + lists = Array.from(elements)
121 + .filter(list => list.children.length >= 3 && list.children.length <= 20)
122 + .map(list => ({
123 + type: list.tagName.toLowerCase(),
124 + items: Array.from(list.children)
125 + .filter(li => li.textContent.trim().length > 10)
126 + .map(li => {
127 + // Remove CSS classes, IDs, and inline styles
128 + let text = li.textContent.replace(/\s+/g, ' ').trim();
129 + // Remove any remaining HTML tags
130 + text = text.replace(/<[^>]+>/g, '');
131 + // Remove CSS-related content
132 + text = text.replace(/\.mw-parser-output[^{]+\{[^}]+\}/g, '');
133 + // Remove ISBN prefix if present
134 + text = text.replace(/^ISBN\s+/, '');
135 + return text;
136 + })
137 + .filter(text => text.length > 0 && !text.includes('mw-parser-output'))
138 + .slice(0, 10)
139 + }))
140 + .filter(list => list.items.length >= 3)
141 + .slice(0, 3);
142 + if (lists.length > 0) break;
143 + }
144 + }
145 + return lists;
146 + };
147 + const getSocialMediaLinks = () => {
148 + const socialSelectors = [
149 + 'a[href*="facebook.com"]',
150 + 'a[href*="twitter.com"]',
151 + 'a[href*="x.com"]',
152 + 'a[href*="github.com"]',
153 + 'a[href*="reddit.com"]',
154 + 'a[href*="tiktok.com"]',
155 + 'a[href*="discord.com"]',
156 + 'a[href*="instagram.com"]',
157 + 'a[href*="linkedin.com"]',
158 + 'a[href*="youtube.com"]',
159 + 'a[href*="pinterest.com"]',
160 + 'a[href*="snapchat.com"]',
161 + 'a[href*="tumblr.com"]',
162 + 'a[href*="medium.com"]',
163 + 'a[href*="whatsapp.com"]',
164 + 'a[href*="telegram.org"]',
165 + 'a[href*="vimeo.com"]',
166 + 'a[href*="flickr.com"]',
167 + 'a[href*="quora.com"]',
168 + 'a[href*="twitch.tv"]'
169 + ];
170 + return socialSelectors.map(selector => {
171 + const element = document.querySelector(selector);
172 + return element ? element.href : null;
173 + }).filter(Boolean);
174 + };
175 + const getCodeScripts = () => {
176 + const codeBlocks = document.querySelectorAll('pre code, .highlight pre, .code-block');
177 + return Array.from(codeBlocks).map(block => ({
178 + language: block.className.match(/language-(\w+)/)?.[1] || 'text',
179 + code: cleanText(block.textContent)
180 + })).filter(script => script.code.length > 0);
181 + };
182 + return {
183 + url: window.location.href,
184 + title: document.title,
185 + author: getMetaContent('author'),
186 + publishDate: getMetaContent('article:published_time') || getMetaContent('date'),
187 + lastModified: document.lastModified,
188 + keywords: getMetaContent('keywords'),
189 + metaDescription: getMetaContent('description') || getMetaContent('og:description'),
190 + mainContent: getMainContent(),
191 + navigation: getNavigation(),
192 + images: getImages(),
193 + lists: getLists(),
194 + products: getProducts(),
195 + socialMediaLinks: getSocialMediaLinks(),
196 + codeScripts: getCodeScripts(),
197 + };
198 + }''')
199 +
200 + await browser.close()
201 + return result
202 + except PlaywrightTimeoutError as e:
203 + PrintStyle(font_color="yellow", padding=True).print(f"Attempt {attempt + 1} failed due to timeout: {str(e)}")
204 + except Exception as e:
205 + PrintStyle(font_color="red", padding=True).print(f"Attempt {attempt + 1} failed: {str(e)}")
206 +
207
9 -trim_com, trim_out = calculate_valid_match_lengths(
10 - first, second, deviation_threshold=8, deviation_reset=2,
11 - ignore_patterns = [
12 - rb'\x1b\[\?\d{4}[a-zA-Z](?:> )?', # ANSI escape sequences
13 - rb'\r', # Carriage return
14 - rb'>\s', # Greater-than symbol
15 - ],
16 - debug=True)
208 +async def scrape_url(url, headless):
209 + try:
210 + result = await scrape_page(url, headless)
211 +
212 + if result is None:
213 + raise ValueError("Scraping result is None")
214 +
215 + markdown_content = f"# {result.get('title', 'No Title')}\n\n"
216 +
217 + if result.get('url'):
218 + markdown_content += f"URL: {result['url']}\n\n"
219 +
220 + if result.get('author'):
221 + markdown_content += f"Author: {result['author']}\n\n"
222 +
223 + if result.get('publishDate'):
224 + markdown_content += f"Published: {result['publishDate']}\n\n"
225 +
226 + if result.get('keywords'):
227 + markdown_content += f"Keywords: {result['keywords']}\n\n"
228 +
229 + if result.get('metaDescription'):
230 + markdown_content += f"## Description\n\n{result['metaDescription']}\n\n"
231 +
232 + if result.get('mainContent'):
233 + markdown_content += "## Webpage Content:\n\n"
234 + markdown_content += result['mainContent'] + "\n\n"
235 +
236 + if result.get('lists'):
237 + markdown_content += "## Lists\n\n"
238 + for list_item in result['lists']:
239 + markdown_content += f"### {list_item['type'].upper()} List\n\n"
240 + for item in list_item['items']:
241 + markdown_content += f"- {item}\n"
242 + markdown_content += "\n"
243 +
244 + if result.get('products'):
245 + markdown_content += "## Products\n\n"
246 + for product in result['products']:
247 + markdown_content += f"### {product.get('title', 'Untitled Product')}\n\n"
248 + if product.get('price'):
249 + price = product['price']
250 + price = re.sub(r'(\$\d+(?:\.\d{2})?)\1+', r'\1', price)
251 + markdown_content += f"Price: {price}\n\n"
252 + if product.get('link'):
253 + markdown_content += f"[View Product]({product['link']})\n\n"
254 + if product.get('image'):
255 + markdown_content += f"![Product Image]({product['image']})\n\n"
256 + markdown_content += "---\n\n"
257 +
258 + if result.get('socialMediaLinks'):
259 + markdown_content += "## Social Media Links\n\n"
260 + for link in result['socialMediaLinks']:
261 + markdown_content += f"- [{link.split('.com')[0].split('/')[-1].capitalize()}]({link})\n"
262 + markdown_content += "\n"
263 +
264 + if result.get('codeScripts'):
265 + markdown_content += "## Code Snippet\n\n"
266 + for script in result['codeScripts']:
267 + markdown_content += f"```{script['language']}\n{script['code']}\n```\n\n"
268 +
269 + markdown_content = markdown_content.strip()
270 +
271 + return markdown_content
272 + except Exception as e:
273 + return f"Error: Failed to scrape URL. Reason: {str(e)}"
274
18 -if(trim_com > 0 and trim_out > 0):
19 - sec_tr = second[:trim_out]
20 -else: sec_tr = "original"
275 +async def fetch_page_content(url: str, max_retries: int = 2, headless: bool = True):
276 + for attempt in range(max_retries):
277 + try:
278 + content = await scrape_url(url, headless)
279 + if content.startswith("Error:"):
280 + raise Exception(content)
281 + return str(content)
282 + except Exception as e:
283 + PrintStyle(font_color="red", padding=True).print(f"Attempt {attempt + 1} failed: {str(e)}")
284 +
285 + raise Exception("Error: Webpage content is not available.")
286
22 -print(sec_tr)
\ No newline at end of file
287 +
288 +async def test():
289 + url = "https://github.com/frdel/agent-zero"
290 + content = await fetch_page_content(url)
291 + print(content)
292 +
293 +asyncio.run(test())
\ No newline at end of file
test2.py new
+120
@@ -0,0 +1,120 @@
1 +import requests
2 +from bs4 import BeautifulSoup
3 +import random
4 +import re
5 +
6 +# List of user agents
7 +user_agents = [
8 + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
9 + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
10 + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
11 + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36',
12 + 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1',
13 + 'Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/91.0.4472.80 Mobile/15E148 Safari/604.1',
14 + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59',
15 + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
16 +]
17 +
18 +def clean_text(text):
19 + """Utility function to clean HTML tags and whitespace."""
20 + text = re.sub(r'class="[^"]*"', '', text)
21 + text = re.sub(r'id="[^"]*"', '', text)
22 + text = re.sub(r'<[^>]+>', '', text)
23 + return re.sub(r'\s+', ' ', text).strip()
24 +
25 +def scrape_page(url):
26 + headers = {
27 + 'User-Agent': random.choice(user_agents)
28 + }
29 +
30 + try:
31 + response = requests.get(url, headers=headers, timeout=30)
32 + response.raise_for_status() # Check if the request was successful
33 + soup = BeautifulSoup(response.content, 'html.parser')
34 +
35 + # Extract title
36 + title = soup.title.string if soup.title else 'No Title'
37 +
38 + # Extract meta information
39 + def get_meta_content(name):
40 + meta = soup.find('meta', attrs={'name': name}) or soup.find('meta', property=name)
41 + return meta['content'] if meta else None
42 +
43 + author = get_meta_content('author')
44 + publish_date = get_meta_content('article:published_time') or get_meta_content('date')
45 + keywords = get_meta_content('keywords')
46 + description = get_meta_content('description') or get_meta_content('og:description')
47 +
48 + # Extract main content
49 + def get_main_content():
50 + content_selectors = ['main', '#content', '#main-content', '.content', '.post-content', '.entry-content', '.article-content', '#mw-content-text', '.mw-parser-output']
51 + for selector in content_selectors:
52 + element = soup.select_one(selector)
53 + if element:
54 + return clean_text(element.get_text())
55 + return clean_text(soup.body.get_text() if soup.body else "")
56 +
57 + # Extract images
58 + def get_images():
59 + images = []
60 + for img in soup.find_all('img'):
61 + if not img.get('src', '').endswith('.svg'):
62 + images.append({
63 + 'src': img['src'],
64 + 'alt': img.get('alt', '')
65 + })
66 + return images
67 +
68 + # Extract product listings
69 + def get_products():
70 + products = []
71 + product_selectors = ['.s-result-item', '.product-item', '[data-component-type="s-search-result"]', '.sg-col-inner']
72 + for selector in product_selectors:
73 + elements = soup.select(selector)
74 + if elements:
75 + for product in elements[:10]:
76 + title_element = product.select_one('h2 a, .a-link-normal.a-text-normal')
77 + price_element = product.select_one('.a-price .a-offscreen, .a-price')
78 + image_element = product.select_one('img.s-image')
79 + price = clean_text(price_element.text) if price_element else None
80 + price = re.sub(r'(\$\d+(?:\.\d{2})?)\1+', r'\1', price) if price else None
81 + products.append({
82 + 'title': clean_text(title_element.text) if title_element else None,
83 + 'link': title_element['href'] if title_element else None,
84 + 'price': price,
85 + 'image': image_element['src'] if image_element else None
86 + })
87 + return products
88 +
89 + # Extract lists
90 + def get_lists():
91 + lists = []
92 + list_selectors = ['.mw-parser-output > ul', '.mw-parser-output > ol', 'main ul', 'main ol', '.content ul', '.content ol']
93 + for selector in list_selectors:
94 + elements = soup.select(selector)
95 + for element in elements:
96 + list_items = [clean_text(li.get_text()) for li in element.find_all('li') if len(li.get_text().strip()) > 10]
97 + if 3 <= len(list_items) <= 20:
98 + lists.append({'type': element.name, 'items': list_items[:10]})
99 + return lists
100 +
101 + # Gather all extracted data
102 + return {
103 + 'url': url,
104 + 'title': title,
105 + 'author': author,
106 + 'publishDate': publish_date,
107 + 'keywords': keywords,
108 + 'metaDescription': description,
109 + 'mainContent': get_main_content(),
110 + 'images': get_images(),
111 + 'products': get_products(),
112 + 'lists': get_lists()
113 + }
114 +
115 + except requests.exceptions.RequestException as e:
116 + return {"error": str(e)}
117 +
118 +# Example usage:
119 +result = scrape_page('https://github.com/frdel/agent-zero')
120 +print(result)
webui/index.css
+8
@@ -327,6 +327,14 @@ input:checked + .slider:before {
327 .msg-thoughts{
328 }
329
330 +.message-temp {
331 + display: none;
332 +}
333 +
334 +.message-temp:last-child {
335 + display: block; /* or any style you want for visibility */
336 +}
337 +
338 .connected{
339 color: #6ec583;
340 }
webui/index.js
+7 -4
@@ -55,7 +55,7 @@ chatInput.addEventListener('keydown', (e) => {
55
56 sendButton.addEventListener('click', sendMessage);
57
58 -function setMessage(id, type, heading, content, kvps = null) {
58 +function setMessage(id, type, heading, content, temp, kvps = null) {
59 // Search for the existing message container by id
60 let messageContainer = document.getElementById(`message-${id}`);
61
@@ -68,10 +68,12 @@ function setMessage(id, type, heading, content, kvps = null) {
68 messageContainer = document.createElement('div');
69 messageContainer.id = `message-${id}`;
70 messageContainer.classList.add('message-container', `${sender}-container`);
71 + if (temp) messageContainer.classList.add("message-temp")
72 +
73 }
74
75 const handler = msgs.getHandler(type);
74 - handler(messageContainer, id, type, heading, content, kvps);
76 + handler(messageContainer, id, type, heading, content, temp, kvps);
77
78 // If the container was found, it was already in the DOM, no need to append again
79 if (!document.getElementById(`message-${id}`)) {
@@ -82,6 +84,7 @@ function setMessage(id, type, heading, content, kvps = null) {
84 }
85
86
87 +
88 function adjustTextareaHeight() {
89 chatInput.style.height = 'auto';
90 chatInput.style.height = (chatInput.scrollHeight) + 'px';
@@ -131,7 +134,7 @@ async function poll() {
134
135 if (lastLogVersion != response.log_version) {
136 for (const log of response.logs) {
134 - setMessage(log.no, log.type, log.heading, log.content, log.kvps);
137 + setMessage(log.no, log.type, log.heading, log.content, log.temp, log.kvps);
138 }
139 }
140
@@ -186,7 +189,7 @@ window.killChat = async function (id) {
189 if (other) setContext(other.id)
190 else setContext(generateGUID())
191 }
189 -
192 +
193 if (found) sendJsonData("/remove", { context: id });
194 }
195
webui/messages.js
+32 -155
@@ -1,6 +1,3 @@
1 -
2 -
3 -
1 export function getHandler(type) {
2 switch (type) {
3 case 'user':
@@ -30,7 +27,8 @@ export function getHandler(type) {
27 }
28 }
29
33 -export function drawMessageDefault(messageContainer, id, type, heading, content, kvps = null) {
30 +export function _drawMessage(messageContainer, heading, content, temp, kvps = null, messageClasses = [], contentClasses = []) {
31 +
32
33 // if (type !== 'user') {
34 // const agentStart = document.createElement('div');
@@ -40,7 +38,7 @@ export function drawMessageDefault(messageContainer, id, type, heading, content,
38 // }
39
40 const messageDiv = document.createElement('div');
43 - messageDiv.classList.add('message', 'message-ai', 'message-default');
41 + messageDiv.classList.add('message', ...messageClasses);
42
43 if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
44
@@ -50,9 +48,8 @@ export function drawMessageDefault(messageContainer, id, type, heading, content,
48 textNode.textContent = content;
49 textNode.style.whiteSpace = 'pre-wrap';
50 textNode.style.wordBreak = 'break-word';
53 - textNode.classList.add("msg-json");
51 + textNode.classList.add("message-content", ...contentClasses)
52 messageDiv.appendChild(textNode);
55 -
53 messageContainer.appendChild(messageDiv);
54
55 // if (type !== 'user') {
@@ -62,181 +59,61 @@ export function drawMessageDefault(messageContainer, id, type, heading, content,
59 // messageContainer.appendChild(actions);
60 // }
61
62 + return messageDiv
63 }
64
67 -export function drawMessageAgent(messageContainer, id, type, heading, content, kvps = null) {
68 -
69 -
70 - // if (kvps && kvps['tool_name'] === 'response') {
71 - // drawMessageResponse(messageContainer, id, type, heading, content, kvps)
72 - // return
73 - // }
74 -
75 - const messageDiv = document.createElement('div');
76 - messageDiv.classList.add('message', 'message-ai', 'message-agent');
77 -
78 - if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
65 +export function drawMessageDefault(messageContainer, id, type, heading, content, temp, kvps = null) {
66 + _drawMessage(messageContainer, heading, content, temp, kvps, ['message-ai', 'message-default'], ['msg-json']);
67 +}
68
69 +export function drawMessageAgent(messageContainer, id, type, heading, content, temp, kvps = null) {
70 + let kvpsFlat=null
71 if (kvps) {
81 - const kvpsFlat = { ...kvps, ...kvps['tool_args'] || {} }
72 + kvpsFlat = { ...kvps, ...kvps['tool_args'] || {} }
73 delete kvpsFlat['tool_args']
83 - drawKvps(messageDiv, kvpsFlat);
74 }
75
86 - const textNode = document.createElement('pre');
87 - textNode.textContent = content;
88 - textNode.style.whiteSpace = 'pre-wrap';
89 - textNode.style.wordBreak = 'break-word';
90 - textNode.classList.add("msg-json");
91 - messageDiv.appendChild(textNode);
92 -
93 - messageContainer.appendChild(messageDiv);
94 -
95 - // const actions = document.createElement('div');
96 - // actions.classList.add('message-actions');
97 - // actions.innerHTML = '<span class="message-action">Copy</span> · <span class="message-action">Retry</span> · <span class="message-action">Edit</span>';
98 - // messageContainer.appendChild(actions);
99 -
76 + _drawMessage(messageContainer, heading, content, temp, kvpsFlat, ['message-ai', 'message-agent'], ['msg-json']);
77 }
78
102 -export function drawMessageResponse(messageContainer, id, type, heading, content, kvps = null) {
103 -
104 - const messageDiv = document.createElement('div');
105 - messageDiv.classList.add('message', 'message-ai', 'message-agent-response', 'message-fw');
106 -
107 - if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
108 -
109 - const textNode = document.createElement('pre');
110 - textNode.textContent = content;
111 - textNode.style.whiteSpace = 'pre-wrap';
112 - textNode.style.wordBreak = 'break-word';
113 - messageDiv.appendChild(textNode);
114 -
115 - messageContainer.appendChild(messageDiv);
79 +export function drawMessageResponse(messageContainer, id, type, heading, content, temp, kvps = null) {
80 + _drawMessage(messageContainer, heading, content, temp, null, ['message-ai', 'message-agent-response', 'message-fw']);
81 }
82
118 -export function drawMessageDelegation(messageContainer, id, type, heading, content, kvps = null) {
119 -
120 - const messageDiv = document.createElement('div');
121 - messageDiv.classList.add('message', 'message-ai', 'message-agent', 'message-fw', 'message-agent-delegation');
122 -
123 - if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
124 -
125 - const pars = kvps && kvps["tool_args"] ? kvps["tool_args"] : { text: "" }
126 -
127 - drawKvps(messageDiv, { "Thoughts": kvps["thoughts"], "Message": pars["text"], "Reset": pars["reset"] });
128 -
129 -
130 - const textNode = document.createElement('pre');
131 - textNode.textContent = content;
132 - textNode.style.whiteSpace = 'pre-wrap';
133 - textNode.style.wordBreak = 'break-word';
134 - messageDiv.appendChild(textNode);
135 -
136 - messageContainer.appendChild(messageDiv);
83 +export function drawMessageDelegation(messageContainer, id, type, heading, content, temp, kvps = null) {
84 + _drawMessage(messageContainer, heading, content, temp, kvps, ['message-ai', 'message-agent', 'message-fw', 'message-agent-delegation']);
85 }
86
139 -export function drawMessageUser(messageContainer, id, type, heading, content, kvps = null) {
140 -
141 - const messageDiv = document.createElement('div');
142 - messageDiv.classList.add('message', 'message-user');
143 -
144 - drawKvps(messageDiv, kvps);
145 -
146 - const textNode = document.createElement('pre');
147 - textNode.textContent = content;
148 - textNode.style.whiteSpace = 'pre-wrap';
149 - textNode.style.wordBreak = 'break-word';
150 - messageDiv.appendChild(textNode);
151 -
152 - messageContainer.appendChild(messageDiv);
87 +export function drawMessageUser(messageContainer, id, type, heading, content, temp, kvps = null) {
88 + _drawMessage(messageContainer, heading, content, temp, kvps, ['message-user']);
89 }
90
155 -
156 -export function drawMessageTool(messageContainer, id, type, heading, content, kvps = null) {
157 -
158 - const messageDiv = document.createElement('div');
159 - messageDiv.classList.add('message', 'message-ai', 'message-tool', 'message-fw');
160 -
161 - if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
162 -
163 - drawKvps(messageDiv, kvps);
164 -
165 - const textNode = document.createElement('pre');
166 - textNode.textContent = content;
167 - textNode.style.whiteSpace = 'pre-wrap';
168 - textNode.style.wordBreak = 'break-word';
169 - textNode.classList.add("msg-output");
170 - messageDiv.appendChild(textNode);
171 -
172 - messageContainer.appendChild(messageDiv);
173 -
174 - // const actions = document.createElement('div');
175 - // actions.classList.add('message-actions');
176 - // actions.innerHTML = '<span class="message-action">Copy output</span>';
177 - // messageContainer.appendChild(actions);
178 -
91 +export function drawMessageTool(messageContainer, id, type, heading, content, temp, kvps = null) {
92 + _drawMessage(messageContainer, heading, content, temp, kvps, ['message-ai', 'message-tool', 'message-fw'], ['msg-output']);
93 }
94
181 -export function drawMessageCodeExe(messageContainer, id, type, heading, content, kvps = null) {
182 -
183 - const messageDiv = document.createElement('div');
184 - messageDiv.classList.add('message', 'message-ai', 'message-code-exe', 'message-fw');
185 -
186 - if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
187 -
188 - drawKvps(messageDiv, kvps);
189 -
190 - const textNode = document.createElement('pre');
191 - textNode.textContent = content;
192 - textNode.style.whiteSpace = 'pre-wrap';
193 - textNode.style.wordBreak = 'break-word';
194 - messageDiv.appendChild(textNode);
195 -
196 - messageContainer.appendChild(messageDiv);
197 -
198 - // const actions = document.createElement('div');
199 - // actions.classList.add('message-actions');
200 - // actions.innerHTML = '<span class="message-action">Copy code</span> · <span class="message-action">Copy output</span>';
201 - // messageContainer.appendChild(actions);
95 +export function drawMessageCodeExe(messageContainer, id, type, heading, content, temp, kvps = null) {
96 + _drawMessage(messageContainer, heading, content, temp, null, ['message-ai', 'message-code-exe', 'message-fw']);
97 }
98
204 -export function drawMessageAgentPlain(classes, messageContainer, id, type, heading, content, kvps = null) {
205 -
206 - // const agentStart = document.createElement('div');
207 - // agentStart.classList.add('agent-start');
208 - // agentStart.textContent = 'System warning...';
209 - // messageContainer.appendChild(agentStart);
210 -
211 - const messageDiv = document.createElement('div');
212 - messageDiv.classList.add('message', 'message-ai', ...classes);
213 -
214 - drawKvps(messageDiv, kvps);
215 -
216 - const textNode = document.createElement('pre');
217 - textNode.textContent = content;
218 - textNode.style.whiteSpace = 'pre-wrap';
219 - textNode.style.wordBreak = 'break-word';
220 - messageDiv.appendChild(textNode);
221 -
222 - messageContainer.appendChild(messageDiv);
223 -
99 +export function drawMessageAgentPlain(classes, messageContainer, id, type, heading, content, temp, kvps = null) {
100 + _drawMessage(messageContainer, heading, content, temp, null, ['message-ai', ...classes]);
101 }
102
226 -export function drawMessageAdhoc(messageContainer, id, type, heading, content, kvps = null) {
227 - return drawMessageAgentPlain(['message-adhoc'], messageContainer, id, type, heading, content, kvps);
103 +export function drawMessageAdhoc(messageContainer, id, type, heading, content, temp, kvps = null) {
104 + return drawMessageAgentPlain(['message-adhoc'], messageContainer, id, type, heading, content, temp, kvps);
105 }
106
230 -export function drawMessageInfo(messageContainer, id, type, heading, content, kvps = null) {
231 - return drawMessageAgentPlain(['message-info'], messageContainer, id, type, heading, content, kvps);
107 +export function drawMessageInfo(messageContainer, id, type, heading, content, temp, kvps = null) {
108 + return drawMessageAgentPlain(['message-info'], messageContainer, id, type, heading, content, temp, kvps);
109 }
110
234 -export function drawMessageWarning(messageContainer, id, type, heading, content, kvps = null) {
235 - return drawMessageAgentPlain(['message-warning'], messageContainer, id, type, heading, content, kvps);
111 +export function drawMessageWarning(messageContainer, id, type, heading, content, temp, kvps = null) {
112 + return drawMessageAgentPlain(['message-warning'], messageContainer, id, type, heading, content, temp, kvps);
113 }
114
238 -export function drawMessageError(messageContainer, id, type, heading, content, kvps = null) {
239 - return drawMessageAgentPlain(['message-error'], messageContainer, id, type, heading, content, kvps);
115 +export function drawMessageError(messageContainer, id, type, heading, content, temp, kvps = null) {
116 + return drawMessageAgentPlain(['message-error'], messageContainer, id, type, heading, content, temp, kvps);
117 }
118
119 function drawKvps(container, kvps) {