context separation

contexts async multiple chats

frdel committed Sep 4, 2024 at 00:05 UTC 68ba6e9ecfefdfea696a66093b2d5d598c020967
28 files changed +689 -478
.gitignore
-1
@@ -3,7 +3,6 @@
3 **/__pycache__/
4
5
6 -
6 # Ignore all contents of the virtual environment directory
7 .venv/*
8
agent.py
+147 -94
@@ -1,6 +1,8 @@
1 +import asyncio
2 from dataclasses import dataclass, field
3 import time, importlib, inspect, os, json
4 from typing import Any, Optional, Dict
5 +import uuid
6 from python.helpers import extract_tools, rate_limiter, files, errors
7 from python.helpers.print_style import PrintStyle
8 from langchain.schema import AIMessage
@@ -9,11 +11,72 @@ from langchain_core.messages import HumanMessage, SystemMessage
11 from langchain_core.language_models.chat_models import BaseChatModel
12 from langchain_core.language_models.llms import BaseLLM
13 from langchain_core.embeddings import Embeddings
12 -from concurrent.futures import Future
13 -from python.helpers.log import Log
14 +import python.helpers.log as Log
15 from python.helpers.dirty_json import DirtyJson
16 +from python.helpers.defer import DeferredTask
17
18 +class AgentContext:
19
20 + _contexts: dict[str, 'AgentContext'] = {}
21 + _counter: int = 0
22 +
23 + def __init__(self, config: 'AgentConfig', id:str|None = None, agent0: 'Agent|None' = None):
24 + # build context
25 + self.id = id or str(uuid.uuid4())
26 + self.config = config
27 + self.log = Log.Log()
28 + self.agent0 = agent0 or Agent(0, self.config, self)
29 + self.paused = False
30 + self.streaming_agent: Agent|None = None
31 + self.process: DeferredTask|None = None
32 + AgentContext._counter += 1
33 + self.no = AgentContext._counter
34 +
35 + self._contexts[self.id] = self
36 +
37 + @staticmethod
38 + def get(id:str):
39 + return AgentContext._contexts.get(id, None)
40 +
41 + @staticmethod
42 + def first():
43 + if not AgentContext._contexts: return None
44 + return list(AgentContext._contexts.values())[0]
45 +
46 +
47 + @staticmethod
48 + def remove(id:str):
49 + context = AgentContext._contexts.pop(id, None)
50 + if context and context.process: context.process.kill()
51 + return context
52 +
53 + def reset(self):
54 + if self.process: self.process.kill()
55 + self.log.reset()
56 + self.agent0 = Agent(0, self.config, self)
57 + self.streaming_agent = None
58 + self.paused = False
59 +
60 +
61 + def communicate(self, msg: str, broadcast_level: int = 1):
62 + self.paused=False #unpause if paused
63 +
64 + if self.process and self.process.is_alive():
65 + if self.streaming_agent: current_agent = self.streaming_agent
66 + else: current_agent = self.agent0
67 +
68 + # set intervention messages to agent(s):
69 + intervention_agent = current_agent
70 + while intervention_agent and broadcast_level !=0:
71 + intervention_agent.intervention_message = msg
72 + broadcast_level -= 1
73 + intervention_agent = intervention_agent.data.get("superior",None)
74 + else:
75 + self.process = DeferredTask(self.agent0.message_loop, msg)
76 +
77 + return self.process
78 +
79 +
80 @dataclass
81 class AgentConfig:
82 chat_model: BaseChatModel | BaseLLM
@@ -44,18 +107,25 @@ class AgentConfig:
107 code_exec_ssh_user: str = "root"
108 code_exec_ssh_pass: str = "toor"
109 additional: Dict[str, Any] = field(default_factory=dict)
47 -
110
49 -class Agent:
111 +# intervention exception class - skips rest of message loop iteration
112 +class InterventionException(Exception):
113 + pass
114 +
115 +# killer exception class - not forwarded to LLM, cannot be fixed on its own, ends message loop
116 +class KillerException(Exception):
117 + pass
118
51 - paused=False
52 - streaming_agent=None
119 +class Agent:
120
54 - def __init__(self, number:int, config: AgentConfig):
121 + def __init__(self, number:int, config: AgentConfig, context: AgentContext|None = None):
122
123 # agent config
124 self.config = config
125
126 + # agent context
127 + self.context = context or AgentContext(config)
128 +
129 # non-config vars
130 self.number = number
131 self.agent_name = f"Agent {self.number}"
@@ -63,44 +133,24 @@ class Agent:
133 self.history = []
134 self.last_message = ""
135 self.intervention_message = ""
66 - self.intervention_status = False
67 - self.rate_limiter = rate_limiter.RateLimiter(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)
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
69 - self.future: Future|None = None
70 -
71 -
72 - os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir
138
74 - def communicate(self, msg: str):
75 - Agent.paused=False #unpause if paused
76 -
77 - if not self.future or self.future.done():
78 - return self.message_loop(msg)
79 - else:
80 - if Agent.streaming_agent: current_agent = Agent.streaming_agent
81 - else: current_agent = self
82 -
83 - current_agent.intervention_message = msg #intervene current agent
84 - if self.future: return self.future.result() #wait for original agent
85 - else: return ""
86 -
87 - def message_loop(self, msg: str):
139 + async def message_loop(self, msg: str):
140 try:
89 - self.future = Future()
141 printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
142 user_message = self.read_prompt("fw.user_message.md", message=msg)
92 - self.append_message(user_message, human=True) # Append the user's input to the history
93 - memories = self.fetch_memories(True)
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
96 - Agent.streaming_agent = self #mark self as current streamer
147 + self.context.streaming_agent = self #mark self as current streamer
148 agent_response = ""
98 - self.intervention_status = False # reset interventon status
149
150 try:
151
152 system = self.read_prompt("agent.system.md", agent_name=self.agent_name) + "\n\n" + self.read_prompt("agent.tools.md")
103 - memories = self.fetch_memories()
153 + memories = await self.fetch_memories()
154 if memories: system+= "\n\n"+memories
155
156 prompt = ChatPromptTemplate.from_messages([
@@ -116,10 +166,10 @@ class Agent:
166
167 # output that the agent is starting
168 PrintStyle(bold=True, font_color="green", padding=True, background_color="white").print(f"{self.agent_name}: Generating:")
119 - log = Log(type="agent", heading=f"{self.agent_name}: Generating:")
169 + log = self.context.log.log(type="agent", heading=f"{self.agent_name}: Generating:")
170
121 - for chunk in chain.stream(inputs):
122 - if self.handle_intervention(agent_response): break # wait for intervention and handle it, if paused
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)
@@ -130,34 +180,41 @@ class Agent:
180 agent_response += content # concatenate stream into the response
181 self.log_from_stream(agent_response, log)
182
133 - self.rate_limiter.set_output_tokens(int(len(agent_response)/4))
183 + self.rate_limiter.set_output_tokens(int(len(agent_response)/4)) # rough estimation
184
135 - if not self.handle_intervention(agent_response):
136 - if self.last_message == agent_response: #if assistant_response is the same as last message in history, let him know
137 - self.append_message(agent_response) # Append the assistant's response to the history
138 - warning_msg = self.read_prompt("fw.msg_repeat.md")
139 - self.append_message(warning_msg, human=True) # Append warning message to the history
140 - PrintStyle(font_color="orange", padding=True).print(warning_msg)
141 - Log.log(type="warning", content=warning_msg)
142 -
143 - else: #otherwise proceed with tool
144 - self.append_message(agent_response) # Append the assistant's response to the history
145 - tools_result = self.process_tools(agent_response) # process tools requested in agent message
146 - if tools_result: #final response of message loop available
147 - self.future.set_result(tools_result) #set result to future
148 - return tools_result #break the execution if the task is done
149 -
150 - # Forward errors to the LLM, maybe it can fix them
151 - except Exception as e:
185 + await self.handle_intervention(agent_response)
186 +
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
189 + warning_msg = self.read_prompt("fw.msg_repeat.md")
190 + await self.append_message(warning_msg, human=True) # Append warning message to the history
191 + PrintStyle(font_color="orange", padding=True).print(warning_msg)
192 + self.context.log.log(type="warning", content=warning_msg)
193 +
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
199 +
200 + except InterventionException as e:
201 + pass # intervention message has been handled in handle_intervention(), proceed with conversation loop
202 + 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
210 error_message = errors.format_error(e)
211 msg_response = self.read_prompt("fw.error.md", error=error_message) # error message template
154 - self.append_message(msg_response, human=True)
212 + await self.append_message(msg_response, human=True)
213 PrintStyle(font_color="red", padding=True).print(msg_response)
156 - Log.log(type="error", content=msg_response)
157 - self.future.set_exception(e) #set result to future
214 + self.context.log.log(type="error", content=msg_response)
215
216 finally:
160 - Agent.streaming_agent = None # unset current streamer
217 + self.context.streaming_agent = None # unset current streamer
218
219 def read_prompt(self, file:str, **kwargs):
220 content = ""
@@ -176,21 +233,21 @@ class Agent:
233 def set_data(self, field:str, value):
234 self.data[field] = value
235
179 - def append_message(self, msg: str, human: bool = False):
236 + async def append_message(self, msg: str, human: bool = False):
237 message_type = "human" if human else "ai"
238 if self.history and self.history[-1].type == message_type:
239 self.history[-1].content += "\n\n" + msg
240 else:
241 new_message = HumanMessage(content=msg) if human else AIMessage(content=msg)
242 self.history.append(new_message)
186 - self.cleanup_history(self.config.msgs_keep_max, self.config.msgs_keep_start, self.config.msgs_keep_end)
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":
245 self.last_message = msg
246
247 def concat_messages(self,messages):
248 return "\n".join([f"{msg.type}: {msg.content}" for msg in messages])
249
193 - def send_adhoc_message(self, system: str, msg: str, output_label:str):
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)])
@@ -203,13 +260,13 @@ class Agent:
260 if output_label:
261 PrintStyle(bold=True, font_color="orange", padding=True, background_color="white").print(f"{self.agent_name}: {output_label}:")
262 printer = PrintStyle(italic=True, font_color="orange", padding=False)
206 - logger = Log(type="adhoc", heading=f"{self.agent_name}: {output_label}:")
263 + logger = self.context.log.log(type="adhoc", heading=f"{self.agent_name}: {output_label}:")
264
265 formatted_inputs = prompt.format()
266 tokens = int(len(formatted_inputs)/4)
267 self.rate_limiter.limit_call_and_input(tokens)
268
212 - for chunk in chain.stream({}):
269 + async for chunk in chain.astream({}):
270 if self.handle_intervention(): break # wait for intervention and handle it, if paused
271
272 if isinstance(chunk, str): content = chunk
@@ -228,13 +285,13 @@ class Agent:
285 if self.history:
286 return self.history[-1]
287
231 - def replace_middle_messages(self,middle_messages):
288 + async def replace_middle_messages(self,middle_messages):
289 cleanup_prompt = self.read_prompt("fw.msg_cleanup.md")
233 - summary = self.send_adhoc_message(system=cleanup_prompt,msg=self.concat_messages(middle_messages), output_label="Mid messages cleanup summary")
290 + summary = await self.send_adhoc_message(system=cleanup_prompt,msg=self.concat_messages(middle_messages), output_label="Mid messages cleanup summary")
291 new_human_message = HumanMessage(content=summary)
292 return [new_human_message]
293
237 - def cleanup_history(self, max:int, keep_start:int, keep_end:int):
294 + async def cleanup_history(self, max:int, keep_start:int, keep_end:int):
295 if len(self.history) <= max:
296 return self.history
297
@@ -254,48 +311,44 @@ class Agent:
311 middle_part = middle_part[:-1]
312
313 # Replace the middle part using the replacement function
257 - new_middle_part = self.replace_middle_messages(middle_part)
314 + new_middle_part = await self.replace_middle_messages(middle_part)
315
316 self.history = first_x + new_middle_part + last_y
317
318 return self.history
319
263 - def handle_intervention(self, progress:str="") -> bool:
264 - while self.paused: time.sleep(0.1) # wait if paused
265 - if self.intervention_message and not self.intervention_status: # if there is an intervention message, but not yet processed
266 - if progress.strip(): self.append_message(progress) # append the response generated so far
267 - user_msg = self.read_prompt("fw.intervention.md", user_message=self.intervention_message) # format the user intervention template
268 - self.append_message(user_msg,human=True) # append the intervention message
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
323 + msg = self.intervention_message
324 self.intervention_message = "" # reset the intervention message
270 - self.intervention_status = True
271 - return self.intervention_status # return intervention status
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=self.intervention_message) # format the user intervention template
327 + await self.append_message(user_msg,human=True) # append the intervention message
328 + raise InterventionException(msg)
329
273 - def process_tools(self, msg: str):
330 + async def process_tools(self, msg: str):
331 # search for tool usage requests in agent message
332 tool_request = extract_tools.json_parse_dirty(msg)
333
334 if tool_request is not None:
335 tool_name = tool_request.get("tool_name", "")
336 tool_args = tool_request.get("tool_args", {})
280 -
281 - tool = self.get_tool(
282 - tool_name,
283 - tool_args,
284 - msg)
337 + tool = self.get_tool(tool_name, tool_args, msg)
338
286 - if self.handle_intervention(): return # wait if paused and handle intervention message if needed
287 - tool.before_execution(**tool_args)
288 - if self.handle_intervention(): return # wait if paused and handle intervention message if needed
289 - response = tool.execute(**tool_args)
290 - if self.handle_intervention(): return # wait if paused and handle intervention message if needed
291 - tool.after_execution(response)
292 - if self.handle_intervention(): return # wait if paused and handle intervention message if needed
339 + await self.handle_intervention() # wait if paused and handle intervention message if needed
340 + await tool.before_execution(**tool_args)
341 + await self.handle_intervention() # wait if paused and handle intervention message if needed
342 + response = await tool.execute(**tool_args)
343 + await self.handle_intervention() # wait if paused and handle intervention message if needed
344 + 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
347 else:
348 msg = self.read_prompt("fw.msg_misformat.md")
296 - self.append_message(msg, human=True)
349 + await self.append_message(msg, human=True)
350 PrintStyle(font_color="red", padding=True).print(msg)
298 - Log.log(type="error", content=f"{self.agent_name}: Message misformat:")
351 + self.context.log.log(type="error", content=f"{self.agent_name}: Message misformat:")
352
353
354 def get_tool(self, name: str, args: dict, message: str, **kwargs):
@@ -314,7 +367,7 @@ class Agent:
367
368 return tool_class(agent=self, name=name, args=args, message=message, **kwargs)
369
317 - def fetch_memories(self,reset_skip=False):
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
@@ -331,14 +384,14 @@ class Agent:
384 "raw_memories": memories
385 }
386 cleanup_prompt = self.read_prompt("msg.memory_cleanup.md").replace("{", "{{")
334 - clean_memories = self.send_adhoc_message(cleanup_prompt,json.dumps(input), output_label="Memory injection")
387 + clean_memories = await self.send_adhoc_message(cleanup_prompt,json.dumps(input), output_label="Memory injection")
388 return clean_memories
389
337 - def log_from_stream(self, stream: str, log: Log):
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)
341 - if isinstance(response, dict): log.update(content=stream, kvps=response) #log if result is a dictionary already
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
397
initialize.py
+3 -6
@@ -1,5 +1,5 @@
1 import models
2 -from agent import Agent, AgentConfig
2 +from agent import AgentConfig
3
4 def initialize():
5
@@ -52,9 +52,6 @@ def initialize():
52 # code_exec_ssh_pass = "toor",
53 # additional = {},
54 )
55 -
56 - # create the first agent
57 - agent0 = Agent( number = 0, config = config )
55
59 - # return initialized agent
60 - return agent0
\ No newline at end of file
56 + # return config object
57 + return config
\ No newline at end of file
python/helpers/defer.py new
+61
@@ -0,0 +1,61 @@
1 +import asyncio
2 +import threading
3 +from concurrent.futures import Future
4 +
5 +class DeferredTask:
6 + def __init__(self, func, *args, **kwargs):
7 + self._loop = asyncio.new_event_loop()
8 + # self._thread = None
9 + self._task = None
10 + self._future = Future()
11 + self._start_task(func, *args, **kwargs)
12 +
13 + def _start_task(self, func, *args, **kwargs):
14 + def run_in_thread(loop, func, args, kwargs):
15 + asyncio.set_event_loop(loop)
16 + self._task = loop.create_task(self._run(func, *args, **kwargs))
17 + loop.run_forever()
18 +
19 + self._thread = threading.Thread(target=run_in_thread, args=(self._loop, func, args, kwargs))
20 + self._thread.start()
21 +
22 + async def _run(self, func, *args, **kwargs):
23 + try:
24 + result = await func(*args, **kwargs)
25 + self._future.set_result(result)
26 + except Exception as e:
27 + self._future.set_exception(e)
28 + finally:
29 + self._loop.call_soon_threadsafe(self._loop.stop)
30 +
31 + def is_ready(self):
32 + return self._future.done()
33 +
34 + async def result(self, timeout=None):
35 + if self._task is None:
36 + raise RuntimeError("Task was not initialized properly.")
37 +
38 + try:
39 + return await asyncio.wait_for(asyncio.wrap_future(self._future), timeout)
40 + except asyncio.TimeoutError:
41 + raise TimeoutError("The task did not complete within the specified timeout.")
42 +
43 + def result_sync(self, timeout=None):
44 + try:
45 + return self._future.result(timeout)
46 + except TimeoutError:
47 + raise TimeoutError("The task did not complete within the specified timeout.")
48 +
49 + def kill(self):
50 + if self._task and not self._task.done():
51 + self._loop.call_soon_threadsafe(self._task.cancel)
52 +
53 + def is_alive(self):
54 + return self._thread.is_alive() and not self._future.done()
55 +
56 + def __del__(self):
57 + if self._loop.is_running():
58 + self._loop.call_soon_threadsafe(self._loop.stop)
59 + if self._thread.is_alive():
60 + self._thread.join()
61 + self._loop.close()
\ No newline at end of file
python/helpers/docker.py
+9 -8
@@ -8,7 +8,8 @@ from python.helpers.print_style import PrintStyle
8 from python.helpers.log import Log
9
10 class DockerContainerManager:
11 - def __init__(self, image: str, name: str, ports: Optional[dict[str, int]] = None, volumes: Optional[dict[str, dict[str, str]]] = None):
11 + def __init__(self, logger: Log, image: str, name: str, ports: Optional[dict[str, int]] = None, volumes: Optional[dict[str, dict[str, str]]] = None):
12 + self.logger = logger
13 self.image = image
14 self.name = name
15 self.ports = ports
@@ -25,9 +26,9 @@ class DockerContainerManager:
26 err = format_error(e)
27 if ("ConnectionRefusedError(61," in err or "Error while fetching server API version" in err):
28 PrintStyle.hint("Connection to Docker failed. Is docker or Docker Desktop running?") # hint for user
28 - Log.log(type="hint", content="Connection to Docker failed. Is docker or Docker Desktop running?")
29 + self.logger.log(type="hint", content="Connection to Docker failed. Is docker or Docker Desktop running?")
30 PrintStyle.error(err)
30 - Log.log(type="error", content=err)
31 + self.logger.log(type="error", content=err)
32 time.sleep(5) # try again in 5 seconds
33 else: raise
34 return self.client
@@ -38,10 +39,10 @@ class DockerContainerManager:
39 self.container.stop()
40 self.container.remove()
41 print(f"Stopped and removed the container: {self.container.id}")
41 - Log.log(type="info", content=f"Stopped and removed the container: {self.container.id}")
42 + self.logger.log(type="info", content=f"Stopped and removed the container: {self.container.id}")
43 except Exception as e:
44 print(f"Failed to stop and remove the container: {e}")
44 - Log.log(type="error", content=f"Failed to stop and remove the container: {e}")
45 + self.logger.log(type="error", content=f"Failed to stop and remove the container: {e}")
46
47
48 def start_container(self) -> None:
@@ -55,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...")
58 - Log.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...")
60
61 existing_container.start()
62 self.container = existing_container
@@ -66,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...")
69 - Log.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...")
71
72 self.container = self.client.containers.run(
73 self.image,
@@ -77,5 +78,5 @@ class DockerContainerManager:
78 )
79 atexit.register(self.cleanup_container)
80 print(f"Started container with ID: {self.container.id}")
80 - Log.log(type="info", content=f"Started container with ID: {self.container.id}")
81 + self.logger.log(type="info", content=f"Started container with ID: {self.container.id}")
82 time.sleep(5) # this helps to get SSH ready
python/helpers/errors.py
+6
@@ -1,6 +1,12 @@
1 import re
2 import traceback
3 +import asyncio
4
5 +def handle_error(e: Exception):
6 + # if asyncio.CancelledError, re-raise
7 + if isinstance(e, asyncio.CancelledError):
8 + raise e
9 +
10 def format_error(e: Exception, max_entries=2):
11 traceback_text = traceback.format_exc()
12 # Split the traceback into lines
python/helpers/knowledge_import.py
+3 -3
@@ -28,7 +28,7 @@ def calculate_checksum(file_path: str) -> str:
28 hasher.update(buf)
29 return hasher.hexdigest()
30
31 -def load_knowledge(knowledge_dir: str, index: Dict[str, KnowledgeImport]) -> Dict[str, KnowledgeImport]:
31 +def load_knowledge(logger: Log, knowledge_dir: str, index: Dict[str, KnowledgeImport]) -> Dict[str, KnowledgeImport]:
32 knowledge_dir = files.get_abs_path(knowledge_dir)
33
34
@@ -49,7 +49,7 @@ def load_knowledge(knowledge_dir: str, index: Dict[str, KnowledgeImport]) -> Dic
49 kn_files = glob.glob(knowledge_dir + '/**/*', recursive=True)
50 if kn_files:
51 print(f"Found {len(kn_files)} knowledge files in {knowledge_dir}, processing...")
52 - Log.log(type="info", content=f"Found {len(kn_files)} knowledge files in {knowledge_dir}, processing...")
52 + logger.log(type="info", content=f"Found {len(kn_files)} knowledge files in {knowledge_dir}, processing...")
53
54 for file_path in kn_files:
55 ext = file_path.split('.')[-1].lower()
@@ -83,5 +83,5 @@ def load_knowledge(knowledge_dir: str, index: Dict[str, KnowledgeImport]) -> Dic
83 index[file_key]['state'] = 'removed'
84
85 print(f"Processed {cnt_docs} documents from {cnt_files} files.")
86 - Log.log(type="info", content=f"Processed {cnt_docs} documents from {cnt_files} files.")
86 + logger.log(type="info", content=f"Processed {cnt_docs} documents from {cnt_files} files.")
87 return index
python/helpers/log.py
+64 -44
@@ -1,58 +1,78 @@
1 -from dataclasses import dataclass
1 +from dataclasses import dataclass, field
2 +import json
3 from typing import Optional, Dict
4 import uuid
5
6 +
7 @dataclass
8 class LogItem:
9 + log: 'Log'
10 no: int
11 type: str
12 heading: str
13 content: str
14 kvps: Optional[Dict] = None
12 -
15 + guid: str = ""
16 +
17 + def __post_init__(self):
18 + self.guid = self.log.guid
19 +
20 + def update(self, type: str | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None):
21 + if self.guid == self.log.guid:
22 + self.log.update_item(self.no, type=type, heading=heading, content=content, kvps=kvps)
23 +
24 + def output(self):
25 + return {
26 + "no": self.no,
27 + "type": self.type,
28 + "heading": self.heading,
29 + "content": self.content,
30 + "kvps": self.kvps
31 + }
32
33 class Log:
34
16 - guid = uuid.uuid4()
17 - version: int = 0
18 - last_updated: int = 0
19 - logs: list = []
20 -
21 - def __init__(self, type: str="placeholder", heading: str="", content: str="", kvps: dict|None = None):
22 - self.item = Log.log(type, heading, content, kvps) # create placeholder log item that will be updated
23 -
24 - def update(self, type: Optional[str] = None, heading: str|None = None, content: str|None = None, kvps: dict|None = None):
25 - Log.edit(self.item.no, type=type, heading=heading, content=content, kvps=kvps)
26 -
27 - @staticmethod
28 - def reset():
29 - Log.guid = uuid.uuid4()
30 - Log.version = 0
31 - Log.last_updated = 0
32 - Log.logs = []
33 -
34 - @staticmethod
35 - def log(type: str, heading: str|None = None, content: str|None = None, kvps: dict|None = None):
36 - item = LogItem(len(Log.logs), type, heading or "", content or "", kvps)
37 - Log.logs.append(item)
38 - Log.last_updated = item.no
39 - Log.version += 1
35 + def __init__(self):
36 + self.guid: str = str(uuid.uuid4())
37 + self.updates: list[int] = []
38 + self.logs: list[LogItem] = []
39 +
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)
42 + self.logs.append(item)
43 + self.updates += [item.no]
44 return item
41 -
42 - @staticmethod
43 - def edit(no: int, type: Optional[str] = None, heading: str|None = None, content: str|None = None, kvps: dict|None = None):
44 - if 0 <= no < len(Log.logs):
45 - item = Log.logs[no]
46 - if type is not None:
47 - item.type = type
48 - if heading is not None:
49 - item.heading = heading
50 - if content is not None:
51 - item.content = content
52 - if kvps is not None:
53 - item.kvps = kvps
54 -
55 - Log.last_updated = no
56 - Log.version += 1
57 - else:
58 - raise IndexError("Log item number out of range")
45 +
46 + def update_item(self, no: int, type: str | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None):
47 + item = self.logs[no]
48 + if type is not None:
49 + item.type = type
50 + if heading is not None:
51 + item.heading = heading
52 + if content is not None:
53 + item.content = content
54 + if kvps is not None:
55 + item.kvps = kvps
56 + self.updates += [item.no]
57 +
58 + def output(self, start=None, end=None):
59 + if start is None:
60 + start = 0
61 + if end is None:
62 + end = len(self.updates)
63 +
64 + out = []
65 + seen = set()
66 + for update in self.updates[start:end]:
67 + if update not in seen:
68 + out.append(self.logs[update].output())
69 + seen.add(update)
70 +
71 + return out
72 +
73 +
74 +
75 + def reset(self):
76 + self.guid = str(uuid.uuid4())
77 + self.updates = []
78 + self.logs = []
python/helpers/rate_limiter.py
+3 -2
@@ -12,7 +12,8 @@ class CallRecord:
12 output_tokens: int = 0 # Default to 0, will be set separately
13
14 class RateLimiter:
15 - def __init__(self, max_calls: int, max_input_tokens: int, max_output_tokens: int, window_seconds: int = 60):
15 + def __init__(self, logger: Log, max_calls: int, max_input_tokens: int, max_output_tokens: int, window_seconds: int = 60):
16 + self.logger = logger
17 self.max_calls = max_calls
18 self.max_input_tokens = max_input_tokens
19 self.max_output_tokens = max_output_tokens
@@ -49,7 +50,7 @@ class RateLimiter:
50 wait_time = oldest_record.timestamp + self.window_seconds - current_time
51 if wait_time > 0:
52 PrintStyle(font_color="yellow", padding=True).print(f"Rate limit exceeded. Waiting for {wait_time:.2f} seconds due to: {', '.join(wait_reasons)}")
52 - Log.log("rate_limit","Rate limit exceeded",f"Rate limit exceeded. Waiting for {wait_time:.2f} seconds due to: {', '.join(wait_reasons)}")
53 + self.logger.log("rate_limit","Rate limit exceeded",f"Rate limit exceeded. Waiting for {wait_time:.2f} seconds due to: {', '.join(wait_reasons)}")
54 time.sleep(wait_time)
55 current_time = time.time()
56
python/helpers/shell_local.py
+2 -2
@@ -9,7 +9,7 @@ class LocalInteractiveSession:
9 self.process = None
10 self.full_output = ''
11
12 - def connect(self):
12 + async def connect(self):
13 # Start a new subprocess with the appropriate shell for the OS
14 if sys.platform.startswith('win'):
15 # Windows
@@ -44,7 +44,7 @@ class LocalInteractiveSession:
44 self.process.stdin.write(command + '\n') # type: ignore
45 self.process.stdin.flush() # type: ignore
46
47 - def read_output(self) -> Tuple[str, Optional[str]]:
47 + async def read_output(self) -> Tuple[str, Optional[str]]:
48 if not self.process:
49 raise Exception("Shell not connected")
50
python/helpers/shell_ssh.py
+22 -11
@@ -1,7 +1,8 @@
1 +import asyncio
2 import paramiko
3 import time
4 import re
4 -from typing import Optional, Tuple
5 +from typing import Tuple
6 from python.helpers.log import Log
7 from python.helpers.strings import calculate_valid_match_lengths
8
@@ -10,7 +11,8 @@ class SSHInteractiveSession:
11 # end_comment = "# @@==>> SSHInteractiveSession End-of-Command <<==@@"
12 # ps1_label = "SSHInteractiveSession CLI>"
13
13 - def __init__(self, hostname: str, port: int, username: str, password: str):
14 + def __init__(self, logger: Log, hostname: str, port: int, username: str, password: str):
15 + self.logger = logger
16 self.hostname = hostname
17 self.port = port
18 self.username = username
@@ -23,7 +25,7 @@ class SSHInteractiveSession:
25 self.trimmed_command_length = 0 # Initialize trimmed_command_length
26
27
26 - def connect(self):
28 + async def connect(self):
29 # try 3 times with wait and then except
30 errors = 0
31 while True:
@@ -33,14 +35,14 @@ class SSHInteractiveSession:
35 # self.shell.send(f'PS1="{SSHInteractiveSession.ps1_label}"'.encode())
36 # return
37 while True: # wait for end of initial output
36 - full, part = self.read_output()
38 + full, part = await self.read_output()
39 if full and not part: return
40 time.sleep(0.1)
41 except Exception as e:
42 errors += 1
43 if errors < 3:
44 print(f"SSH Connection attempt {errors}...")
43 - Log.log(type="info", content=f"SSH Connection attempt {errors}...")
45 + self.logger.log(type="info", content=f"SSH Connection attempt {errors}...")
46
47 time.sleep(5)
48 else:
@@ -64,11 +66,12 @@ class SSHInteractiveSession:
66 self.trimmed_command_length = 0
67 self.shell.send(self.last_command)
68
67 - def read_output(self) -> Tuple[str, str]:
69 + async def read_output(self) -> Tuple[str, str]:
70 if not self.shell:
71 raise Exception("Shell not connected")
72
73 partial_output = b''
74 + leftover = b''
75
76 while self.shell.recv_ready():
77 data = self.shell.recv(1024)
@@ -76,17 +79,25 @@ class SSHInteractiveSession:
79 # Trim own command from output
80 if self.last_command and len(self.last_command) > self.trimmed_command_length:
81 command_to_trim = self.last_command[self.trimmed_command_length:]
79 -
82 + data_to_trim = leftover + data
83 +
84 trim_com, trim_out = calculate_valid_match_lengths(
81 - command_to_trim, data, deviation_threshold=8, deviation_reset=2,
82 - ignore_patterns=[rb'\x1b\[\?\d{4}[a-zA-Z](?:> )?', rb'\r', rb'>'])
85 + command_to_trim, data_to_trim, deviation_threshold=8, deviation_reset=2,
86 + ignore_patterns = [
87 + rb'\x1b\[\?\d{4}[a-zA-Z](?:> )?', # ANSI escape sequences
88 + rb'\r', # Carriage return
89 + rb'>\s', # Greater-than symbol
90 + ], debug=False)
91 +
92 + leftover = b''
93 if(trim_com > 0 and trim_out > 0):
84 - data = data[trim_out:]
94 + data = data_to_trim[trim_out:]
95 + leftover = data
96 self.trimmed_command_length += trim_com
97
98 partial_output += data
99 self.full_output += data
89 - time.sleep(0.1) # Prevent busy waiting
100 + await asyncio.sleep(0.1) # Prevent busy waiting
101
102 # Decode once at the end
103 decoded_partial_output = partial_output.decode('utf-8', errors='replace')
python/helpers/strings.py
+23 -55
@@ -1,61 +1,12 @@
1 -# def calculate_valid_match_lengths(first: bytes | str, second: bytes | str, deviation_threshold: int = 5, deviation_reset: int = 5) -> tuple[int, int]:
2 -# first_length = len(first)
3 -# second_length = len(second)
4 -
5 -# i, j = 0, 0
6 -# deviations = 0
7 -# matched_since_deviation = 0
8 -# last_matched_i, last_matched_j = 0, 0 # Track the last matched index
9 -
10 -# while i < first_length and j < second_length:
11 -# if first[i] == second[j]:
12 -# last_matched_i, last_matched_j = i + 1, j + 1 # Update last matched position
13 -# i += 1
14 -# j += 1
15 -# matched_since_deviation += 1
16 -
17 -# # Reset the deviation counter if we've matched enough characters since the last deviation
18 -# if matched_since_deviation >= deviation_reset:
19 -# deviations = 0
20 -# matched_since_deviation = 0
21 -# else:
22 -# # Determine the look-ahead based on the remaining deviation threshold
23 -# look_ahead = deviation_threshold - deviations
24 -
25 -# # Look ahead to find the best match within the remaining deviation allowance
26 -# best_match = None
27 -# for k in range(1, look_ahead + 1):
28 -# if i + k < first_length and first[i + k] == second[j]:
29 -# best_match = ('i', k)
30 -# break
31 -# if j + k < second_length and first[i] == second[j + k]:
32 -# best_match = ('j', k)
33 -# break
34 -
35 -# if best_match:
36 -# if best_match[0] == 'i':
37 -# i += best_match[1]
38 -# elif best_match[0] == 'j':
39 -# j += best_match[1]
40 -# else:
41 -# i += 1
42 -# j += 1
43 -
44 -# deviations += 1
45 -# matched_since_deviation = 0
46 -
47 -# if deviations > deviation_threshold:
48 -# break
49 -
50 -# # Return the last matched positions instead of the current indices
51 -# return last_matched_i, last_matched_j
52 -
1 import re
2 +import sys
3 +import time
4
5 def calculate_valid_match_lengths(first: bytes | str, second: bytes | str,
6 deviation_threshold: int = 5,
7 deviation_reset: int = 5,
58 - ignore_patterns: list[bytes|str] = []) -> tuple[int, int]:
8 + ignore_patterns: list[bytes|str] = [],
9 + debug: bool = False) -> tuple[int, int]:
10
11 first_length = len(first)
12 second_length = len(second)
@@ -99,10 +50,10 @@ def calculate_valid_match_lengths(first: bytes | str, second: bytes | str,
50 # Look ahead to find the best match within the remaining deviation allowance
51 best_match = None
52 for k in range(1, look_ahead + 1):
102 - if i + k < first_length and first[i + k] == second[j]:
53 + if i + k < first_length and j < second_length and first[i + k] == second[j]:
54 best_match = ('i', k)
55 break
105 - if j + k < second_length and first[i] == second[j + k]:
56 + if j + k < second_length and i < first_length and first[i] == second[j + k]:
57 best_match = ('j', k)
58 break
59
@@ -121,5 +72,22 @@ def calculate_valid_match_lengths(first: bytes | str, second: bytes | str,
72 if deviations > deviation_threshold:
73 break
74
75 + if debug:
76 + output = (
77 + f"First (up to {last_matched_i}): {first[:last_matched_i]!r}\n"
78 + "\n"
79 + f"Second (up to {last_matched_j}): {second[:last_matched_j]!r}\n"
80 + "\n"
81 + f"Current deviation: {deviations}\n"
82 + f"Matched since last deviation: {matched_since_deviation}\n"
83 + + "-" * 40 + "\n"
84 + )
85 + sys.stdout.write("\r" + output)
86 + sys.stdout.flush()
87 + time.sleep(0.01) # Add a short delay for readability (optional)
88 +
89 + # Return the last matched positions instead of the current indices
90 + return last_matched_i, last_matched_j
91 +
92 # Return the last matched positions instead of the current indices
93 return last_matched_i, last_matched_j
\ No newline at end of file
python/helpers/tool.py
+10 -13
@@ -1,14 +1,13 @@
1 from abc import abstractmethod
2 -from typing import TypedDict
2 +from dataclasses import dataclass
3 from agent import Agent
4 from python.helpers.print_style import PrintStyle
5 -from python.helpers import files, messages
6 -from python.helpers.log import Log
5 +from python.helpers import messages
6
7 +@dataclass
8 class Response:
9 - def __init__(self, message: str, break_loop: bool) -> None:
10 - self.message = message
11 - self.break_loop = break_loop
9 + message:str
10 + break_loop:bool
11
12 class Tool:
13
@@ -19,24 +18,22 @@ class Tool:
18 self.message = message
19
20 @abstractmethod
22 - def execute(self,**kwargs) -> Response:
21 + async def execute(self,**kwargs) -> Response:
22 pass
23
25 - def before_execution(self, **kwargs):
26 - if self.agent.handle_intervention(): return # wait for intervention and handle it, if paused
24 + async def before_execution(self, **kwargs):
25 PrintStyle(font_color="#1B4F72", padding=True, background_color="white", bold=True).print(f"{self.agent.agent_name}: Using tool '{self.name}':")
28 - self.log = Log(type="tool", heading=f"{self.agent.agent_name}: Using tool '{self.name}':", content="", kvps=self.args)
26 + self.log = self.agent.context.log.log(type="tool", heading=f"{self.agent.agent_name}: Using tool '{self.name}':", content="", kvps=self.args)
27 if self.args and isinstance(self.args, dict):
28 for key, value in self.args.items():
29 PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ")
30 PrintStyle(font_color="#85C1E9", padding=isinstance(value,str) and "\n" in value).stream(value)
31 PrintStyle().print()
32
35 - def after_execution(self, response: Response, **kwargs):
33 + async def after_execution(self, response: Response, **kwargs):
34 text = messages.truncate_text(self.agent, response.message.strip(), self.agent.config.max_tool_response_length)
35 msg_response = self.agent.read_prompt("fw.tool_response.md", tool_name=self.name, tool_response=text)
38 - if self.agent.handle_intervention(): return # wait for intervention and handle it, if paused
39 - self.agent.append_message(msg_response, human=True)
36 + await self.agent.append_message(msg_response, human=True)
37 PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}':")
38 PrintStyle(font_color="#85C1E9").print(response.message)
39 self.log.update(content=response.message)
python/helpers/vector_db.py
+5 -3
@@ -14,9 +14,11 @@ from python.helpers.log import Log
14
15 class VectorDB:
16
17 - def __init__(self, embeddings_model, in_memory=False, memory_dir="./memory", knowledge_dir="./knowledge"):
17 + def __init__(self, logger: Log, embeddings_model, in_memory=False, memory_dir="./memory", knowledge_dir="./knowledge"):
18 + self.logger = logger
19 +
20 print("Initializing VectorDB...")
19 - Log.log("info", content="Initializing VectorDB...")
21 + self.logger.log("info", content="Initializing VectorDB...")
22
23 self.embeddings_model = embeddings_model
24
@@ -76,7 +78,7 @@ class VectorDB:
78 with open(index_path, 'r') as f:
79 index = json.load(f)
80
79 - index = knowledge_import.load_knowledge(kn_dir,index)
81 + index = knowledge_import.load_knowledge(self.logger,kn_dir,index)
82
83 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
python/tools/call_subordinate.py
+3 -5
@@ -1,15 +1,13 @@
1 from agent import Agent
2 from python.helpers.tool import Tool, Response
3 -from python.helpers import files
4 -from python.helpers.print_style import PrintStyle
3
4 class Delegation(Tool):
5
8 - def execute(self, message="", reset="", **kwargs):
6 + async def execute(self, message="", reset="", **kwargs):
7 # create subordinate agent using the data object on this agent and set superior agent to his data object
8 if self.agent.get_data("subordinate") is None or str(reset).lower().strip() == "true":
11 - subordinate = Agent(self.agent.number+1, self.agent.config)
9 + subordinate = Agent(self.agent.number+1, self.agent.config, self.agent.context)
10 subordinate.set_data("superior", self.agent)
11 self.agent.set_data("subordinate", subordinate)
12 # run subordinate agent message loop
15 - return Response( message=self.agent.get_data("subordinate").message_loop(message), break_loop=False)
\ No newline at end of file
13 + return Response( message= await self.agent.get_data("subordinate").message_loop(message), break_loop=False)
\ No newline at end of file
python/tools/code_execution_tool.py
+31 -35
@@ -1,17 +1,13 @@
1 +import asyncio
2 from dataclasses import dataclass
2 -import os, json, contextlib, subprocess, ast, shlex
3 -from io import StringIO
3 +import shlex
4 import time
5 -from typing import Literal
6 -from python.helpers import files, messages
7 -from agent import Agent
5 from python.helpers.tool import Tool, Response
6 from python.helpers import files
7 from python.helpers.print_style import PrintStyle
8 from python.helpers.shell_local import LocalInteractiveSession
9 from python.helpers.shell_ssh import SSHInteractiveSession
10 from python.helpers.docker import DockerContainerManager
14 -from python.helpers.log import Log
11
12 @dataclass
13 class State:
@@ -21,91 +17,91 @@ class State:
17
18 class CodeExecution(Tool):
19
24 - def execute(self,**kwargs):
20 + async def execute(self,**kwargs):
21
26 - if self.agent.handle_intervention(): return Response(message="", break_loop=False) # wait for intervention and handle it, if paused
22 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
23
28 - self.prepare_state()
24 + await self.prepare_state()
25
26 # os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir
27
28 runtime = self.args["runtime"].lower().strip()
29 if runtime == "python":
34 - response = self.execute_python_code(self.args["code"])
30 + response = await self.execute_python_code(self.args["code"])
31 elif runtime == "nodejs":
36 - response = self.execute_nodejs_code(self.args["code"])
32 + response = await self.execute_nodejs_code(self.args["code"])
33 elif runtime == "terminal":
38 - response = self.execute_terminal_command(self.args["code"])
34 + response = await self.execute_terminal_command(self.args["code"])
35 elif runtime == "output":
40 - response = self.get_terminal_output()
36 + response = await self.get_terminal_output()
37 else:
38 response = self.agent.read_prompt("fw.code_runtime_wrong.md", runtime=runtime)
39
40 if not response: response = self.agent.read_prompt("fw.code_no_output.md")
41 return Response(message=response, break_loop=False)
42
47 - def before_execution(self, **kwargs):
48 - if self.agent.handle_intervention(): return # wait for intervention and handle it, if paused
43 + async def before_execution(self, **kwargs):
44 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
45 PrintStyle(font_color="#1B4F72", padding=True, background_color="white", bold=True).print(f"{self.agent.agent_name}: Using tool '{self.name}':")
50 - self.log = Log(type="code_exe", heading=f"{self.agent.agent_name}: Using tool '{self.name}':", content="", kvps=self.args)
46 + self.log = self.agent.context.log.log(type="code_exe", heading=f"{self.agent.agent_name}: Using tool '{self.name}':", content="", kvps=self.args)
47 if self.args and isinstance(self.args, dict):
48 for key, value in self.args.items():
49 PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ")
50 PrintStyle(font_color="#85C1E9", padding=isinstance(value,str) and "\n" in value).stream(value)
51 PrintStyle().print()
52
57 - def after_execution(self, response, **kwargs):
53 + async def after_execution(self, response, **kwargs):
54 msg_response = self.agent.read_prompt("fw.tool_response.md", tool_name=self.name, tool_response=response.message)
59 - self.agent.append_message(msg_response, human=True)
55 + await self.agent.append_message(msg_response, human=True)
56
61 - def prepare_state(self):
57 + async def prepare_state(self):
58 self.state = self.agent.get_data("cot_state")
59 if not self.state:
60
61 #initialize docker container if execution in docker is configured
62 if self.agent.config.code_exec_docker_enabled:
67 - docker = DockerContainerManager(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)
63 + 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)
64 docker.start_container()
65 else: docker = None
66
67 #initialize local or remote interactive shell insterface
68 if self.agent.config.code_exec_ssh_enabled:
73 - shell = SSHInteractiveSession(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)
69 + 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)
70 else: shell = LocalInteractiveSession()
71
72 self.state = State(shell=shell,docker=docker)
77 - shell.connect()
73 + await shell.connect()
74 self.agent.set_data("cot_state", self.state)
75
80 - def execute_python_code(self, code):
76 + async def execute_python_code(self, code):
77 escaped_code = shlex.quote(code)
78 command = f'python3 -c {escaped_code}'
83 - return self.terminal_session(command)
79 + return await self.terminal_session(command)
80
85 - def execute_nodejs_code(self, code):
81 + async def execute_nodejs_code(self, code):
82 escaped_code = shlex.quote(code)
83 command = f'node -e {escaped_code}'
88 - return self.terminal_session(command)
84 + return await self.terminal_session(command)
85
90 - def execute_terminal_command(self, command):
91 - return self.terminal_session(command)
86 + async def execute_terminal_command(self, command):
87 + return await self.terminal_session(command)
88
93 - def terminal_session(self, command):
89 + async def terminal_session(self, command):
90
95 - if self.agent.handle_intervention(): return "" # wait for intervention and handle it, if paused
91 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
92
93 self.state.shell.send_command(command)
94
95 PrintStyle(background_color="white",font_color="#1B4F72",bold=True).print(f"{self.agent.agent_name} code execution output:")
100 - return self.get_terminal_output()
96 + return await self.get_terminal_output()
97
102 - def get_terminal_output(self):
98 + async def get_terminal_output(self):
99 idle=0
100 while True:
105 - time.sleep(0.1) # Wait for some output to be generated
106 - full_output, partial_output = self.state.shell.read_output()
101 + await asyncio.sleep(0.1) # Wait for some output to be generated
102 + full_output, partial_output = await self.state.shell.read_output()
103
108 - if self.agent.handle_intervention(): return full_output # wait for intervention and handle it, if paused
104 + await self.agent.handle_intervention() # wait for intervention and handle it, if paused
105
106 if partial_output:
107 PrintStyle(font_color="#85C1E9").stream(partial_output)
python/tools/knowledge_tool.py
+6 -7
@@ -1,18 +1,14 @@
1 import os
2 -from agent import Agent
2 from python.helpers import perplexity_search
3 from python.helpers import duckduckgo_search
5 -
4 from . import memory_tool
5 import concurrent.futures
8 -
6 from python.helpers.tool import Tool, Response
10 -from python.helpers import files
7 from python.helpers.print_style import PrintStyle
12 -from python.helpers.log import Log
8 +from python.helpers.errors import handle_error
9
10 class Knowledge(Tool):
15 - def execute(self, question="", **kwargs):
11 + async def execute(self, question="", **kwargs):
12 with concurrent.futures.ThreadPoolExecutor() as executor:
13 # Schedule the two functions to be run in parallel
14
@@ -21,7 +17,7 @@ class Knowledge(Tool):
17 perplexity = executor.submit(perplexity_search.perplexity_search, question)
18 else:
19 PrintStyle.hint("No API key provided for Perplexity. Skipping Perplexity search.")
24 - Log(type="hint", content="No API key provided for Perplexity. Skipping Perplexity search.")
20 + self.agent.context.log.log(type="hint", content="No API key provided for Perplexity. Skipping Perplexity search.")
21 perplexity = None
22
23
@@ -35,16 +31,19 @@ class Knowledge(Tool):
31 try:
32 perplexity_result = (perplexity.result() if perplexity else "") or ""
33 except Exception as e:
34 + handle_error(e)
35 perplexity_result = "Perplexity search failed: " + str(e)
36
37 try:
38 duckduckgo_result = duckduckgo.result()
39 except Exception as e:
40 + handle_error(e)
41 duckduckgo_result = "DuckDuckGo search failed: " + str(e)
42
43 try:
44 memory_result = future_memory.result()
45 except Exception as e:
46 + handle_error(e)
47 memory_result = "Memory search failed: " + str(e)
48
49 msg = self.agent.read_prompt("tool.knowledge.response.md",
python/tools/memory_tool.py
+6 -6
@@ -1,17 +1,16 @@
1 import re
2 from agent import Agent
3 from python.helpers.vector_db import VectorDB, Document
4 -from python.helpers import files
5 -import os, json
4 +import os
5 from python.helpers.tool import Tool, Response
6 from python.helpers.print_style import PrintStyle
8 -from python.helpers.log import Log
7 +from python.helpers.errors import handle_error
8
9 # databases based on subdirectories from agent config
10 dbs = {}
11
12 class Memory(Tool):
14 - def execute(self,**kwargs):
13 + async def execute(self,**kwargs):
14 result=""
15
16 try:
@@ -26,9 +25,10 @@ class Memory(Tool):
25 elif "delete" in kwargs:
26 result = delete(self.agent, kwargs["delete"])
27 except Exception as e:
28 + handle_error(e)
29 # hint about embedding change with existing database
30 PrintStyle.hint("If you changed your embedding model, you will need to remove contents of /memory directory.")
31 - Log(type="hint", content="If you changed your embedding model, you will need to remove contents of /memory directory.")
31 + self.agent.context.log.log(type="hint", content="If you changed your embedding model, you will need to remove contents of /memory directory.")
32 raise
33
34 # result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
@@ -63,7 +63,7 @@ def get_db(agent: Agent):
63 key = (mem_dir, kn_dir)
64
65 if key not in dbs:
66 - db = VectorDB(embeddings_model=agent.config.embeddings_model, in_memory=False, memory_dir=mem_dir, knowledge_dir=kn_dir)
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]
python/tools/response.py
+4 -12
@@ -1,22 +1,14 @@
1 -from agent import Agent
2 -from python.helpers import files
3 -from python.helpers.print_style import PrintStyle
4 -
5 -from agent import Agent
1 from python.helpers.tool import Tool, Response
7 -from python.helpers import files
8 -from python.helpers.print_style import PrintStyle
9 -from python.helpers.log import Log
2
3 class ResponseTool(Tool):
4
13 - def execute(self,**kwargs):
5 + async def execute(self,**kwargs):
6 self.agent.set_data("timeout", self.agent.config.response_timeout_seconds)
7 return Response(message=self.args["text"], break_loop=True)
8
17 - def before_execution(self, **kwargs):
18 - self.log = Log(type="response", heading=f"{self.agent.agent_name}: Responding:", content=self.args.get("text", ""))
9 + async def before_execution(self, **kwargs):
10 + self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding:", content=self.args.get("text", ""))
11
12
21 - def after_execution(self, response, **kwargs):
13 + async def after_execution(self, response, **kwargs):
14 pass # do not add anything to the history or output
\ No newline at end of file
python/tools/task_done.py
+4 -12
@@ -1,21 +1,13 @@
1 -from agent import Agent
2 -from python.helpers import files
3 -from python.helpers.print_style import PrintStyle
4 -
5 -from agent import Agent
1 from python.helpers.tool import Tool, Response
7 -from python.helpers import files
8 -from python.helpers.print_style import PrintStyle
9 -from python.helpers.log import Log
2
3 class TaskDone(Tool):
4
13 - def execute(self,**kwargs):
5 + async def execute(self,**kwargs):
6 self.agent.set_data("timeout", 0)
7 return Response(message=self.args["text"], break_loop=True)
8
17 - def before_execution(self, **kwargs):
18 - self.log = Log(type="response", heading=f"{self.agent.agent_name}: Task done:", content=self.args.get("text", ""))
9 + async def before_execution(self, **kwargs):
10 + self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Task done:", content=self.args.get("text", ""))
11
20 - def after_execution(self, response, **kwargs):
12 + async def after_execution(self, response, **kwargs):
13 pass # do add anything to the history or output
\ No newline at end of file
python/tools/unknown.py
+1 -2
@@ -1,8 +1,7 @@
1 from python.helpers.tool import Tool, Response
2 -from python.helpers import files
2
3 class Unknown(Tool):
5 - def execute(self, **kwargs):
4 + async def execute(self, **kwargs):
5 return Response(
6 message=self.agent.read_prompt("fw.tool_not_found.md",
7 tool_name=self.name,
python/tools/webpage_content_tool.py
+4 -1
@@ -3,9 +3,11 @@ from bs4 import BeautifulSoup
3 from urllib.parse import urlparse
4 from newspaper import Article
5 from python.helpers.tool import Tool, Response
6 +from python.helpers.errors import handle_error
7 +
8
9 class WebpageContentTool(Tool):
8 - def execute(self, url="", **kwargs):
10 + async def execute(self, url="", **kwargs):
11 if not url:
12 return Response(message="Error: No URL provided.", break_loop=False)
13
@@ -36,4 +38,5 @@ class WebpageContentTool(Tool):
38 except requests.RequestException as e:
39 return Response(message=f"Error fetching webpage: {str(e)}", break_loop=False)
40 except Exception as e:
41 + handle_error(e)
42 return Response(message=f"An error occurred: {str(e)}", break_loop=False)
\ No newline at end of file
run_cli.py
+17 -14
@@ -1,7 +1,8 @@
1 +import asyncio
2 import threading, time, models, os
3 from ansio import application_keypad, mouse_input, raw_input
4 from ansio.input import InputEvent, get_input_event
4 -from agent import Agent, AgentConfig
5 +from agent import AgentContext
6 from python.helpers.print_style import PrintStyle
7 from python.helpers.files import read_file
8 from python.helpers import files
@@ -9,17 +10,18 @@ import python.helpers.timed_input as timed_input
10 from initialize import initialize
11
12
13 +context: AgentContext = None # type: ignore
14 input_lock = threading.Lock()
13 -os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir
15 +
16
17 # Main conversation loop
16 -def chat(agent:Agent):
18 +async def chat(context: AgentContext):
19
20 # start the conversation loop
21 while True:
22 # ask user for message
23 with input_lock:
22 - timeout = agent.get_data("timeout") # how long the agent is willing to wait
24 + timeout = context.agent0.get_data("timeout") # how long the agent is willing to wait
25 if not timeout: # if agent wants to wait for user input forever
26 PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ('e' to leave):")
27 import readline # this fixes arrow keys in terminal
@@ -33,7 +35,7 @@ def chat(agent:Agent):
35 user_input = timeout_input("> ", timeout=timeout)
36
37 if not user_input:
36 - user_input = agent.read_prompt("fw.msg_timeout.md")
38 + user_input = context.agent0.read_prompt("fw.msg_timeout.md")
39 PrintStyle(font_color="white", padding=False).stream(f"{user_input}")
40 else:
41 user_input = user_input.strip()
@@ -47,17 +49,17 @@ def chat(agent:Agent):
49 if user_input.lower() == 'e': break
50
51 # send message to agent0,
50 - assistant_response = agent.message_loop(user_input)
52 + assistant_response = await context.communicate(user_input).result()
53
54 # print agent0 response
53 - PrintStyle(font_color="white",background_color="#1D8348", bold=True, padding=True).print(f"{agent.agent_name}: reponse:")
55 + PrintStyle(font_color="white",background_color="#1D8348", bold=True, padding=True).print(f"{context.agent0.agent_name}: reponse:")
56 PrintStyle(font_color="white").print(f"{assistant_response}")
57
58
59 # User intervention during agent streaming
60 def intervention():
59 - if Agent.streaming_agent and not Agent.paused:
60 - Agent.paused = True # stop agent streaming
61 + if context.streaming_agent and not context.paused:
62 + context.paused = True # stop agent streaming
63 PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User intervention ('e' to leave, empty to continue):")
64
65 import readline # this fixes arrow keys in terminal
@@ -65,8 +67,8 @@ def intervention():
67 PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
68
69 if user_input.lower() == 'e': os._exit(0) # exit the conversation when the user types 'exit'
68 - if user_input: Agent.streaming_agent.intervention_message = user_input # set intervention message if non-empty
69 - Agent.paused = False # continue agent streaming
70 + if user_input: context.streaming_agent.intervention_message = user_input # set intervention message if non-empty
71 + context.paused = False # continue agent streaming
72
73
74 # Capture keyboard input to trigger user intervention
@@ -78,7 +80,7 @@ def capture_keys():
80 intervent = False
81 time.sleep(0.1)
82
81 - if Agent.streaming_agent:
83 + if context.streaming_agent:
84 # with raw_input, application_keypad, mouse_input:
85 with input_lock, raw_input, application_keypad:
86 event: InputEvent | None = get_input_event(timeout=0.1)
@@ -97,5 +99,6 @@ if __name__ == "__main__":
99 threading.Thread(target=capture_keys, daemon=True).start()
100
101 # initialize and start the chat
100 - agent0 = initialize()
101 - chat(agent0)
\ No newline at end of file
102 + config = initialize()
103 + context = AgentContext(config)
104 + asyncio.run(chat(context))
\ No newline at end of file
run_ui.py
+100 -70
@@ -1,18 +1,18 @@
1 +import asyncio
2 from functools import wraps
3 import os
4 from pathlib import Path
5 import threading
6 +import uuid
7 from flask import Flask, request, jsonify, Response
8 from flask_basicauth import BasicAuth
7 -from agent import Agent
9 +from agent import AgentContext
10 from initialize import initialize
11 from python.helpers.files import get_abs_path
12 from python.helpers.print_style import PrintStyle
13 from python.helpers.log import Log
14 from dotenv import load_dotenv
15
14 -#global agent instance
15 -agent0: Agent|None = None
16
17 #initialize the internal Flask server
18 app = Flask("app",static_folder=get_abs_path("./webui"),static_url_path="/")
@@ -22,12 +22,10 @@ app.config['BASIC_AUTH_USERNAME'] = os.environ.get('BASIC_AUTH_USERNAME') or "ad
22 app.config['BASIC_AUTH_PASSWORD'] = os.environ.get('BASIC_AUTH_PASSWORD') or "admin" #default pass
23 basic_auth = BasicAuth(app)
24
25 -# get global agent
26 -def get_agent(reset: bool = False) -> Agent:
27 - global agent0
28 - if agent0 is None or reset:
29 - agent0 = initialize()
30 - return agent0
25 +# get context to run agent zero in
26 +def get_context(ctxid:str):
27 + if not ctxid: return AgentContext.first() or AgentContext(config=initialize())
28 + return AgentContext.get(ctxid) or AgentContext(config=initialize(),id=ctxid)
29
30 # Now you can use @requires_auth function decorator to require login on certain pages
31 def requires_auth(f):
@@ -60,64 +58,46 @@ async def health_check():
58
59 # send message to agent (async UI)
60 @app.route('/msg', methods=['POST'])
63 -async def handle_message():
64 - try:
65 -
66 - #agent instance
67 - agent = get_agent()
68 -
69 - #data sent to the server
70 - input = request.get_json()
71 - text = input.get("text", "")
72 -
73 - # print to console and log
74 - PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message:")
75 - PrintStyle(font_color="white", padding=False).print(f"> {text}")
76 - Log.log(type="user", heading="User message", content=text)
77 -
78 - #pass the message to the agent
79 - threading.Thread(target=agent.communicate, args=(text,)).start()
80 -
81 - #data from this server
82 - response = {
83 - "ok": True,
84 - "message": "Message received.",
85 - }
86 -
87 - except Exception as e:
88 - response = {
89 - "ok": False,
90 - "message": str(e),
91 - }
92 -
93 - #respond with json
94 - return jsonify(response)
61 +async def handle_message_async():
62 + return await handle_message(False)
63
64 # send message to agent (synchronous API)
65 @app.route('/msg_sync', methods=['POST'])
66 async def handle_msg_sync():
99 - try:
100 -
101 - #agent instance
102 - agent = get_agent()
67 + return await handle_message(True)
68
69 +async def handle_message(sync:bool):
70 + try:
71 +
72 #data sent to the server
73 input = request.get_json()
74 text = input.get("text", "")
75 + ctxid = input.get("context", "")
76 + blev = input.get("broadcast", 1)
77 +
78 + #context instance - get or create
79 + context = get_context(ctxid)
80
81 # print to console and log
82 PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message:")
83 PrintStyle(font_color="white", padding=False).print(f"> {text}")
111 - Log.log(type="user", heading="User message", content=text)
112 -
113 - #pass the message to the agent
114 - response = agent.communicate(text)
115 -
116 - #data from this server
117 - response = {
118 - "ok": True,
119 - "message": response,
120 - }
84 + context.log.log(type="user", heading="User message", content=text)
85 +
86 + if sync:
87 + context.communicate(text)
88 + result = await context.process.result() #type: ignore
89 + response = {
90 + "ok": True,
91 + "message": result,
92 + }
93 + else:
94 +
95 + print("\n\n",(context.process and context.process.is_alive()))
96 + context.communicate(text)
97 + response = {
98 + "ok": True,
99 + "message": "Message received.",
100 + }
101
102 except Exception as e:
103 response = {
@@ -127,7 +107,7 @@ async def handle_msg_sync():
107
108 #respond with json
109 return jsonify(response)
130 -
110 +
111 # pausing/unpausing the agent
112 @app.route('/pause', methods=['POST'])
113 async def pause():
@@ -136,8 +116,12 @@ async def pause():
116 #data sent to the server
117 input = request.get_json()
118 paused = input.get("paused", False)
119 + ctxid = input.get("context", "")
120 +
121 + #context instance - get or create
122 + context = get_context(ctxid)
123
140 - Agent.paused = paused
124 + context.paused = paused
125
126 response = {
127 "ok": True,
@@ -158,10 +142,15 @@ async def pause():
142 @app.route('/reset', methods=['POST'])
143 async def reset():
144 try:
161 -
162 - agent = get_agent(reset=True)
163 - Log.reset()
145
146 + #data sent to the server
147 + input = request.get_json()
148 + ctxid = input.get("context", "")
149 +
150 + #context instance - get or create
151 + context = get_context(ctxid)
152 + context.reset()
153 +
154 response = {
155 "ok": True,
156 "message": "Agent restarted.",
@@ -176,6 +165,32 @@ async def reset():
165 #respond with json
166 return jsonify(response)
167
168 +# killing context
169 +@app.route('/remove', methods=['POST'])
170 +async def remove():
171 + try:
172 +
173 + #data sent to the server
174 + input = request.get_json()
175 + ctxid = input.get("context", "")
176 +
177 + #context instance - get or create
178 + AgentContext.remove(ctxid)
179 +
180 + response = {
181 + "ok": True,
182 + "message": "Context removed.",
183 + }
184 +
185 + except Exception as e:
186 + response = {
187 + "ok": False,
188 + "message": str(e),
189 + }
190 +
191 + #respond with json
192 + return jsonify(response)
193 +
194 # Web UI polling
195 @app.route('/poll', methods=['POST'])
196 async def poll():
@@ -183,19 +198,36 @@ async def poll():
198
199 #data sent to the server
200 input = request.get_json()
186 - from_no = input.get("log_from", "")
187 -
188 - logs = Log.logs[int(from_no):]
189 - to = Log.last_updated #max(0, len(Log.logs)-1)
201 + ctxid = input.get("context", uuid.uuid4())
202 + from_no = input.get("log_from", 0)
203 +
204 + #context instance - get or create
205 + context = get_context(ctxid)
206 +
207 +
208 + logs = context.log.output(start=from_no)
209 +
210 + # loop AgentContext._contexts
211 + ctxs = []
212 + for ctx in AgentContext._contexts.values():
213 + ctxs.append({
214 + "id": ctx.id,
215 + "no": ctx.no,
216 + "log_guid": ctx.log.guid,
217 + "log_version": len(ctx.log.updates),
218 + "log_length": len(ctx.log.logs),
219 + "paused": ctx.paused
220 + })
221
222 #data from this server
223 response = {
224 "ok": True,
225 + "context": context.id,
226 + "contexts": ctxs,
227 "logs": logs,
195 - "log_to": to,
196 - "log_guid": Log.guid,
197 - "log_version": Log.version,
198 - "paused": Agent.paused
228 + "log_guid": context.log.guid,
229 + "log_version": len(context.log.updates),
230 + "paused": context.paused
231 }
232
233 except Exception as e:
@@ -214,8 +246,6 @@ if __name__ == "__main__":
246
247 load_dotenv()
248
217 - get_agent() #initialize
218 -
249 # Suppress only request logs but keep the startup messages
250 from werkzeug.serving import WSGIRequestHandler
251 class NoRequestLoggingWSGIRequestHandler(WSGIRequestHandler):
test.py
+19 -9
@@ -1,12 +1,22 @@
1 -from python.helpers.dirty_json import DirtyJson
1 +from python.helpers.strings import calculate_valid_match_lengths
2
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'
5
4 -json_string = """
5 -{"key1": "value1",
6 - "key2": "value2",
7 - "key3": "value3"
8 -}
9 -"""
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[?'
8
11 -json = DirtyJson.parse_string(json_string)
12 -print(json)
\ No newline at end of file
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)
17 +
18 +if(trim_com > 0 and trim_out > 0):
19 + sec_tr = second[:trim_out]
20 +else: sec_tr = "original"
21 +
22 +print(sec_tr)
\ No newline at end of file
webui/index.css
+14
@@ -209,6 +209,16 @@ h4 {
209 transform: scale(0.95);
210 }
211
212 +.chat-list-button {
213 + cursor: pointer;
214 + color: inherit; /* Keep the text color the same as the surrounding text */
215 + text-decoration: none; /* Remove underline by default */
216 +}
217 +
218 +.chat-list-button:hover {
219 + text-decoration: underline; /* Add underline on hover */
220 +}
221 +
222 #send-button {
223 background-color: #bb86fc;
224 }
@@ -323,4 +333,8 @@ color: #6ec583;
333
334 .disconnected{
335 color: #d87979;
336 +}
337 +
338 +.font-bold{
339 + font-weight: bold;
340 }
\ No newline at end of file
webui/index.html
+27 -24
@@ -8,7 +8,7 @@
8 <link rel="stylesheet" href="index.css">
9
10 <script>
11 - window.safeCall = function(name,...args){
11 + window.safeCall = function (name, ...args) {
12 if (window[name]) window[name](...args)
13 }
14 </script>
@@ -21,7 +21,7 @@
21 <div class="container">
22 <div id="left-panel" class="panel">
23 <!-- <h2>Configuration</h2> -->
24 -
24 +
25 <div class="config-section" id="status-section" x-data="{ connected: true }">
26 <h3>Status</h3>
27 <h4 class="connected" x-show="connected">&#10004; Connected</h4>
@@ -30,34 +30,34 @@
30
31 <div class="config-section" x-data="{ showQuickActions: true }">
32 <h3>Quick Actions</h3>
33 - <button class="config-button" id="resetChat" @click="resetChat()">New Chat</button>
33 + <button class="config-button" id="resetChat" @click="resetChat()">Reset chat</button>
34 + <button class="config-button" id="newChat" @click="newChat()">New Chat</button>
35 </div>
35 - <!--
36 - <div class="config-section">
37 - <h3>Model Settings</h3>
36 +
37 + <div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }" x-show="contexts.length > 0">
38 + <h3>Chats</h3>
39 <ul class="config-list">
39 - <li>
40 - <span>GPT-4</span>
41 - <button class="edit-button">Edit</button>
42 - </li>
43 - <li>
44 - <span>Temperature: 0.7</span>
45 - <button class="edit-button">Edit</button>
46 - </li>
47 - <li>
48 - <span>Max Tokens: 2048</span>
49 - <button class="edit-button">Edit</button>
50 - </li>
40 + <template x-for="context in contexts">
41 + <li>
42 + <span
43 + :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
44 + @click="selected = context.id; selectChat(context.id)">
45 + Chat #<span x-text="context.no"></span>
46 + </span>
47 + <button class="edit-button" @click="killChat(context.id)">X</button>
48 + </li>
49 + </template>
50 </ul>
51 </div>
53 - -->
52 +
53 <div class="config-section">
54 <h3>Preferences</h3>
55 <ul class="config-list">
56 <li x-data="{ autoScroll: true }">
57 <span>Autoscroll</span>
58 <label class="switch">
60 - <input type="checkbox" x-model="autoScroll" x-effect="window.safeCall('toggleAutoScroll',autoScroll)">
59 + <input type="checkbox" x-model="autoScroll"
60 + x-effect="window.safeCall('toggleAutoScroll',autoScroll)">
61 <span class="slider"></span>
62 </label>
63 </li>
@@ -65,11 +65,12 @@
65 <li x-data="{ showThoughts: true }">
66 <span>Show thoughts</span>
67 <label class="switch">
68 - <input type="checkbox" x-model="showThoughts" x-effect="window.safeCall('toggleThoughts',showThoughts)">
68 + <input type="checkbox" x-model="showThoughts"
69 + x-effect="window.safeCall('toggleThoughts',showThoughts)">
70 <span class="slider"></span>
71 </label>
72 </li>
72 -
73 +
74 <li x-data="{ showJson: false }">
75 <span>Show JSON</span>
76 <label class="switch">
@@ -90,8 +91,10 @@
91 <div id="input-section" x-data="{ paused: false }">
92 <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
93 <button class="chat-button" id="send-button">&#10148;</button>
93 - <button class="chat-button pause-button" id="pause-button" @click="pauseAgent(true)" x-show="!paused">&#10074;&#10074;</button>
94 - <button class="chat-button pause-button" id="unpause-button" @click="pauseAgent(false)" x-show="paused">&#9654;</button>
94 + <button class="chat-button pause-button" id="pause-button" @click="pauseAgent(true)"
95 + x-show="!paused">&#10074;&#10074;</button>
96 + <button class="chat-button pause-button" id="unpause-button" @click="pauseAgent(false)"
97 + x-show="paused">&#9654;</button>
98 </div>
99 </div>
100 </div>
webui/index.js
+95 -39
@@ -8,10 +8,13 @@ const chatHistory = document.getElementById('chat-history');
8 const sendButton = document.getElementById('send-button');
9 const inputSection = document.getElementById('input-section');
10 const statusSection = document.getElementById('status-section');
11 +const chatsSection = document.getElementById('chats-section');
12
13 let isResizing = false;
14 let autoScroll = true;
15
16 +let context = "";
17 +
18
19 splitter.addEventListener('mousedown', (e) => {
20 isResizing = true;
@@ -35,7 +38,7 @@ async function sendMessage() {
38 const message = chatInput.value.trim();
39 if (message) {
40
38 - const response = await sendJsonData("/msg", { text: message });
41 + const response = await sendJsonData("/msg", { text: message, context });
42
43 //setMessage('user', message);
44 chatInput.value = '';
@@ -75,7 +78,7 @@ function setMessage(id, type, heading, content, kvps = null) {
78 chatHistory.appendChild(messageContainer);
79 }
80
78 - if(autoScroll) chatHistory.scrollTop = chatHistory.scrollHeight;
81 + if (autoScroll) chatHistory.scrollTop = chatHistory.scrollHeight;
82 }
83
84
@@ -85,53 +88,66 @@ function adjustTextareaHeight() {
88 }
89
90 async function sendJsonData(url, data) {
88 - const response = await fetch(url, {
89 - method: 'POST',
90 - headers: {
91 - 'Content-Type': 'application/json'
92 - },
93 - body: JSON.stringify(data)
94 - });
95 -
96 - if (!response.ok) {
97 - throw new Error('Network response was not ok');
98 - }
91 + const response = await fetch(url, {
92 + method: 'POST',
93 + headers: {
94 + 'Content-Type': 'application/json'
95 + },
96 + body: JSON.stringify(data)
97 + });
98 +
99 + if (!response.ok) {
100 + throw new Error('Network response was not ok');
101 + }
102 +
103 + const jsonResponse = await response.json();
104 + return jsonResponse;
105 +}
106
100 - const jsonResponse = await response.json();
101 - return jsonResponse;
107 +function generateGUID() {
108 + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
109 + var r = Math.random() * 16 | 0;
110 + var v = c === 'x' ? r : (r & 0x3 | 0x8);
111 + return v.toString(16);
112 + });
113 }
114
104 -let lastLog = 0;
115 let lastLogVersion = 0;
116 let lastLogGuid = ""
117
118 async function poll() {
109 - try{
110 - const response = await sendJsonData("/poll", { log_from: lastLog });
111 - // console.log(response)
119 + try {
120 + const response = await sendJsonData("/poll", { log_from: lastLogVersion, context });
121 + // console.log(response)
122
113 - if (response.ok) {
123 + if (response.ok) {
124
115 - if (lastLogGuid != response.log_guid) {
116 - chatHistory.innerHTML = ""
117 - }
125 + setContext(response.context)
126
119 - if (lastLogVersion != response.log_version) {
120 - for (const log of response.logs) {
121 - setMessage(log.no, log.type, log.heading, log.content, log.kvps);
127 + if (lastLogGuid != response.log_guid) {
128 + chatHistory.innerHTML = ""
129 + lastLogVersion = 0
130 }
123 - }
131
125 - //set ui model vars from backend
126 - const inputAD = Alpine.$data(inputSection);
127 - inputAD.paused = response.paused;
128 - const statusAD = Alpine.$data(statusSection);
129 - statusAD.connected = response.ok;
132 + if (lastLogVersion != response.log_version) {
133 + for (const log of response.logs) {
134 + setMessage(log.no, log.type, log.heading, log.content, log.kvps);
135 + }
136 + }
137
131 - lastLog = response.log_to;
132 - lastLogVersion = response.log_version;
133 - lastLogGuid = response.log_guid;
134 - }
138 + //set ui model vars from backend
139 + const inputAD = Alpine.$data(inputSection);
140 + inputAD.paused = response.paused;
141 + const statusAD = Alpine.$data(statusSection);
142 + statusAD.connected = response.ok;
143 + const chatsAD = Alpine.$data(chatsSection);
144 + chatsAD.contexts = response.contexts;
145 +
146 + lastLogVersion = response.log_version;
147 + lastLogGuid = response.log_guid;
148 +
149 +
150 + }
151
152 } catch (error) {
153 console.error('Error:', error);
@@ -141,13 +157,53 @@ async function poll() {
157 }
158
159 window.pauseAgent = async function (paused) {
144 - const resp = await sendJsonData("/pause", { paused: paused });
160 + const resp = await sendJsonData("/pause", { paused: paused, context });
161 }
162
163 window.resetChat = async function () {
148 - const resp = await sendJsonData("/reset", {});
164 + const resp = await sendJsonData("/reset", { context });
165 }
166
167 +window.newChat = async function () {
168 + setContext(generateGUID());
169 +}
170 +
171 +window.killChat = async function (id) {
172 +
173 +
174 + const chatsAD = Alpine.$data(chatsSection);
175 + let found, other
176 + for (let i = 0; i < chatsAD.contexts.length; i++) {
177 + if (chatsAD.contexts[i].id == id) {
178 + found = true
179 + } else {
180 + other = chatsAD.contexts[i]
181 + }
182 + if (found && other) break
183 + }
184 +
185 + if (context == id && found) {
186 + if (other) setContext(other.id)
187 + else setContext(generateGUID())
188 + }
189 +
190 + if (found) sendJsonData("/remove", { context: id });
191 +}
192 +
193 +window.selectChat = async function (id) {
194 + setContext(id)
195 +}
196 +
197 +const setContext = function (id) {
198 + if (id == context) return
199 + context = id
200 + lastLogGuid = ""
201 + lastLogVersion = 0
202 + const chatsAD = Alpine.$data(chatsSection);
203 + chatsAD.selected = id
204 +}
205 +
206 +
207 window.toggleAutoScroll = async function (_autoScroll) {
208 autoScroll = _autoScroll;
209 }
@@ -176,7 +232,7 @@ function toggleCssProperty(selector, property, value) {
232 const rule = rules[j];
233 if (rule.selectorText == selector) {
234 // Check if the property is already applied
179 - if (value===undefined) {
235 + if (value === undefined) {
236 rule.style.removeProperty(property); // Remove the property
237 } else {
238 rule.style.setProperty(property, value); // Add the property (you can customize the value)