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">✔ 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">➤</button>
93
- <button class="chat-button pause-button" id="pause-button" @click="pauseAgent(true)" x-show="!paused">❚❚</button>
94
- <button class="chat-button pause-button" id="unpause-button" @click="pauseAgent(false)" x-show="paused">▶</button>
94
+ <button class="chat-button pause-button" id="pause-button" @click="pauseAgent(true)"
95
+ x-show="!paused">❚❚</button>
96
+ <button class="chat-button pause-button" id="unpause-button" @click="pauseAgent(false)"
97
+ x-show="paused">▶</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)