Updates for v0.6
Main pack of updates: - web ui - knowledge import - FAISS DB - prompt subfolders
frdel committed
Aug 31, 2024 at 22:38 UTC
9c258b77a2222c50093b4f0b7ff95aefcd1b3c1c
59 files changed
+1759
-393
.gitignore
+11
-1
@@ -20,4 +20,14 @@ memory/*
20
# Ignore all contents of the directory "logs"
21
logs/*
22
# But do not ignore the directory itself
23
-!logs/.gitkeep
\ No newline at end of file
23
+!logs/.gitkeep
24
+
25
+# Ignore all contents of the directory "tmp"
26
+tmp/*
27
+# But do not ignore the directory itself
28
+!tmp/.gitkeep
29
+
30
+# Ignore all contents of the directory "knowledge"
31
+knowledge/*
32
+# But do not ignore the directory itself
33
+!knowledge/.gitkeep
\ No newline at end of file
.vscode/launch.json
+10
-2
@@ -2,10 +2,18 @@
2
"version": "0.2.0",
3
"configurations": [
4
{
5
- "name": "Debug main.py",
5
+ "name": "Debug run_ui.py",
6
"type": "debugpy",
7
"request": "launch",
8
- "program": "./main.py",
8
+ "program": "./run_ui.py",
9
+ "console": "integratedTerminal",
10
+ "args": ["-Xfrozen_modules=off"]
11
+ },
12
+ {
13
+ "name": "Debug run_cli.py",
14
+ "type": "debugpy",
15
+ "request": "launch",
16
+ "program": "./run_cli.py",
17
"console": "integratedTerminal",
18
"args": ["-Xfrozen_modules=off"]
19
},
README.md
+9
-9
@@ -1,6 +1,6 @@
1
# Agent Zero
2
3
-[](https://discord.gg/B8KZKNsPpj) [](https://www.youtube.com/@AgentZeroFW) [](https://www.linkedin.com/in/jan-tomasek/) [](https://x.com/JanTomasekDev)
3
+[](https://www.skool.com/agent-zero) [](https://discord.gg/B8KZKNsPpj) [](https://www.youtube.com/@AgentZeroFW) [](https://www.linkedin.com/in/jan-tomasek/) [](https://x.com/JanTomasekDev)
4
5
6
@@ -23,12 +23,12 @@
23
24
3. **Multi-agent cooperation**
25
- Every agent has a superior agent giving it tasks and instructions. Every agent then reports back to its superior.
26
-- In the case of the first agent, the superior is the human user; the agent sees no difference.
26
+- In the case of the first agent in the chain (Agent 0), the superior is the human user; the agent sees no difference.
27
- Every agent can create its subordinate agent to help break down and solve subtasks. This helps all agents keep their context clean and focused.
28
29
4. **Completely customizable and extensible**
30
- Almost nothing in this framework is hard-coded. Nothing is hidden. Everything can be extended or changed by the user.
31
-- The whole behavior is defined by a system prompt in the **prompts/agent.system.md** file. Change this prompt and change the framework dramatically.
31
+- The whole behavior is defined by a system prompt in the **prompts/default/agent.system.md** file. Change this prompt and change the framework dramatically.
32
- The framework does not guide or limit the agent in any way. There are no hard-coded rails that agents have to follow.
33
- Every prompt, every small message template sent to the agent in its communication loop, can be found in the **prompts/** folder and changed.
34
- Every default tool can be found in the **python/tools/** folder and changed or copied to create new predefined tools.
@@ -81,10 +81,10 @@ If you cannot provide all the necessary conditions or API keys, just change the
81
Update: [Guide by CheezChat for Windows](./docs/win_installation_guide.txt)
82
83
1. **Required API keys:**
84
-- At the moment, the only recommended API key is for https://www.perplexity.ai/ API. Perplexity is used as a convenient web search tool and has not yet been replaced by an open-source alternative. If you do not have an API key for Perplexity, leave it empty in the .env file and Perplexity will not be used.
85
-- Chat models and embedding models can be executed locally via Ollama and HuggingFace or via API as well.
84
+- No API keys are required. Models can run locally. The only recommended API key is for https://www.perplexity.ai/ API. Perplexity is used as a convenient web search tool and has not yet been replaced by an open-source alternative in Agent Zero. If you do not have an API key for Perplexity, leave it empty in the .env file and Perplexity will not be used.
85
+- Chat models and embedding models can be executed locally via Ollama, LMStudio and HuggingFace or via API as well.
86
87
-2. **Enter your API keys:**
87
+2. **Enter your API keys if needed:**
88
- You can enter your API keys into the **.env** file, which you can copy from **example.env**
89
- Or you can export your API keys in the terminal session:
90
~~~bash
@@ -97,9 +97,9 @@ export API_KEY_OPENAI="your-api-key-here"
97
pip install -r requirements.txt
98
~~~
99
100
-3. **Choose your chat, utility and embeddings model:**
101
-- In the **main.py** file, right at the start of the **chat()** function, you can see how the chat model and embedding model are set.
102
-- You can choose between online models (OpenAI, Anthropic, Groq) or offline (Ollama, HuggingFace) for both.
100
+3. **Choose your chat, utility and embeddings model and check other configuration options:**
101
+- In the **initialize.py** file, you can see how the chat model and embedding model are set.
102
+- You can choose between online models (OpenAI, Anthropic, Groq) or offline (Ollama, LMStudio, HuggingFace) for both.
103
104
4. **run Docker:**
105
- Easiest way is to install Docker Desktop application and just run it. The rest will be handled by the framework itself.
agent.py
+66
-18
@@ -9,18 +9,24 @@ from langchain_core.messages import HumanMessage, SystemMessage
9
from langchain_core.language_models.chat_models import BaseChatModel
10
from langchain_core.language_models.llms import BaseLLM
11
from langchain_core.embeddings import Embeddings
12
+from concurrent.futures import Future
13
+from python.helpers.log import Log
14
+from python.helpers.dirty_json import DirtyJson
15
+
16
17
@dataclass
18
class AgentConfig:
19
chat_model: BaseChatModel | BaseLLM
20
utility_model: BaseChatModel | BaseLLM
21
embeddings_model:Embeddings
22
+ prompts_subdir: str = ""
23
memory_subdir: str = ""
24
+ knowledge_subdir: str = ""
25
auto_memory_count: int = 3
26
auto_memory_skip: int = 2
27
rate_limit_seconds: int = 60
28
rate_limit_requests: int = 15
23
- rate_limit_input_tokens: int = 1000000
29
+ rate_limit_input_tokens: int = 0
30
rate_limit_output_tokens: int = 0
31
msgs_keep_max: int = 25
32
msgs_keep_start: int = 5
@@ -54,23 +60,35 @@ class Agent:
60
self.number = number
61
self.agent_name = f"Agent {self.number}"
62
57
- self.system_prompt = files.read_file("./prompts/agent.system.md", agent_name=self.agent_name)
58
- self.tools_prompt = files.read_file("./prompts/agent.tools.md")
59
-
63
self.history = []
64
self.last_message = ""
65
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)
68
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
73
+
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):
88
try:
89
+ self.future = Future()
90
printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
73
- user_message = files.read_file("./prompts/fw.user_message.md", message=msg)
91
+ 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)
94
@@ -81,7 +99,7 @@ class Agent:
99
100
try:
101
84
- system = self.system_prompt + "\n\n" + self.tools_prompt
102
+ 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()
104
if memories: system+= "\n\n"+memories
105
@@ -97,8 +115,9 @@ class Agent:
115
self.rate_limiter.limit_call_and_input(tokens)
116
117
# output that the agent is starting
100
- PrintStyle(bold=True, font_color="green", padding=True, background_color="white").print(f"{self.agent_name}: Starting a message:")
101
-
118
+ 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:")
120
+
121
for chunk in chain.stream(inputs):
122
if self.handle_intervention(agent_response): break # wait for intervention and handle it, if paused
123
@@ -107,33 +126,50 @@ class Agent:
126
else: content = str(chunk)
127
128
if content:
110
- printer.stream(content) # output the agent response stream
129
+ printer.stream(content) # output the agent response stream
130
agent_response += content # concatenate stream into the response
131
+ self.log_from_stream(agent_response, log)
132
133
self.rate_limiter.set_output_tokens(int(len(agent_response)/4))
134
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
118
- warning_msg = files.read_file("./prompts/fw.msg_repeat.md")
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
125
- if tools_result: return tools_result #break the execution if the task is done
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
127
- # Forward errors to the LLM, maybe he can fix them
150
+ # Forward errors to the LLM, maybe it can fix them
151
except Exception as e:
152
error_message = errors.format_error(e)
130
- msg_response = files.read_file("./prompts/fw.error.md", error=error_message) # error message template
153
+ msg_response = self.read_prompt("fw.error.md", error=error_message) # error message template
154
self.append_message(msg_response, human=True)
155
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
158
159
finally:
160
Agent.streaming_agent = None # unset current streamer
161
162
+ def read_prompt(self, file:str, **kwargs):
163
+ content = ""
164
+ if self.config.prompts_subdir:
165
+ try:
166
+ content = files.read_file(files.get_abs_path(f"./prompts/{self.config.prompts_subdir}/{file}"), **kwargs)
167
+ except Exception as e:
168
+ pass
169
+ if not content:
170
+ content = files.read_file(files.get_abs_path(f"./prompts/default/{file}"), **kwargs)
171
+ return content
172
+
173
def get_data(self, field:str):
174
return self.data.get(field, None)
175
@@ -162,10 +198,12 @@ class Agent:
198
chain = prompt | self.config.utility_model
199
response = ""
200
printer = None
201
+ logger = None
202
203
if output_label:
204
PrintStyle(bold=True, font_color="orange", padding=True, background_color="white").print(f"{self.agent_name}: {output_label}:")
168
- printer = PrintStyle(italic=True, font_color="orange", padding=False)
205
+ printer = PrintStyle(italic=True, font_color="orange", padding=False)
206
+ logger = Log(type="adhoc", heading=f"{self.agent_name}: {output_label}:")
207
208
formatted_inputs = prompt.format()
209
tokens = int(len(formatted_inputs)/4)
@@ -180,6 +218,7 @@ class Agent:
218
219
if printer: printer.stream(content)
220
response+=content
221
+ if logger: logger.update(content=response)
222
223
self.rate_limiter.set_output_tokens(int(len(response)/4))
224
@@ -190,7 +229,7 @@ class Agent:
229
return self.history[-1]
230
231
def replace_middle_messages(self,middle_messages):
193
- cleanup_prompt = files.read_file("./prompts/fw.msg_cleanup.md")
232
+ 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")
234
new_human_message = HumanMessage(content=summary)
235
return [new_human_message]
@@ -225,7 +264,7 @@ class Agent:
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
228
- user_msg = files.read_file("./prompts/fw.intervention.md", user_message=self.intervention_message) # format the user intervention template
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
269
self.intervention_message = "" # reset the intervention message
270
self.intervention_status = True
@@ -253,9 +292,10 @@ class Agent:
292
if self.handle_intervention(): return # wait if paused and handle intervention message if needed
293
if response.break_loop: return response.message
294
else:
256
- msg = files.read_file("prompts/fw.msg_misformat.md")
295
+ msg = self.read_prompt("fw.msg_misformat.md")
296
self.append_message(msg, human=True)
297
PrintStyle(font_color="red", padding=True).print(msg)
298
+ Log.log(type="error", content=f"{self.agent_name}: Message misformat:")
299
300
301
def get_tool(self, name: str, args: dict, message: str, **kwargs):
@@ -290,9 +330,17 @@ class Agent:
330
"conversation_history" : messages,
331
"raw_memories": memories
332
}
293
- cleanup_prompt = files.read_file("./prompts/msg.memory_cleanup.md").replace("{", "{{")
333
+ 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")
335
return clean_memories
336
337
+ def log_from_stream(self, stream: str, log: Log):
338
+ try:
339
+ if len(stream) < 25: return # no reason to try
340
+ response = DirtyJson.parse_string(stream)
341
+ if isinstance(response, dict): log.update(content=stream, kvps=response) #log if result is a dictionary already
342
+ except Exception as e:
343
+ pass
344
+
345
def call_extension(self, name: str, **kwargs) -> Any:
346
pass
\ No newline at end of file
docs/win_installation_guide.txt
+4
-4
@@ -49,7 +49,7 @@ pip install -r requirements.txt
49
## Run the program
50
- Just run the **main.py** file in Python:
51
~~~bash
52
-python main.py
52
+python run_ui.py
53
~~~
54
- Or run it in debug mode in VS Code using the **debug** button in the top right corner of the editor. I have provided config files for VS Code for this purpose.
55
@@ -140,7 +140,7 @@ PYDEVD_DISABLE_FILE_VALIDATION=1
140
6. Configure API Preferences
141
142
If you entered an openAI API key earlier, you may skip this step. If you entered an alternative key,
143
-* Right Click on the file **main.py** in the Agent-Zero root folder
143
+* Right Click on the file **initialize.py** in the Agent-Zero root folder
144
* Select **Open With**
145
* Select **Notepad** or your preferred text editor
146
* Scroll about 20 lines down from the top until you see lines that look like this: *chat_llm = models.get_*
@@ -182,11 +182,11 @@ If all of the requirements installed succesfully, you can proceed to run the pro
182
* Activate the Agent-Zero environment by double clicking on its name
183
* click Open Terminal
184
* Navigate to the agent-zero folder
185
-* Type **python main.py** and press enter
185
+* Type **python run_ui.py** and press enter
186
187
```
188
(Agent-Zero) C:\Users\yourUserName>cd c:\projects\agent-zero
189
-(Agent-Zero) C:\projects\agent-zero\python main.py
189
+(Agent-Zero) C:\projects\agent-zero\python run_ui.py
190
Initializing framework...
191
192
User message ('e' to leave):
example.env
+6
-9
@@ -9,14 +9,11 @@ API_KEY_OPENAI_AZURE=
9
OPENAI_AZURE_ENDPOINT=
10
OPENAI_API_VERSION=
11
12
-TOKENIZERS_PARALLELISM=true
13
-PYDEVD_DISABLE_FILE_VALIDATION=1
14
-
12
HF_TOKEN=
13
17
-EDGE_TTS_MODEL="fr-FR-RemyMultilingualNeural"
18
-# en-US-AndrewMultilingualNeural
19
-# de-DE-FlorianMultilingualNeural
20
-# es-ES-AlvaroNeural
21
-# zh-CN-shaanxi-XiaoniNeural
22
-# more check https://huggingface.co/spaces/innoai/Edge-TTS-Text-to-Speech and at bottom use via api
14
+
15
+WEB_UI_PORT=50001
16
+
17
+
18
+TOKENIZERS_PARALLELISM=true
19
+PYDEVD_DISABLE_FILE_VALIDATION=1
\ No newline at end of file
initialize.py
new
+59
@@ -0,0 +1,59 @@
1
+import models
2
+from agent import Agent, AgentConfig
3
+
4
+def initialize():
5
+
6
+ # main chat model used by agents (smarter, more accurate)
7
+ chat_llm = models.get_openai_chat(model_name="gpt-4o-mini", temperature=0)
8
+ # chat_llm = models.get_ollama_chat(model_name="gemma2:latest", temperature=0)
9
+ chat_llm = models.get_lmstudio_chat(model_name="TheBloke/Mistral-7B-Instruct-v0.2-GGUF", temperature=0)
10
+ # chat_llm = models.get_openrouter(model_name="meta-llama/llama-3-8b-instruct:free")
11
+ # chat_llm = models.get_azure_openai_chat(deployment_name="gpt-4o-mini", temperature=0)
12
+ # chat_llm = models.get_anthropic_chat(model_name="claude-3-5-sonnet-20240620", temperature=0)
13
+ # chat_llm = models.get_google_chat(model_name="gemini-1.5-flash", temperature=0)
14
+ # chat_llm = models.get_groq_chat(model_name="llama-3.1-70b-versatile", temperature=0)
15
+
16
+ # utility model used for helper functions (cheaper, faster)
17
+ utility_llm = chat_llm # change if you want to use a different utility model
18
+
19
+ # embedding model used for memory
20
+ embedding_llm = models.get_openai_embedding(model_name="text-embedding-3-small")
21
+ # embedding_llm = models.get_ollama_embedding(model_name="nomic-embed-text")
22
+ # embedding_llm = models.get_huggingface_embedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
23
+
24
+ # agent configuration
25
+ config = AgentConfig(
26
+ chat_model = chat_llm,
27
+ utility_model = utility_llm,
28
+ embeddings_model = embedding_llm,
29
+ prompts_subdir = "custom",
30
+ # memory_subdir = "",
31
+ auto_memory_count = 0,
32
+ # auto_memory_skip = 2,
33
+ # rate_limit_seconds = 60,
34
+ rate_limit_requests = 15,
35
+ # rate_limit_input_tokens = 0,
36
+ # rate_limit_output_tokens = 0,
37
+ # msgs_keep_max = 25,
38
+ # msgs_keep_start = 5,
39
+ # msgs_keep_end = 10,
40
+ max_tool_response_length = 3000,
41
+ # response_timeout_seconds = 60,
42
+ code_exec_docker_enabled = True,
43
+ # code_exec_docker_name = "agent-zero-exe",
44
+ # code_exec_docker_image = "frdel/agent-zero-exe:latest",
45
+ # code_exec_docker_ports = { "22/tcp": 50022 }
46
+ # code_exec_docker_volumes = { files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"} }
47
+ code_exec_ssh_enabled = True,
48
+ # code_exec_ssh_addr = "localhost",
49
+ # code_exec_ssh_port = 50022,
50
+ # code_exec_ssh_user = "root",
51
+ # code_exec_ssh_pass = "toor",
52
+ # additional = {},
53
+ )
54
+
55
+ # create the first agent
56
+ agent0 = Agent( number = 0, config = config )
57
+
58
+ # return initialized agent
59
+ return agent0
\ No newline at end of file
knowledge/.gitkeep
renamed
prompts/default/agent.memory.md
renamed
prompts/default/agent.system.md
renamed
prompts/default/agent.tools.md
renamed
prompts/default/fw.code_no_output.md
renamed
prompts/default/fw.code_runtime_wrong.md
renamed
prompts/default/fw.error.md
renamed
prompts/default/fw.intervention.md
renamed
prompts/default/fw.memories_deleted.md
renamed
prompts/default/fw.memories_not_found.md
renamed
prompts/default/fw.memory_saved.md
renamed
prompts/default/fw.msg_cleanup.md
renamed
prompts/default/fw.msg_from_subordinate.md
renamed
prompts/default/fw.msg_misformat.md
renamed
prompts/default/fw.msg_repeat.md
renamed
prompts/default/fw.msg_timeout.md
renamed
prompts/default/fw.msg_truncated.md
renamed
prompts/default/fw.tool_not_found.md
renamed
prompts/default/fw.tool_response.md
renamed
prompts/default/fw.user_message.md
renamed
prompts/default/msg.memory_cleanup.md
renamed
prompts/default/tool.knowledge.response.md
renamed
python/helpers/docker.py
+14
-3
@@ -5,6 +5,7 @@ from typing import Optional
5
from python.helpers.files import get_abs_path
6
from python.helpers.errors import format_error
7
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):
@@ -24,7 +25,9 @@ class DockerContainerManager:
25
err = format_error(e)
26
if ("ConnectionRefusedError(61," in err or "Error while fetching server API version" in err):
27
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
PrintStyle.error(err)
30
+ Log.log(type="error", content=err)
31
time.sleep(5) # try again in 5 seconds
32
else: raise
33
return self.client
@@ -35,8 +38,11 @@ class DockerContainerManager:
38
self.container.stop()
39
self.container.remove()
40
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
except Exception as e:
43
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
+
46
47
def start_container(self) -> None:
48
if not self.client: self.client = self.init_docker()
@@ -49,6 +55,8 @@ class DockerContainerManager:
55
if existing_container:
56
if existing_container.status != 'running':
57
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
+
60
existing_container.start()
61
self.container = existing_container
62
time.sleep(2) # this helps to get SSH ready
@@ -58,13 +66,16 @@ class DockerContainerManager:
66
# print(f"Container with name '{self.name}' is already running with ID: {existing_container.id}")
67
else:
68
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
+
71
self.container = self.client.containers.run(
72
self.image,
73
detach=True,
64
- ports=self.ports,
74
+ ports=self.ports, # type: ignore
75
name=self.name,
66
- volumes=self.volumes,
67
- )
76
+ volumes=self.volumes, # type: ignore
77
+ )
78
atexit.register(self.cleanup_container)
79
print(f"Started container with ID: {self.container.id}")
80
+ Log.log(type="info", content=f"Started container with ID: {self.container.id}")
81
time.sleep(5) # this helps to get SSH ready
python/helpers/extract_tools.py
-1
@@ -39,7 +39,6 @@ def extract_json_string(content):
39
# Return the matched JSON string
40
return match.group(0)
41
else:
42
- print("No JSON content found.")
42
return ""
43
44
def fix_json_string(json_string):
python/helpers/knowledge_import.py
new
+87
@@ -0,0 +1,87 @@
1
+import glob
2
+import os
3
+import hashlib
4
+import json
5
+from typing import Any, Dict, Literal, TypedDict
6
+from langchain_community.document_loaders import (
7
+ CSVLoader, JSONLoader, PyPDFLoader, TextLoader, UnstructuredHTMLLoader,
8
+ UnstructuredMarkdownLoader
9
+)
10
+from python.helpers import files
11
+from python.helpers.log import Log
12
+
13
+text_loader_kwargs = {'autodetect_encoding': True}
14
+
15
+
16
+class KnowledgeImport(TypedDict):
17
+ file: str
18
+ checksum: str
19
+ ids: list[str]
20
+ state: Literal["changed", "original", "removed"]
21
+ documents: list[Any]
22
+
23
+
24
+def calculate_checksum(file_path: str) -> str:
25
+ hasher = hashlib.md5()
26
+ with open(file_path, 'rb') as f:
27
+ buf = f.read()
28
+ hasher.update(buf)
29
+ return hasher.hexdigest()
30
+
31
+def load_knowledge(knowledge_dir: str, index: Dict[str, KnowledgeImport]) -> Dict[str, KnowledgeImport]:
32
+ knowledge_dir = files.get_abs_path(knowledge_dir)
33
+
34
+
35
+ # Mapping file extensions to corresponding loader classes
36
+ file_types_loaders = {
37
+ 'txt': TextLoader,
38
+ 'pdf': PyPDFLoader,
39
+ 'csv': CSVLoader,
40
+ 'html': UnstructuredHTMLLoader,
41
+ 'json': JSONLoader,
42
+ 'md': UnstructuredMarkdownLoader
43
+ }
44
+
45
+ cnt_files = 0
46
+ cnt_docs = 0
47
+
48
+ # Fetch all files in the directory with specified extensions
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...")
53
+
54
+ for file_path in kn_files:
55
+ ext = file_path.split('.')[-1].lower()
56
+ if ext in file_types_loaders:
57
+ checksum = calculate_checksum(file_path)
58
+ file_key = os.path.relpath(file_path, knowledge_dir)
59
+
60
+ # Load existing data from the index or create a new entry
61
+ file_data = index.get(file_key, {})
62
+
63
+ if file_data.get('checksum') == checksum:
64
+ file_data['state'] = 'original'
65
+ else:
66
+ file_data['state'] = 'changed'
67
+
68
+ if file_data['state'] == 'changed':
69
+ file_data['checksum'] = checksum
70
+ loader_cls = file_types_loaders[ext]
71
+ loader = loader_cls(file_path, **(text_loader_kwargs if ext in ['txt', 'csv', 'html', 'md'] else {}))
72
+ file_data['documents'] = loader.load_and_split()
73
+ cnt_files += 1
74
+ cnt_docs += len(file_data['documents'])
75
+ # print(f"Imported {len(file_data['documents'])} documents from {file_path}")
76
+
77
+ # Update the index
78
+ index[file_key] = file_data # type: ignore
79
+
80
+ # loop index where state is not set and mark it as removed
81
+ for file_key, file_data in index.items():
82
+ if not file_data.get('state', ''):
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.")
87
+ return index
python/helpers/log.py
new
+58
@@ -0,0 +1,58 @@
1
+from dataclasses import dataclass
2
+from typing import Optional, Dict
3
+import uuid
4
+
5
+@dataclass
6
+class LogItem:
7
+ no: int
8
+ type: str
9
+ heading: str
10
+ content: str
11
+ kvps: Optional[Dict] = None
12
+
13
+
14
+class Log:
15
+
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.version += 1
39
+ Log.last_updated = item.no
40
+ 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.version += 1
56
+ Log.last_updated = no
57
+ else:
58
+ raise IndexError("Log item number out of range")
python/helpers/messages.py
+4
-4
@@ -1,12 +1,12 @@
1
-from . import files
1
+# from . import files
2
3
-
4
-def truncate_text(output, threshold=1000):
3
+def truncate_text(agent, output, threshold=1000):
4
if len(output) <= threshold:
5
return output
6
7
# Adjust the file path as needed
9
- placeholder = files.read_file("./prompts/fw.msg_truncated.md", removed_chars=(len(output) - threshold))
8
+ placeholder = agent.read_prompt("fw.msg_truncated.md", removed_chars=(len(output) - threshold))
9
+ # placeholder = files.read_file("./prompts/default/fw.msg_truncated.md", removed_chars=(len(output) - threshold))
10
11
start_len = (threshold - len(placeholder)) // 2
12
end_len = threshold - len(placeholder) - start_len
python/helpers/rate_limiter.py
+2
-14
@@ -3,6 +3,7 @@ from collections import deque
3
from dataclasses import dataclass
4
from typing import List, Tuple
5
from .print_style import PrintStyle
6
+from .log import Log
7
8
@dataclass
9
class CallRecord:
@@ -48,6 +49,7 @@ class RateLimiter:
49
wait_time = oldest_record.timestamp + self.window_seconds - current_time
50
if wait_time > 0:
51
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
time.sleep(wait_time)
54
current_time = time.time()
55
@@ -62,17 +64,3 @@ class RateLimiter:
64
if self.call_records:
65
self.call_records[-1].output_tokens += output_token_count
66
return self
65
-
66
-# Example usage
67
-rate_limiter = RateLimiter(max_calls=5, max_input_tokens=1000, max_output_tokens=2000)
68
-
69
-def rate_limited_function(input_token_count: int, output_token_count: int):
70
- # First, limit the call and input tokens (this may wait)
71
- rate_limiter.limit_call_and_input(input_token_count)
72
-
73
- # Your function logic here
74
- print(f"Function called with {input_token_count} input tokens")
75
-
76
- # After processing, set the output tokens (this doesn't wait)
77
- rate_limiter.set_output_tokens(output_token_count)
78
- print(f"Function completed with {output_token_count} output tokens")
python/helpers/shell_ssh.py
+49
-17
@@ -2,12 +2,13 @@ import paramiko
2
import time
3
import re
4
from typing import Optional, Tuple
5
+from python.helpers.log import Log
6
+from python.helpers.strings import calculate_valid_match_lengths
7
8
class SSHInteractiveSession:
9
8
- end_comment = "# @@==>> SSHInteractiveSession End-of-Command <<==@@"
9
-
10
- ps1_label = "SSHInteractiveSession CLI>"
10
+ # end_comment = "# @@==>> SSHInteractiveSession End-of-Command <<==@@"
11
+ # ps1_label = "SSHInteractiveSession CLI>"
12
13
def __init__(self, hostname: str, port: int, username: str, password: str):
14
self.hostname = hostname
@@ -18,6 +19,9 @@ class SSHInteractiveSession:
19
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
20
self.shell = None
21
self.full_output = b''
22
+ self.last_command = b''
23
+ self.trimmed_command_length = 0 # Initialize trimmed_command_length
24
+
25
26
def connect(self):
27
# try 3 times with wait and then except
@@ -27,15 +31,17 @@ class SSHInteractiveSession:
31
self.client.connect(self.hostname, self.port, self.username, self.password)
32
self.shell = self.client.invoke_shell(width=160,height=48)
33
# self.shell.send(f'PS1="{SSHInteractiveSession.ps1_label}"'.encode())
30
- return
31
- # while True: # wait for end of initial output
32
- # full, part = self.read_output()
33
- # if full and not part: return
34
- # time.sleep(0.1)
34
+ # return
35
+ while True: # wait for end of initial output
36
+ full, part = self.read_output()
37
+ if full and not part: return
38
+ time.sleep(0.1)
39
except Exception as e:
40
errors += 1
41
if errors < 3:
42
print(f"SSH Connection attempt {errors}...")
43
+ Log.log(type="info", content=f"SSH Connection attempt {errors}...")
44
+
45
time.sleep(5)
46
else:
47
raise e
@@ -50,15 +56,32 @@ class SSHInteractiveSession:
56
if not self.shell:
57
raise Exception("Shell not connected")
58
self.full_output = b""
53
- self.shell.send((command + " \\\n" +SSHInteractiveSession.end_comment + "\n").encode())
59
+ # if len(command) > 10: # if command is long, add end_comment to split output
60
+ # command = (command + " \\\n" +SSHInteractiveSession.end_comment + "\n")
61
+ # else:
62
+ command = command + "\n"
63
+ self.last_command = command.encode()
64
+ self.trimmed_command_length = 0
65
+ self.shell.send(self.last_command)
66
67
def read_output(self) -> Tuple[str, str]:
68
if not self.shell:
69
raise Exception("Shell not connected")
70
71
partial_output = b''
72
+
73
while self.shell.recv_ready():
74
data = self.shell.recv(1024)
75
+
76
+ # Trim own command from output
77
+ if self.last_command and len(self.last_command) > self.trimmed_command_length:
78
+ command_to_trim = self.last_command[self.trimmed_command_length:]
79
+
80
+ trim_com, trim_out = calculate_valid_match_lengths(command_to_trim, data, deviation_threshold=8, deviation_reset=2, ignore_patterns=[rb'\x1b\[\?\d{4}[a-zA-Z]',rb'\r',rb'>'])
81
+ if(trim_com > 0 and trim_out > 0):
82
+ data = data[trim_out:]
83
+ self.trimmed_command_length += trim_com
84
+
85
partial_output += data
86
self.full_output += data
87
time.sleep(0.1) # Prevent busy waiting
@@ -66,15 +89,15 @@ class SSHInteractiveSession:
89
# Decode once at the end
90
decoded_partial_output = partial_output.decode('utf-8', errors='replace')
91
decoded_full_output = self.full_output.decode('utf-8', errors='replace')
69
-
92
+
93
decoded_partial_output = self.clean_string(decoded_partial_output)
94
decoded_full_output = self.clean_string(decoded_full_output)
95
73
- # Split output at end_comment
74
- if SSHInteractiveSession.end_comment in decoded_full_output:
75
- decoded_full_output = decoded_full_output.split(SSHInteractiveSession.end_comment)[-1].lstrip("\r\n")
76
- decoded_partial_output = decoded_partial_output.split(SSHInteractiveSession.end_comment)[-1].lstrip("\r\n")
77
-
96
+ # # Split output at end_comment
97
+ # if SSHInteractiveSession.end_comment in decoded_full_output:
98
+ # decoded_full_output = decoded_full_output.split(SSHInteractiveSession.end_comment)[-1].lstrip("\r\n")
99
+ # decoded_partial_output = decoded_partial_output.split(SSHInteractiveSession.end_comment)[-1].lstrip("\r\n")
100
+
101
return decoded_full_output, decoded_partial_output
102
103
@@ -85,5 +108,14 @@ class SSHInteractiveSession:
108
109
# Replace '\r\n' with '\n'
110
cleaned = cleaned.replace('\r\n', '\n')
88
-
89
- return cleaned
\ No newline at end of file
111
+
112
+ # Split the string by newline characters to process each segment separately
113
+ lines = cleaned.split('\n')
114
+
115
+ for i in range(len(lines)):
116
+ # Handle carriage returns '\r' by splitting and taking the last part
117
+ parts = [part for part in lines[i].split('\r') if part.strip()]
118
+ if parts: lines[i] = parts[-1].rstrip() # Overwrite with the last part after the last '\r'
119
+
120
+ return '\n'.join(lines)
121
+
python/helpers/strings.py
new
+125
@@ -0,0 +1,125 @@
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
+
53
+import re
54
+
55
+def calculate_valid_match_lengths(first: bytes | str, second: bytes | str,
56
+ deviation_threshold: int = 5,
57
+ deviation_reset: int = 5,
58
+ ignore_patterns: list[bytes|str] = []) -> tuple[int, int]:
59
+
60
+ first_length = len(first)
61
+ second_length = len(second)
62
+
63
+ i, j = 0, 0
64
+ deviations = 0
65
+ matched_since_deviation = 0
66
+ last_matched_i, last_matched_j = 0, 0 # Track the last matched index
67
+
68
+ def skip_ignored_patterns(s, index):
69
+ """Skip characters in `s` that match any pattern in `ignore_patterns` starting from `index`."""
70
+ while index < len(s):
71
+ for pattern in ignore_patterns:
72
+ match = re.match(pattern, s[index:])
73
+ if match:
74
+ index += len(match.group(0))
75
+ break
76
+ else:
77
+ break
78
+ return index
79
+
80
+ while i < first_length and j < second_length:
81
+ # Skip ignored patterns
82
+ i = skip_ignored_patterns(first, i)
83
+ j = skip_ignored_patterns(second, j)
84
+
85
+ if i < first_length and j < second_length and first[i] == second[j]:
86
+ last_matched_i, last_matched_j = i + 1, j + 1 # Update last matched position
87
+ i += 1
88
+ j += 1
89
+ matched_since_deviation += 1
90
+
91
+ # Reset the deviation counter if we've matched enough characters since the last deviation
92
+ if matched_since_deviation >= deviation_reset:
93
+ deviations = 0
94
+ matched_since_deviation = 0
95
+ else:
96
+ # Determine the look-ahead based on the remaining deviation threshold
97
+ look_ahead = deviation_threshold - deviations
98
+
99
+ # Look ahead to find the best match within the remaining deviation allowance
100
+ best_match = None
101
+ for k in range(1, look_ahead + 1):
102
+ if i + k < first_length and first[i + k] == second[j]:
103
+ best_match = ('i', k)
104
+ break
105
+ if j + k < second_length and first[i] == second[j + k]:
106
+ best_match = ('j', k)
107
+ break
108
+
109
+ if best_match:
110
+ if best_match[0] == 'i':
111
+ i += best_match[1]
112
+ elif best_match[0] == 'j':
113
+ j += best_match[1]
114
+ else:
115
+ i += 1
116
+ j += 1
117
+
118
+ deviations += 1
119
+ matched_since_deviation = 0
120
+
121
+ if deviations > deviation_threshold:
122
+ break
123
+
124
+ # Return the last matched positions instead of the current indices
125
+ return last_matched_i, last_matched_j
\ No newline at end of file
python/helpers/tool.py
+5
-2
@@ -3,6 +3,7 @@ from typing import TypedDict
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
7
8
class Response:
9
def __init__(self, message: str, break_loop: bool) -> None:
@@ -24,6 +25,7 @@ class Tool:
25
def before_execution(self, **kwargs):
26
if self.agent.handle_intervention(): return # wait for intervention and handle it, if paused
27
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)
29
if self.args and isinstance(self.args, dict):
30
for key, value in self.args.items():
31
PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ")
@@ -31,12 +33,13 @@ class Tool:
33
PrintStyle().print()
34
35
def after_execution(self, response: Response, **kwargs):
34
- text = messages.truncate_text(response.message.strip(), self.agent.config.max_tool_response_length)
35
- msg_response = files.read_file("./prompts/fw.tool_response.md", tool_name=self.name, tool_response=text)
36
+ text = messages.truncate_text(self.agent, response.message.strip(), self.agent.config.max_tool_response_length)
37
+ 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)
40
PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}':")
41
PrintStyle(font_color="#85C1E9").print(response.message)
42
+ self.log.update(content=response.message)
43
44
def nice_key(self, key:str):
45
words = key.split('_')
python/helpers/vdb.py
deleted
-69
@@ -1,69 +0,0 @@
1
-from langchain.storage import InMemoryByteStore, LocalFileStore
2
-from langchain.embeddings import CacheBackedEmbeddings
3
-from langchain_core.embeddings import Embeddings
4
-
5
-from langchain_chroma import Chroma
6
-import chromadb
7
-from chromadb.config import Settings
8
-
9
-from . import files
10
-from langchain_core.documents import Document
11
-import uuid
12
-
13
-
14
-class VectorDB:
15
-
16
- def __init__(self, embeddings_model:Embeddings, in_memory=False, cache_dir="./cache"):
17
- print("Initializing VectorDB...")
18
- self.embeddings_model = embeddings_model
19
-
20
- db_cache = files.get_abs_path(cache_dir,"database")
21
-
22
- self.client =chromadb.PersistentClient(path=db_cache)
23
- self.collection = self.client.create_collection("my_collection")
24
- self.collection
25
-
26
-
27
- def search(self, query:str, results=2):
28
- emb = self.embeddings_model.embed_query(query)
29
- res = self.collection.query(query_embeddings=[emb],n_results=results)
30
- best = res["documents"][0][0] # type: ignore
31
-
32
- # def delete_documents(self, query):
33
- # score_limit = 1
34
- # k = 2
35
- # tot = 0
36
- # while True:
37
- # # Perform similarity search with score
38
- # docs = self.db.similarity_search_with_score(query, k=k)
39
-
40
- # # Extract document IDs and filter based on score
41
- # document_ids = [result[0].metadata["id"] for result in docs if result[1] < score_limit]
42
-
43
- # # Delete documents with IDs over the threshold score
44
- # if document_ids:
45
- # fnd = self.db.get(where={"id": {"$in": document_ids}})
46
- # if fnd["ids"]: self.db.delete(ids=fnd["ids"])
47
- # tot += len(fnd["ids"])
48
-
49
- # # If fewer than K document IDs, break the loop
50
- # if len(document_ids) < k:
51
- # break
52
-
53
- # return tot
54
-
55
- def insert(self, data:str):
56
-
57
- id = str(uuid.uuid4())
58
- emb = self.embeddings_model.embed_documents([data])[0]
59
-
60
- self.collection.add(
61
- ids=[id],
62
- embeddings=[emb],
63
- documents=[data],
64
- )
65
-
66
- return id
67
-
68
-
69
-
python/helpers/vector_db.py
+82
-12
@@ -1,25 +1,33 @@
1
from langchain.storage import InMemoryByteStore, LocalFileStore
2
from langchain.embeddings import CacheBackedEmbeddings
3
-from langchain_chroma import Chroma
3
+# from langchain_chroma import Chroma
4
+from langchain_community.vectorstores import FAISS
5
+import faiss
6
+from langchain_community.docstore.in_memory import InMemoryDocstore
7
8
+import os, json
9
from . import files
10
from langchain_core.documents import Document
11
import uuid
8
-
12
+from python.helpers import knowledge_import
13
+from python.helpers.log import Log
14
15
class VectorDB:
16
12
- def __init__(self, embeddings_model, in_memory=False, cache_dir="./cache"):
17
+ def __init__(self, embeddings_model, in_memory=False, memory_dir="./memory", knowledge_dir="./knowledge"):
18
print("Initializing VectorDB...")
19
+ Log.log("info", content="Initializing VectorDB...")
20
+
21
self.embeddings_model = embeddings_model
22
16
- em_cache = files.get_abs_path(cache_dir,"embeddings")
17
- db_cache = files.get_abs_path(cache_dir,"database")
23
+ self.em_dir = files.get_abs_path(memory_dir,"embeddings")
24
+ self.db_dir = files.get_abs_path(memory_dir,"database")
25
+ self.kn_dir = files.get_abs_path(knowledge_dir) if knowledge_dir else ""
26
27
if in_memory:
28
self.store = InMemoryByteStore()
29
else:
22
- self.store = LocalFileStore(em_cache)
30
+ self.store = LocalFileStore(self.em_dir)
31
32
33
#here we setup the embeddings model with the chosen cache storage
@@ -28,9 +36,63 @@ class VectorDB:
36
self.store,
37
namespace=getattr(embeddings_model, 'model', getattr(embeddings_model, 'model_name', "default")) )
38
39
+ # self.db = Chroma(
40
+ # embedding_function=self.embedder,
41
+ # persist_directory=db_dir)
42
32
- self.db = Chroma(embedding_function=self.embedder,persist_directory=db_cache)
43
44
+ # if db folder exists and is not empty:
45
+ if os.path.exists(self.db_dir) and len(os.listdir(self.db_dir)) > 0:
46
+ self.db = FAISS.load_local(
47
+ folder_path=self.db_dir,
48
+ embeddings=self.embedder,
49
+ allow_dangerous_deserialization=True
50
+ )
51
+ else:
52
+ index = faiss.IndexFlatL2(len(self.embedder.embed_query("example text")))
53
+
54
+ self.db = FAISS(
55
+ embedding_function=self.embedder,
56
+ index=index,
57
+ docstore=InMemoryDocstore(),
58
+ index_to_docstore_id={})
59
+
60
+ #preload knowledge files
61
+ if self.kn_dir:
62
+ self.preload_knowledge(self.kn_dir, self.db_dir)
63
+
64
+
65
+ def preload_knowledge(self, kn_dir:str, db_dir:str):
66
+
67
+ # Load the index file if it exists
68
+ index_path = files.get_abs_path(db_dir, "knowledge_import.json")
69
+
70
+ #make sure directory exists
71
+ if not os.path.exists(db_dir):
72
+ os.makedirs(db_dir)
73
+
74
+ index: dict[str, knowledge_import.KnowledgeImport] = {}
75
+ if os.path.exists(index_path):
76
+ with open(index_path, 'r') as f:
77
+ index = json.load(f)
78
+
79
+ index = knowledge_import.load_knowledge(kn_dir,index)
80
+
81
+ for file in index:
82
+ if index[file]['state'] in ['changed', 'removed'] and index[file].get('ids',[]): # for knowledge files that have been changed or removed and have IDs
83
+ self.delete_documents_by_ids(index[file]['ids']) # remove original version
84
+ if index[file]['state'] == 'changed':
85
+ index[file]['ids'] = self.insert_documents(index[file]['documents']) # insert new version
86
+
87
+ # remove index where state="removed"
88
+ index = {k: v for k, v in index.items() if v['state'] != 'removed'}
89
+
90
+ # strip state and documents from index and save it
91
+ for file in index:
92
+ if "documents" in index[file]: del index[file]['documents'] # type: ignore
93
+ if "state" in index[file]: del index[file]['state'] # type: ignore
94
+ with open(index_path, 'w') as f:
95
+ json.dump(index, f)
96
97
def search_similarity(self, query, results=3):
98
return self.db.similarity_search(query,results)
@@ -64,7 +126,8 @@ class VectorDB:
126
# If fewer than K document IDs, break the loop
127
if len(document_ids) < k:
128
break
67
-
129
+
130
+ if tot: self.db.save_local(folder_path=self.db_dir) # persist
131
return tot
132
133
def delete_documents_by_ids(self, ids:list[str]):
@@ -72,13 +135,20 @@ class VectorDB:
135
self.db.delete(ids=ids)
136
# post = self.db.get(ids=ids)["ids"]
137
#TODO? compare pre and post
138
+ if ids: self.db.save_local(folder_path=self.db_dir) #persist
139
return len(ids)
140
77
- def insert_document(self, data):
141
+ def insert_text(self, text):
142
id = str(uuid.uuid4())
79
- self.db.add_documents(documents=[ Document(data, metadata={"id": id}) ], ids=[id])
80
-
143
+ self.db.add_documents(documents=[ Document(text, metadata={"id": id}) ], ids=[id])
144
+ self.db.save_local(folder_path=self.db_dir) #persist
145
return id
82
-
146
+
147
+ def insert_documents(self, docs:list[Document]):
148
+ ids = [str(uuid.uuid4()) for _ in range(len(docs))]
149
+ for doc, id in zip(docs, ids): doc.metadata["id"] = id #add ids to documents metadata
150
+ self.db.add_documents(documents=docs, ids=ids)
151
+ self.db.save_local(folder_path=self.db_dir) #persist
152
+ return ids
153
154
python/tools/code_execution_tool.py
+15
-3
@@ -11,6 +11,7 @@ from python.helpers.print_style import PrintStyle
11
from python.helpers.shell_local import LocalInteractiveSession
12
from python.helpers.shell_ssh import SSHInteractiveSession
13
from python.helpers.docker import DockerContainerManager
14
+from python.helpers.log import Log
15
16
@dataclass
17
class State:
@@ -38,13 +39,23 @@ class CodeExecution(Tool):
39
elif runtime == "output":
40
response = self.get_terminal_output()
41
else:
41
- response = files.read_file("./prompts/fw.code_runtime_wrong.md", runtime=runtime)
42
+ response = self.agent.read_prompt("fw.code_runtime_wrong.md", runtime=runtime)
43
43
- if not response: response = files.read_file("./prompts/fw.code_no_output.md")
44
+ if not response: response = self.agent.read_prompt("fw.code_no_output.md")
45
return Response(message=response, break_loop=False)
46
47
+ def before_execution(self, **kwargs):
48
+ if self.agent.handle_intervention(): return # wait for intervention and handle it, if paused
49
+ 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)
51
+ if self.args and isinstance(self.args, dict):
52
+ for key, value in self.args.items():
53
+ PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ")
54
+ PrintStyle(font_color="#85C1E9", padding=isinstance(value,str) and "\n" in value).stream(value)
55
+ PrintStyle().print()
56
+
57
def after_execution(self, response, **kwargs):
47
- msg_response = files.read_file("./prompts/fw.tool_response.md", tool_name=self.name, tool_response=response.message)
58
+ 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)
60
61
def prepare_state(self):
@@ -98,6 +109,7 @@ class CodeExecution(Tool):
109
110
if partial_output:
111
PrintStyle(font_color="#85C1E9").stream(partial_output)
112
+ self.log.update(content=full_output)
113
idle=0
114
else:
115
idle+=1
python/tools/knowledge_tool.py
+3
-2
@@ -1,6 +1,5 @@
1
import os
2
from agent import Agent
3
-from . import online_knowledge_tool
3
from python.helpers import perplexity_search
4
from python.helpers import duckduckgo_search
5
@@ -10,6 +9,7 @@ import concurrent.futures
9
from python.helpers.tool import Tool, Response
10
from python.helpers import files
11
from python.helpers.print_style import PrintStyle
12
+from python.helpers.log import Log
13
14
class Knowledge(Tool):
15
def execute(self, question="", **kwargs):
@@ -21,6 +21,7 @@ class Knowledge(Tool):
21
perplexity = executor.submit(perplexity_search.perplexity_search, question)
22
else:
23
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.")
25
perplexity = None
26
27
@@ -46,7 +47,7 @@ class Knowledge(Tool):
47
except Exception as e:
48
memory_result = "Memory search failed: " + str(e)
49
49
- msg = files.read_file("prompts/tool.knowledge.response.md",
50
+ msg = self.agent.read_prompt("tool.knowledge.response.md",
51
online_sources = ((perplexity_result + "\n\n") if perplexity else "") + str(duckduckgo_result),
52
memory = memory_result )
53
python/tools/memory_tool.py
+26
-16
@@ -6,9 +6,10 @@ import os, json
6
from python.helpers.tool import Tool, Response
7
from python.helpers.print_style import PrintStyle
8
from chromadb.errors import InvalidDimensionException
9
+from python.helpers.log import Log
10
10
-# TODO multiple DBs at once
11
-db: VectorDB | None= None
11
+# databases based on subdirectories from agent config
12
+dbs = {}
13
14
class Memory(Tool):
15
def execute(self,**kwargs):
@@ -28,39 +29,48 @@ class Memory(Tool):
29
except InvalidDimensionException as e:
30
# hint about embedding change with existing database
31
PrintStyle.hint("If you changed your embedding model, you will need to remove contents of /memory directory.")
32
+ Log(type="hint", content="If you changed your embedding model, you will need to remove contents of /memory directory.")
33
raise
34
35
# result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
36
return Response(message=result, break_loop=False)
37
38
def search(agent:Agent, query:str, count:int=5, threshold:float=0.1):
37
- initialize(agent)
39
+ db = get_db(agent)
40
+ # docs = db.search_similarity(query,count) # type: ignore
41
docs = db.search_similarity_threshold(query,count,threshold) # type: ignore
39
- if len(docs)==0: return files.read_file("./prompts/fw.memories_not_found.md", query=query)
42
+ if len(docs)==0: return agent.read_prompt("fw.memories_not_found.md", query=query)
43
else: return str(docs)
44
45
def save(agent:Agent, text:str):
43
- initialize(agent)
44
- id = db.insert_document(text) # type: ignore
45
- return files.read_file("./prompts/fw.memory_saved.md", memory_id=id)
46
+ db = get_db(agent)
47
+ id = db.insert_text(text) # type: ignore
48
+ return agent.read_prompt("fw.memory_saved.md", memory_id=id)
49
50
def delete(agent:Agent, ids_str:str):
48
- initialize(agent)
51
+ db = get_db(agent)
52
ids = extract_guids(ids_str)
53
deleted = db.delete_documents_by_ids(ids) # type: ignore
51
- return files.read_file("./prompts/fw.memories_deleted.md", memory_count=deleted)
54
+ return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
55
56
def forget(agent:Agent, query:str):
54
- initialize(agent)
57
+ db = get_db(agent)
58
deleted = db.delete_documents_by_query(query) # type: ignore
56
- return files.read_file("./prompts/fw.memories_deleted.md", memory_count=deleted)
59
+ return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
60
58
-def initialize(agent:Agent):
59
- global db
60
- if not db:
61
- dir = os.path.join("memory",agent.config.memory_subdir)
62
- db = VectorDB(embeddings_model=agent.config.embeddings_model, in_memory=False, cache_dir=dir)
61
+def get_db(agent: Agent):
62
+ mem_dir = os.path.join("memory", agent.config.memory_subdir)
63
+ kn_dir = os.path.join("knowledge", agent.config.knowledge_subdir)
64
+ key = (mem_dir, kn_dir)
65
66
+ if key not in dbs:
67
+ db = VectorDB(embeddings_model=agent.config.embeddings_model, in_memory=False, memory_dir=mem_dir, knowledge_dir=kn_dir)
68
+ dbs[key] = db
69
+ else:
70
+ db = dbs[key]
71
+
72
+ return db
73
+
74
def extract_guids(text):
75
pattern = r'\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b'
76
return re.findall(pattern, text)
\ No newline at end of file
python/tools/online_knowledge_tool.py
deleted
-13
@@ -1,13 +0,0 @@
1
-from agent import Agent
2
-from python.helpers import perplexity_search
3
-from python.helpers.tool import Tool, Response
4
-
5
-class OnlineKnowledge(Tool):
6
- def execute(self,**kwargs):
7
- return Response(
8
- message=process_question(self.args["question"]),
9
- break_loop=False,
10
- )
11
-
12
-def process_question(question):
13
- return str(perplexity_search.perplexity_search(question))
\ No newline at end of file
python/tools/response.py
+3
-1
@@ -6,6 +6,7 @@ from agent import Agent
6
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
10
11
class ResponseTool(Tool):
12
@@ -14,7 +15,8 @@ class ResponseTool(Tool):
15
return Response(message=self.args["text"], break_loop=True)
16
17
def before_execution(self, **kwargs):
17
- pass # do not add anything to the history or output
18
+ self.log = Log(type="response", heading=f"{self.agent.agent_name}: Responding:", content=self.args.get("text", ""))
19
+
20
21
def after_execution(self, response, **kwargs):
22
pass # do not add anything to the history or output
\ No newline at end of file
python/tools/task_done.py
+2
-1
@@ -6,6 +6,7 @@ from agent import Agent
6
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
10
11
class TaskDone(Tool):
12
@@ -14,7 +15,7 @@ class TaskDone(Tool):
15
return Response(message=self.args["text"], break_loop=True)
16
17
def before_execution(self, **kwargs):
17
- pass # do not add anything to the history or output
18
+ self.log = Log(type="response", heading=f"{self.agent.agent_name}: Task done:", content=self.args.get("text", ""))
19
20
def after_execution(self, response, **kwargs):
21
pass # do add anything to the history or output
\ No newline at end of file
python/tools/unknown.py
+2
-2
@@ -4,8 +4,8 @@ from python.helpers import files
4
class Unknown(Tool):
5
def execute(self, **kwargs):
6
return Response(
7
- message=files.read_file("prompts/fw.tool_not_found.md",
7
+ message=self.agent.read_prompt("fw.tool_not_found.md",
8
tool_name=self.name,
9
- tools_prompt=files.read_file("prompts/agent.tools.md")),
9
+ tools_prompt=self.agent.read_prompt("agent.tools.md")),
10
break_loop=False)
11
requirements.txt
+5
-2
@@ -5,7 +5,6 @@ langchain-huggingface==0.0.3
5
langchain-openai==0.1.15
6
langchain-community==0.2.7
7
langchain-anthropic==0.1.19
8
-langchain-chroma==0.1.2
8
langchain-google-genai==1.0.7
9
webcolors==24.6.0
10
sentence-transformers==3.0.1
@@ -16,4 +15,8 @@ inputimeout==1.0.4
15
newspaper3k==0.2.8
16
beautifulsoup4==4.12.3
17
lxml_html_clean==0.2.0
19
-pynput==1.7.7
\ No newline at end of file
18
+pynput==1.7.7
19
+pypdf==4.3.1
20
+Flask[async]==3.0.3
21
+Flask-BasicAuth==0.2.0
22
+faiss-cpu==1.8.0.post1
\ No newline at end of file
run_cli.py
renamed
+17
-84
@@ -1,5 +1,4 @@
1
-import threading, time, models, tts, stt, os, sys, argparse
2
-from dotenv import load_dotenv
1
+import threading, time, models, os
2
from ansio import application_keypad, mouse_input, raw_input
3
from ansio.input import InputEvent, get_input_event
4
from agent import Agent, AgentConfig
@@ -7,105 +6,42 @@ from python.helpers.print_style import PrintStyle
6
from python.helpers.files import read_file
7
from python.helpers import files
8
import python.helpers.timed_input as timed_input
10
-from pynput import keyboard
9
+from initialize import initialize
10
12
-load_dotenv() # take environment variables from.env.
11
12
input_lock = threading.Lock()
13
os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir
14
17
-def initialize():
18
-
19
- # main chat model used by agents (smarter, more accurate)
20
- chat_llm = models.get_openai_chat(model_name="gpt-4o-mini", temperature=0)
21
- # chat_llm = models.get_ollama_chat(model_name="gemma2:latest", temperature=0)
22
- # chat_llm = models.get_lmstudio_chat(model_name="TheBloke/Mistral-7B-Instruct-v0.2-GGUF", temperature=0)
23
- # chat_llm = models.get_openrouter(model_name="meta-llama/llama-3-8b-instruct:free")
24
- # chat_llm = models.get_azure_openai_chat(deployment_name="gpt-4o-mini", temperature=0)
25
- # chat_llm = models.get_anthropic_chat(model_name="claude-3-5-sonnet-20240620", temperature=0)
26
- # chat_llm = models.get_google_chat(model_name="gemini-1.5-flash", temperature=0)
27
- # chat_llm = models.get_groq_chat(model_name="llama-3.1-70b-versatile", temperature=0)
28
-
29
- # utility model used for helper functions (cheaper, faster)
30
- utility_llm = chat_llm # change if you want to use a different utility model
31
-
32
- # embedding model used for memory
33
- embedding_llm = models.get_openai_embedding(model_name="text-embedding-3-small")
34
- # embedding_llm = models.get_ollama_embedding(model_name="nomic-embed-text")
35
- # embedding_llm = models.get_huggingface_embedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
36
-
37
- # agent configuration
38
- config = AgentConfig(
39
- chat_model = chat_llm,
40
- utility_model = utility_llm,
41
- embeddings_model = embedding_llm,
42
- # memory_subdir = "",
43
- auto_memory_count = 0,
44
- # auto_memory_skip = 2,
45
- # rate_limit_seconds = 60,
46
- # rate_limit_requests = 30,
47
- # rate_limit_input_tokens = 0,
48
- # rate_limit_output_tokens = 0,
49
- # msgs_keep_max = 25,
50
- # msgs_keep_start = 5,
51
- # msgs_keep_end = 10,
52
- # max_tool_response_length = 3000,
53
- # response_timeout_seconds = 60,
54
- code_exec_docker_enabled = True,
55
- # code_exec_docker_name = "agent-zero-exe",
56
- # code_exec_docker_image = "frdel/agent-zero-exe:latest",
57
- # code_exec_docker_ports = { "22/tcp": 50022 }
58
- # code_exec_docker_volumes = { files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"} }
59
- code_exec_ssh_enabled = True,
60
- # code_exec_ssh_addr = "localhost",
61
- # code_exec_ssh_port = 50022,
62
- # code_exec_ssh_user = "root",
63
- # code_exec_ssh_pass = "toor",
64
- # additional = {},
65
- )
66
-
67
- # create the first agent
68
- agent0 = Agent( number = 0, config = config )
69
-
70
- # start the chat loop
71
- chat(agent0)
72
-
73
-
15
# Main conversation loop
16
def chat(agent:Agent):
76
-
17
+
18
# start the conversation loop
19
while True:
20
# ask user for message
21
with input_lock:
22
timeout = agent.get_data("timeout") # how long the agent is willing to wait
23
if not timeout: # if agent wants to wait for user input forever
83
- PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ('alt_gr' for mic, 'e' to leave):")
24
+ PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ('e' to leave):")
25
import readline # this fixes arrow keys in terminal
85
- listener = keyboard.Listener(on_press=on_press)
86
- listener.start()
26
user_input = input("> ")
88
- listener.stop()
27
PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
28
29
else: # otherwise wait for user input with a timeout
92
- PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ({timeout}s timeout, 'w' to wait, 'alt_gr' for mic, 'e' to leave):")
30
+ PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ({timeout}s timeout, 'w' to wait, 'e' to leave):")
31
import readline # this fixes arrow keys in terminal
94
- listener = keyboard.Listener(on_press=on_press)
95
- listener.start()
32
+ # user_input = timed_input("> ", timeout=timeout)
33
user_input = timeout_input("> ", timeout=timeout)
97
- listener.stop()
34
+
35
if not user_input:
99
- user_input = read_file("prompts/fw.msg_timeout.md")
36
+ user_input = agent.read_prompt("fw.msg_timeout.md")
37
PrintStyle(font_color="white", padding=False).stream(f"{user_input}")
38
else:
39
user_input = user_input.strip()
40
if user_input.lower()=="w": # the user needs more time
104
- listener = keyboard.Listener(on_press=on_press)
105
- listener.start()
106
- user_input = input("> ")
107
- listener.stop()
108
- PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
41
+ user_input = input("> ").strip()
42
+ PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
43
+
44
+
45
46
# exit the conversation when the user types 'exit'
47
if user_input.lower() == 'e': break
@@ -115,8 +51,8 @@ def chat(agent:Agent):
51
52
# print agent0 response
53
PrintStyle(font_color="white",background_color="#1D8348", bold=True, padding=True).print(f"{agent.agent_name}: reponse:")
118
- PrintStyle(font_color="white").print(f"{assistant_response}")
119
- if (os.getenv('EDGE_TTS_MODEL')) : tts.speech(assistant_response)
54
+ PrintStyle(font_color="white").print(f"{assistant_response}")
55
+
56
57
# User intervention during agent streaming
58
def intervention():
@@ -132,10 +68,6 @@ def intervention():
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
135
-def on_press(key):
136
- if key == keyboard.Key.alt_gr:
137
- # Lancer le speech to text
138
- stt.record()
71
72
# Capture keyboard input to trigger user intervention
73
def capture_keys():
@@ -164,5 +96,6 @@ if __name__ == "__main__":
96
# Start the key capture thread for user intervention during agent streaming
97
threading.Thread(target=capture_keys, daemon=True).start()
98
167
- # Start the chat
168
- initialize()
\ No newline at end of file
99
+ # initialize and start the chat
100
+ agent0 = initialize()
101
+ chat(agent0)
\ No newline at end of file
run_ui.py
new
+192
@@ -0,0 +1,192 @@
1
+from functools import wraps
2
+import os
3
+from pathlib import Path
4
+import threading
5
+from flask import Flask, request, jsonify, Response
6
+from flask_basicauth import BasicAuth
7
+from agent import Agent
8
+from initialize import initialize
9
+from python.helpers.files import get_abs_path
10
+from python.helpers.print_style import PrintStyle
11
+from python.helpers.log import Log
12
+from dotenv import load_dotenv
13
+
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="/")
19
+
20
+# Set up basic authentication, name and password from .env variables
21
+app.config['BASIC_AUTH_USERNAME'] = os.environ.get('BASIC_AUTH_USERNAME') or "admin" #default name
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
31
+
32
+# Now you can use @requires_auth function decorator to require login on certain pages
33
+def requires_auth(f):
34
+ @wraps(f)
35
+ async def decorated(*args, **kwargs):
36
+ auth = request.authorization
37
+ if not auth or not (auth.username == app.config['BASIC_AUTH_USERNAME'] and auth.password == app.config['BASIC_AUTH_PASSWORD']):
38
+ return Response(
39
+ 'Could not verify your access level for that URL.\n'
40
+ 'You have to login with proper credentials', 401,
41
+ {'WWW-Authenticate': 'Basic realm="Login Required"'})
42
+ return await f(*args, **kwargs)
43
+ return decorated
44
+
45
+# handle default address, show demo html page from ./test_form.html
46
+@app.route('/', methods=['GET'])
47
+async def test_form():
48
+ return Path(get_abs_path("./webui/index.html")).read_text()
49
+
50
+# simple health check, just return OK to see the server is running
51
+@app.route('/ok', methods=['GET','POST'])
52
+async def health_check():
53
+ return "OK"
54
+
55
+# # secret page, requires authentication
56
+# @app.route('/secret', methods=['GET'])
57
+# @requires_auth
58
+# async def secret_page():
59
+# return Path("./secret_page.html").read_text()
60
+
61
+# send message to agent
62
+@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)
95
+
96
+# pausing/unpausing the agent
97
+@app.route('/pause', methods=['POST'])
98
+async def pause():
99
+ try:
100
+
101
+ #data sent to the server
102
+ input = request.get_json()
103
+ paused = input.get("paused", False)
104
+
105
+ Agent.paused = paused
106
+
107
+ response = {
108
+ "ok": True,
109
+ "message": "Agent paused." if paused else "Agent unpaused.",
110
+ "pause": paused
111
+ }
112
+
113
+ except Exception as e:
114
+ response = {
115
+ "ok": False,
116
+ "message": str(e),
117
+ }
118
+
119
+ #respond with json
120
+ return jsonify(response)
121
+
122
+# restarting with new agent0
123
+@app.route('/reset', methods=['POST'])
124
+async def reset():
125
+ try:
126
+
127
+ agent = get_agent(reset=True)
128
+ Log.reset()
129
+
130
+ response = {
131
+ "ok": True,
132
+ "message": "Agent restarted.",
133
+ }
134
+
135
+ except Exception as e:
136
+ response = {
137
+ "ok": False,
138
+ "message": str(e),
139
+ }
140
+
141
+ #respond with json
142
+ return jsonify(response)
143
+
144
+# Web UI polling
145
+@app.route('/poll', methods=['POST'])
146
+async def poll():
147
+ try:
148
+
149
+ #data sent to the server
150
+ input = request.get_json()
151
+ from_no = input.get("log_from", "")
152
+
153
+ logs = Log.logs[int(from_no):]
154
+ to = Log.last_updated #max(0, len(Log.logs)-1)
155
+
156
+ #data from this server
157
+ response = {
158
+ "ok": True,
159
+ "logs": logs,
160
+ "log_to": to,
161
+ "log_guid": Log.guid,
162
+ "log_version": Log.version,
163
+ "paused": Agent.paused
164
+ }
165
+
166
+ except Exception as e:
167
+ response = {
168
+ "ok": False,
169
+ "message": str(e),
170
+ }
171
+
172
+ #respond with json
173
+ return jsonify(response)
174
+
175
+
176
+
177
+#run the internal server
178
+if __name__ == "__main__":
179
+
180
+ load_dotenv()
181
+
182
+ get_agent() #initialize
183
+
184
+ # Suppress only request logs but keep the startup messages
185
+ from werkzeug.serving import WSGIRequestHandler
186
+ class NoRequestLoggingWSGIRequestHandler(WSGIRequestHandler):
187
+ def log_request(self, code='-', size='-'):
188
+ pass # Override to suppress request logging
189
+
190
+ # run the server on port from .env
191
+ port = int(os.environ.get("WEB_UI_PORT", 0)) or None
192
+ app.run(request_handler=NoRequestLoggingWSGIRequestHandler,port=port)
stt.py
deleted
-83
@@ -1,83 +0,0 @@
1
-import os, sys
2
-from dotenv import load_dotenv
3
-from groq import Groq
4
-import pyaudio
5
-import wave
6
-from pynput.keyboard import Key, Controller
7
-import numpy as np
8
-from python.helpers.print_style import PrintStyle
9
-
10
-load_dotenv() # take environment variables from.env.
11
-
12
-# Utility function to get API keys from environment variables
13
-def get_api_key(service):
14
- return os.getenv(f"API_KEY_{service.upper()}") or os.getenv(f"{service.upper()}_API_KEY")
15
-
16
-def record():
17
- client = Groq(
18
- api_key=get_api_key("groq")
19
- )
20
-
21
- CHUNK = 1024
22
- FORMAT = pyaudio.paInt16
23
- CHANNELS = 2
24
- RATE = 44100
25
- WAVE_OUTPUT_FILENAME = "tmp_stt.wav"
26
-
27
- p = pyaudio.PyAudio()
28
-
29
- stream = p.open(format=FORMAT,
30
- channels=CHANNELS,
31
- rate=RATE,
32
- input=True,
33
- frames_per_buffer=CHUNK)
34
-
35
- PrintStyle(background_color="red", font_color="white", bold=True, padding=True).print("Recording")
36
-
37
- frames = []
38
- silence_duration = 0
39
- silence_threshold = 0.5 # seuil de silence (en secondes)
40
- max_silence_duration = 3 # durée maximale de silence (en secondes)
41
-
42
- while True:
43
- data = stream.read(CHUNK)
44
- frames.append(data)
45
-
46
- # Analyse du signal audio pour détecter le silence
47
- audio_data = np.frombuffer(data, dtype=np.int16)
48
- audio_level = np.mean(np.abs(audio_data))
49
- if audio_level < 100: # seuil de détection de silence (à ajuster)
50
- silence_duration += CHUNK / RATE
51
- if silence_duration > max_silence_duration:
52
- break
53
- else:
54
- silence_duration = 0
55
-
56
- PrintStyle(background_color="Yellow", font_color="black", bold=True, padding=True).print("Translation")
57
-
58
- stream.stop_stream()
59
- stream.close()
60
- p.terminate()
61
-
62
- filename = os.path.dirname(__file__) + "/tmp/" + WAVE_OUTPUT_FILENAME
63
-
64
- wf = wave.open(filename, 'wb')
65
- wf.setnchannels(CHANNELS)
66
- wf.setsampwidth(p.get_sample_size(FORMAT))
67
- wf.setframerate(RATE)
68
- wf.writeframes(b''.join(frames))
69
- wf.close()
70
-
71
- with open(filename, "rb") as file:
72
- transcription = client.audio.transcriptions.create(
73
- file=(filename, file.read()),
74
- model="whisper-large-v3",
75
- prompt="Human talking to personnal ia agent", # Optional
76
- response_format="json", # Optional
77
- language="fr", # Optional
78
- temperature=0.0 # Optional
79
- )
80
- keyboard = Controller()
81
- PrintStyle(background_color="green", font_color="white", bold=True, padding=True).print("Done")
82
- keyboard.type(transcription.text)
83
- keyboard.press(Key.enter)
\ No newline at end of file
test.py
new
+12
@@ -0,0 +1,12 @@
1
+from python.helpers.dirty_json import DirtyJson
2
+
3
+
4
+json_string = """
5
+{"key1": "value1",
6
+ "key2": "value2",
7
+ "key3": "value3"
8
+}
9
+"""
10
+
11
+json = DirtyJson.parse_string(json_string)
12
+print(json)
\ No newline at end of file
tmp/.gitkeep
tmp/tmp_stt.wav
Binary files a/tmp/tmp_stt.wav and /dev/null differ
tts.py
deleted
-21
@@ -1,21 +0,0 @@
1
-from gradio_client import Client
2
-import os
3
-from dotenv import load_dotenv
4
-os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
5
-import pygame.mixer
6
-
7
-load_dotenv() # take environment variables from.env.
8
-client = Client("innoai/Edge-TTS-Text-to-Speech")
9
-pygame.mixer.init()
10
-
11
-def speech(text):
12
- speech = client.predict(
13
- text=text,
14
- voice=os.getenv('EDGE_TTS_MODEL'),
15
- rate=20,
16
- pitch=0,
17
- api_name="/predict"
18
- )
19
- pygame.mixer.music.load(speech[0]) # chargement de la musique
20
- pygame.mixer.music.play() # la musique est jouée
21
-
\ No newline at end of file
webui/index.css
new
+325
@@ -0,0 +1,325 @@
1
+body,
2
+html {
3
+ margin: 0;
4
+ padding: 0;
5
+ height: 100%;
6
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
7
+ background-color: #1e1e1e;
8
+ color: #e0e0e0;
9
+}
10
+.container {
11
+ display: flex;
12
+ height: 100%;
13
+}
14
+.panel {
15
+ height: 100%;
16
+ overflow: auto;
17
+}
18
+#left-panel {
19
+ width: 300px;
20
+ background-color: #2d2d2d;
21
+ border-right: 0.0625em solid #444; /* 1px */
22
+ padding: 1.25em; /* 20px */
23
+ box-sizing: border-box;
24
+ flex-shrink: 0; /* Ensure the left panel does not shrink */
25
+}
26
+#right-panel {
27
+ flex-grow: 1;
28
+ display: flex;
29
+ flex-direction: column;
30
+}
31
+.splitter {
32
+ width: 0.625em; /* 10px */
33
+ background-color: #444;
34
+ cursor: col-resize;
35
+ transition: background-color 0.3s;
36
+ flex-shrink: 0; /* Prevents the splitter from shrinking */
37
+}
38
+.splitter:hover {
39
+ background-color: #666;
40
+}
41
+h2,
42
+h3 {
43
+ margin: 1.25em 0 0.625em; /* 20px 0 10px */
44
+ color: #bb86fc;
45
+}
46
+h4 {
47
+ margin: 0.2em;
48
+}
49
+#chat-history {
50
+ flex-grow: 1;
51
+ overflow-y: auto;
52
+ padding: 1.25em; /* 20px */
53
+ display: flex;
54
+ flex-direction: column;
55
+}
56
+.message-container {
57
+ margin-bottom: 0.5em;
58
+ animation: fadeIn 0.5s;
59
+}
60
+.message {
61
+ padding: 0.625em 0.9375em; /* 10px 15px */
62
+ border-radius: 1.125em; /* 18px */
63
+}
64
+@keyframes fadeIn {
65
+ from {
66
+ opacity: 0;
67
+ transform: translateY(0.625em); /* 10px */
68
+ }
69
+ to {
70
+ opacity: 1;
71
+ transform: translateY(0);
72
+ }
73
+}
74
+.user-container {
75
+ align-self: flex-end;
76
+ margin-top: 2em;
77
+ margin-bottom: 1.5em;
78
+}
79
+
80
+.ai-container {
81
+ align-self: flex-start;
82
+}
83
+
84
+.message-user {
85
+ background-color: #4a4a4a;
86
+ border-bottom-right-radius: 0.3125em; /* 5px */
87
+}
88
+
89
+.message-ai {
90
+ border-bottom-left-radius: 0.3125em; /* 5px */
91
+}
92
+
93
+.message-fw{
94
+ margin-left: 2em;
95
+ border-radius: 1.125em; /* 18px */
96
+ border-top-left-radius: 0.3125em; /* 5px */
97
+}
98
+
99
+.message-default {
100
+ background-color: #002d3b;
101
+}
102
+
103
+.message-agent {
104
+ margin-top: 2em;
105
+ background-color: #003444;
106
+}
107
+
108
+.message-agent-response {
109
+ background-color: #1f5327;
110
+}
111
+
112
+.message-agent-delegation {
113
+ background-color: #1f534a;
114
+}
115
+
116
+.message-tool {
117
+ background-color: #001b4b;
118
+}
119
+
120
+.message-code-exe {
121
+ background-color: #2f1a60;
122
+}
123
+
124
+.message-adhoc {
125
+ background-color: #124850;
126
+}
127
+
128
+.message-info {
129
+ background-color: #2d2d2d;
130
+}
131
+
132
+.message-warning {
133
+ background-color: #826100;
134
+}
135
+
136
+.message-error {
137
+ background-color: #5e1212;
138
+}
139
+
140
+.agent-start {
141
+ font-size: 0.8em;
142
+ color: #ffffff;
143
+ margin-bottom: 0.3125em; /* 5px */
144
+ opacity: 0.7;
145
+}
146
+.ai-info {
147
+ font-size: 0.9em;
148
+ margin-bottom: 0.625em; /* 10px */
149
+ border-collapse: collapse;
150
+ width: 100%;
151
+}
152
+.ai-info th,
153
+.ai-info td {
154
+ text-align: left;
155
+ padding: 0.25em; /* 4px */
156
+ border-bottom: 0.0625em solid rgba(255, 255, 255, 0.1); /* 1px */
157
+}
158
+.ai-info th {
159
+ color: #bb86fc;
160
+ width: 40%;
161
+}
162
+.message-actions {
163
+ margin-top: 0.3125em; /* 5px */
164
+ font-size: 0.8em;
165
+ color: #ffffff;
166
+}
167
+.message-action {
168
+ cursor: pointer;
169
+ opacity: 0.7;
170
+ transition: opacity 0.3s;
171
+}
172
+.message-action:hover {
173
+ opacity: 1;
174
+}
175
+#input-section {
176
+ display: flex;
177
+ padding: 1.25em; /* 20px */
178
+ background-color: #2d2d2d;
179
+}
180
+#chat-input {
181
+ flex-grow: 1;
182
+ resize: none;
183
+ /* min-height: 3.125em; 50px */
184
+ max-height: 9.375em; /* 150px */
185
+ padding: 0.625em 0.9375em; /* 10px 15px */
186
+ border: none;
187
+ border-radius: 1.5625em; /* 25px */
188
+ background-color: #3d3d3d;
189
+ color: #e0e0e0;
190
+ font-size: 1em; /* 16px */
191
+ transition: background-color 0.3s;
192
+}
193
+#chat-input:focus {
194
+ outline: none;
195
+ background-color: #4d4d4d;
196
+}
197
+.chat-button {
198
+ color: #1e1e1e;
199
+ border: none;
200
+ border-radius: 50%;
201
+ width: 3.125em; /* 50px */
202
+ height: 3.125em; /* 50px */
203
+ margin-left: 0.625em; /* 10px */
204
+ cursor: pointer;
205
+ transition: background-color 0.3s, transform 0.1s;
206
+}
207
+.chat-button:active {
208
+ transform: scale(0.95);
209
+}
210
+
211
+#send-button {
212
+ background-color: #bb86fc;
213
+}
214
+#send-button:hover {
215
+ background-color: #c397ff;
216
+}
217
+
218
+.pause-button{
219
+ background-color: #3270e2;
220
+}
221
+.pause-button:hover{
222
+ background-color: #3270e2;
223
+}
224
+
225
+.config-section {
226
+ margin-bottom: 1.25em; /* 20px */
227
+}
228
+.config-button {
229
+ background-color: #3700b3;
230
+ color: #ffffff;
231
+ border: none;
232
+ padding: 0.5em 0.75em; /* 8px 12px */
233
+ margin: 0.3125em; /* 5px */
234
+ border-radius: 0.3125em; /* 5px */
235
+ cursor: pointer;
236
+ transition: background-color 0.3s;
237
+}
238
+.config-button:hover {
239
+ background-color: #5c2bd1;
240
+}
241
+.config-list {
242
+ list-style-type: none;
243
+ padding: 0;
244
+}
245
+.config-list li {
246
+ display: flex;
247
+ justify-content: space-between;
248
+ align-items: center;
249
+ padding: 0.3125em 0; /* 5px 0 */
250
+ border-bottom: 0.0625em solid #444; /* 1px */
251
+}
252
+.edit-button {
253
+ background-color: transparent;
254
+ color: #bb86fc;
255
+ border: 0.0625em solid #bb86fc; /* 1px */
256
+ padding: 0.125em 0.5em; /* 2px 8px */
257
+ border-radius: 0.1875em; /* 3px */
258
+ cursor: pointer;
259
+ transition: background-color 0.3s;
260
+}
261
+.edit-button:hover {
262
+ background-color: rgba(187, 134, 252, 0.2);
263
+}
264
+.switch {
265
+ position: relative;
266
+ display: inline-block;
267
+ width: 3.125em; /* 50px */
268
+ height: 1.5em; /* 24px */
269
+}
270
+.switch input {
271
+ opacity: 0;
272
+ width: 0;
273
+ height: 0;
274
+}
275
+.slider {
276
+ position: absolute;
277
+ cursor: pointer;
278
+ top: 0;
279
+ left: 0;
280
+ right: 0;
281
+ bottom: 0;
282
+ background-color: #4a4a4a;
283
+ transition: 0.4s;
284
+ border-radius: 1.5em; /* 24px */
285
+}
286
+.slider:before {
287
+ position: absolute;
288
+ content: "";
289
+ height: 1em; /* 16px */
290
+ width: 1em; /* 16px */
291
+ left: 0.25em; /* 4px */
292
+ bottom: 0.25em; /* 4px */
293
+ background-color: white;
294
+ transition: 0.4s;
295
+ border-radius: 50%;
296
+}
297
+input:checked + .slider {
298
+ background-color: #bb86fc;
299
+}
300
+input:checked + .slider:before {
301
+ transform: translateX(1.625em); /* 26px */
302
+}
303
+
304
+.kvps-key {
305
+ font-weight: bold;
306
+}
307
+
308
+.kvps-val {
309
+ white-space: pre-wrap;
310
+}
311
+
312
+.msg-json{
313
+ display:none;
314
+}
315
+
316
+.msg-thoughts{
317
+}
318
+
319
+.connected{
320
+color: #6ec583;
321
+}
322
+
323
+.disconnected{
324
+ color: #d87979;
325
+}
\ No newline at end of file
webui/index.html
new
+99
@@ -0,0 +1,99 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+
4
+<head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Agent Zero</title>
8
+ <link rel="stylesheet" href="index.css">
9
+ <script>
10
+ window.safeCall = function(name,...args){
11
+ if (window[name]) window[name](...args)
12
+ }
13
+ </script>
14
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
15
+ <script type="module" src="index.js"></script>
16
+
17
+</head>
18
+
19
+<body>
20
+ <div class="container">
21
+ <div id="left-panel" class="panel">
22
+ <!-- <h2>Configuration</h2> -->
23
+
24
+ <div class="config-section" id="status-section" x-data="{ connected: true }">
25
+ <h3>Status</h3>
26
+ <h4 class="connected" x-show="connected">✔ Connected</h4>
27
+ <h4 class="disconnected" x-show="!connected">✖ Disconnected</h4>
28
+ </div>
29
+
30
+ <div class="config-section" x-data="{ showQuickActions: true }">
31
+ <h3>Quick Actions</h3>
32
+ <button class="config-button" id="resetChat" @click="resetChat()">New Chat</button>
33
+ </div>
34
+ <!--
35
+ <div class="config-section">
36
+ <h3>Model Settings</h3>
37
+ <ul class="config-list">
38
+ <li>
39
+ <span>GPT-4</span>
40
+ <button class="edit-button">Edit</button>
41
+ </li>
42
+ <li>
43
+ <span>Temperature: 0.7</span>
44
+ <button class="edit-button">Edit</button>
45
+ </li>
46
+ <li>
47
+ <span>Max Tokens: 2048</span>
48
+ <button class="edit-button">Edit</button>
49
+ </li>
50
+ </ul>
51
+ </div>
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">
59
+ <input type="checkbox" x-model="autoScroll" x-effect="window.safeCall('toggleAutoScroll',autoScroll)">
60
+ <span class="slider"></span>
61
+ </label>
62
+ </li>
63
+
64
+ <li x-data="{ showThoughts: true }">
65
+ <span>Show thoughts</span>
66
+ <label class="switch">
67
+ <input type="checkbox" x-model="showThoughts" x-effect="window.safeCall('toggleThoughts',showThoughts)">
68
+ <span class="slider"></span>
69
+ </label>
70
+ </li>
71
+
72
+ <li x-data="{ showJson: false }">
73
+ <span>Show JSON</span>
74
+ <label class="switch">
75
+ <input type="checkbox" x-model="showJson" x-effect="window.safeCall('toggleJson',showJson)">
76
+ <span class="slider"></span>
77
+ </label>
78
+ </li>
79
+
80
+ </ul>
81
+ </div>
82
+ </div>
83
+ <div class="splitter" id="splitter"></div>
84
+ <div id="right-panel" class="panel">
85
+ <!--<h2>Chat</h2>-->
86
+ <div id="chat-history">
87
+
88
+ </div>
89
+ <div id="input-section" x-data="{ paused: false }">
90
+ <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
91
+ <button class="chat-button" id="send-button">➤</button>
92
+ <button class="chat-button pause-button" id="pause-button" @click="pauseAgent(true)" x-show="!paused">⏸</button>
93
+ <button class="chat-button pause-button" id="unpause-button" @click="pauseAgent(false)" x-show="paused">▶</button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+</body>
98
+
99
+</html>
\ No newline at end of file
webui/index.js
new
+192
@@ -0,0 +1,192 @@
1
+import * as msgs from "./messages.js"
2
+
3
+const splitter = document.getElementById('splitter');
4
+const leftPanel = document.getElementById('left-panel');
5
+const container = document.querySelector('.container');
6
+const chatInput = document.getElementById('chat-input');
7
+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
+
12
+let isResizing = false;
13
+let autoScroll = true;
14
+
15
+
16
+splitter.addEventListener('mousedown', (e) => {
17
+ isResizing = true;
18
+ document.addEventListener('mousemove', resize);
19
+ document.addEventListener('mouseup', stopResize);
20
+});
21
+
22
+function resize(e) {
23
+ if (isResizing) {
24
+ const newWidth = e.clientX - container.offsetLeft;
25
+ leftPanel.style.width = `${newWidth}px`;
26
+ }
27
+}
28
+
29
+function stopResize() {
30
+ isResizing = false;
31
+ document.removeEventListener('mousemove', resize);
32
+}
33
+
34
+async function sendMessage() {
35
+ const message = chatInput.value.trim();
36
+ if (message) {
37
+
38
+ const response = await sendJsonData("/msg", { text: message });
39
+
40
+ //setMessage('user', message);
41
+ chatInput.value = '';
42
+ adjustTextareaHeight();
43
+ }
44
+}
45
+
46
+chatInput.addEventListener('keydown', (e) => {
47
+ if (e.key === 'Enter' && !e.shiftKey) {
48
+ e.preventDefault();
49
+ sendMessage();
50
+ }
51
+});
52
+
53
+sendButton.addEventListener('click', sendMessage);
54
+
55
+function setMessage(id, type, heading, content, kvps = null) {
56
+ // Search for the existing message container by id
57
+ let messageContainer = document.getElementById(`message-${id}`);
58
+
59
+ if (messageContainer) {
60
+ // Clear the existing container's content if found
61
+ messageContainer.innerHTML = '';
62
+ } else {
63
+ // Create a new container if not found
64
+ const sender = type === 'user' ? 'user' : 'ai';
65
+ messageContainer = document.createElement('div');
66
+ messageContainer.id = `message-${id}`;
67
+ messageContainer.classList.add('message-container', `${sender}-container`);
68
+ }
69
+
70
+ const handler = msgs.getHandler(type);
71
+ handler(messageContainer, id, type, heading, content, kvps);
72
+
73
+ // If the container was found, it was already in the DOM, no need to append again
74
+ if (!document.getElementById(`message-${id}`)) {
75
+ chatHistory.appendChild(messageContainer);
76
+ }
77
+
78
+ if(autoScroll) chatHistory.scrollTop = chatHistory.scrollHeight;
79
+}
80
+
81
+
82
+function adjustTextareaHeight() {
83
+ chatInput.style.height = 'auto';
84
+ chatInput.style.height = (chatInput.scrollHeight) + 'px';
85
+}
86
+
87
+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
+ }
99
+
100
+ const jsonResponse = await response.json();
101
+ return jsonResponse;
102
+}
103
+
104
+let lastLog = 0;
105
+let lastLogVersion = 0;
106
+let lastLogGuid = ""
107
+
108
+async function poll() {
109
+ try{
110
+ const response = await sendJsonData("/poll", { log_from: lastLog });
111
+ // console.log(response)
112
+
113
+ if (response.ok) {
114
+
115
+ if (lastLogGuid != response.log_guid) {
116
+ chatHistory.innerHTML = ""
117
+ }
118
+
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);
122
+ }
123
+ }
124
+
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;
130
+
131
+ lastLog = response.log_to;
132
+ lastLogVersion = response.log_version;
133
+ lastLogGuid = response.log_guid;
134
+ }
135
+
136
+ } catch (error) {
137
+ console.error('Error:', error);
138
+ const statusAD = Alpine.$data(statusSection);
139
+ statusAD.connected = false;
140
+ }
141
+}
142
+
143
+window.pauseAgent = async function (paused) {
144
+ const resp = await sendJsonData("/pause", { paused: paused });
145
+}
146
+
147
+window.resetChat = async function () {
148
+ const resp = await sendJsonData("/reset", {});
149
+}
150
+
151
+window.toggleAutoScroll = async function (_autoScroll) {
152
+ autoScroll = _autoScroll;
153
+}
154
+
155
+window.toggleJson = async function (showJson) {
156
+ // add display:none to .msg-json class definition
157
+ toggleCssProperty('.msg-json', 'display', showJson ? 'block' : 'none');
158
+}
159
+
160
+window.toggleThoughts = async function (showThoughts) {
161
+ // add display:none to .msg-json class definition
162
+ toggleCssProperty('.msg-thoughts', 'display', showThoughts ? undefined : 'none');
163
+}
164
+
165
+
166
+function toggleCssProperty(selector, property, value) {
167
+ // Get the stylesheet that contains the class
168
+ const styleSheets = document.styleSheets;
169
+
170
+ // Iterate through all stylesheets to find the class
171
+ for (let i = 0; i < styleSheets.length; i++) {
172
+ const styleSheet = styleSheets[i];
173
+ const rules = styleSheet.cssRules || styleSheet.rules;
174
+
175
+ for (let j = 0; j < rules.length; j++) {
176
+ const rule = rules[j];
177
+ if (rule.selectorText == selector) {
178
+ // Check if the property is already applied
179
+ if (value===undefined) {
180
+ rule.style.removeProperty(property); // Remove the property
181
+ } else {
182
+ rule.style.setProperty(property, value); // Add the property (you can customize the value)
183
+ }
184
+ return;
185
+ }
186
+ }
187
+ }
188
+}
189
+
190
+chatInput.addEventListener('input', adjustTextareaHeight);
191
+
192
+setInterval(poll, 250);
\ No newline at end of file
webui/messages.js
new
+275
@@ -0,0 +1,275 @@
1
+
2
+
3
+
4
+export function getHandler(type) {
5
+ switch (type) {
6
+ case 'user':
7
+ return drawMessageUser;
8
+ case 'agent':
9
+ return drawMessageAgent;
10
+ case 'response':
11
+ return drawMessageResponse;
12
+ case 'tool':
13
+ return drawMessageTool;
14
+ case 'code_exe':
15
+ return drawMessageCodeExe;
16
+ case 'warning':
17
+ return drawMessageWarning;
18
+ case 'rate_limit':
19
+ return drawMessageWarning;
20
+ case 'error':
21
+ return drawMessageError;
22
+ case 'info':
23
+ return drawMessageInfo;
24
+ case 'adhoc':
25
+ return drawMessageAdhoc;
26
+ case 'hint':
27
+ return drawMessageInfo;
28
+ default:
29
+ return drawMessageDefault;
30
+ }
31
+}
32
+
33
+export function drawMessageDefault(messageContainer, id, type, heading, content, kvps = null) {
34
+
35
+ // if (type !== 'user') {
36
+ // const agentStart = document.createElement('div');
37
+ // agentStart.classList.add('agent-start');
38
+ // agentStart.textContent = 'Agent 0 starts a message...';
39
+ // messageContainer.appendChild(agentStart);
40
+ // }
41
+
42
+ const messageDiv = document.createElement('div');
43
+ messageDiv.classList.add('message', 'message-ai', 'message-default');
44
+
45
+ if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
46
+
47
+ drawKvps(messageDiv, kvps);
48
+
49
+ const textNode = document.createElement('pre');
50
+ textNode.textContent = content;
51
+ textNode.style.whiteSpace = 'pre-wrap';
52
+ textNode.style.wordBreak = 'break-word';
53
+ textNode.classList.add("msg-json");
54
+ messageDiv.appendChild(textNode);
55
+
56
+ messageContainer.appendChild(messageDiv);
57
+
58
+ // if (type !== 'user') {
59
+ // const actions = document.createElement('div');
60
+ // actions.classList.add('message-actions');
61
+ // actions.innerHTML = '<span class="message-action">Copy</span> · <span class="message-action">Retry</span> · <span class="message-action">Edit</span>';
62
+ // messageContainer.appendChild(actions);
63
+ // }
64
+
65
+}
66
+
67
+export function drawMessageAgent(messageContainer, id, type, heading, content, kvps = null) {
68
+
69
+
70
+ // if (kvps && kvps['tool_name'] === 'response') {
71
+ // drawMessageResponse(messageContainer, id, type, heading, content, kvps)
72
+ // return
73
+ // }
74
+
75
+ const messageDiv = document.createElement('div');
76
+ messageDiv.classList.add('message', 'message-ai', 'message-agent');
77
+
78
+ if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
79
+
80
+ if (kvps) {
81
+ const kvpsFlat = { ...kvps, ...kvps['tool_args'] || {} }
82
+ delete kvpsFlat['tool_args']
83
+ drawKvps(messageDiv, kvpsFlat);
84
+ }
85
+
86
+ const textNode = document.createElement('pre');
87
+ textNode.textContent = content;
88
+ textNode.style.whiteSpace = 'pre-wrap';
89
+ textNode.style.wordBreak = 'break-word';
90
+ textNode.classList.add("msg-json");
91
+ messageDiv.appendChild(textNode);
92
+
93
+ messageContainer.appendChild(messageDiv);
94
+
95
+ // const actions = document.createElement('div');
96
+ // actions.classList.add('message-actions');
97
+ // actions.innerHTML = '<span class="message-action">Copy</span> · <span class="message-action">Retry</span> · <span class="message-action">Edit</span>';
98
+ // messageContainer.appendChild(actions);
99
+
100
+}
101
+
102
+export function drawMessageResponse(messageContainer, id, type, heading, content, kvps = null) {
103
+
104
+ const messageDiv = document.createElement('div');
105
+ messageDiv.classList.add('message', 'message-ai', 'message-agent-response', 'message-fw');
106
+
107
+ if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
108
+
109
+ const textNode = document.createElement('pre');
110
+ textNode.textContent = content;
111
+ textNode.style.whiteSpace = 'pre-wrap';
112
+ textNode.style.wordBreak = 'break-word';
113
+ messageDiv.appendChild(textNode);
114
+
115
+ messageContainer.appendChild(messageDiv);
116
+}
117
+
118
+export function drawMessageDelegation(messageContainer, id, type, heading, content, kvps = null) {
119
+
120
+ const messageDiv = document.createElement('div');
121
+ messageDiv.classList.add('message', 'message-ai', 'message-agent', 'message-fw', 'message-agent-delegation');
122
+
123
+ if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
124
+
125
+ const pars = kvps && kvps["tool_args"] ? kvps["tool_args"] : { text: "" }
126
+
127
+ drawKvps(messageDiv, { "Thoughts": kvps["thoughts"], "Message": pars["text"], "Reset": pars["reset"] });
128
+
129
+
130
+ const textNode = document.createElement('pre');
131
+ textNode.textContent = content;
132
+ textNode.style.whiteSpace = 'pre-wrap';
133
+ textNode.style.wordBreak = 'break-word';
134
+ messageDiv.appendChild(textNode);
135
+
136
+ messageContainer.appendChild(messageDiv);
137
+}
138
+
139
+export function drawMessageUser(messageContainer, id, type, heading, content, kvps = null) {
140
+
141
+ const messageDiv = document.createElement('div');
142
+ messageDiv.classList.add('message', 'message-user');
143
+
144
+ drawKvps(messageDiv, kvps);
145
+
146
+ const textNode = document.createElement('pre');
147
+ textNode.textContent = content;
148
+ textNode.style.whiteSpace = 'pre-wrap';
149
+ textNode.style.wordBreak = 'break-word';
150
+ messageDiv.appendChild(textNode);
151
+
152
+ messageContainer.appendChild(messageDiv);
153
+}
154
+
155
+
156
+export function drawMessageTool(messageContainer, id, type, heading, content, kvps = null) {
157
+
158
+ const messageDiv = document.createElement('div');
159
+ messageDiv.classList.add('message', 'message-ai', 'message-tool', 'message-fw');
160
+
161
+ if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
162
+
163
+ drawKvps(messageDiv, kvps);
164
+
165
+ const textNode = document.createElement('pre');
166
+ textNode.textContent = content;
167
+ textNode.style.whiteSpace = 'pre-wrap';
168
+ textNode.style.wordBreak = 'break-word';
169
+ textNode.classList.add("msg-output");
170
+ messageDiv.appendChild(textNode);
171
+
172
+ messageContainer.appendChild(messageDiv);
173
+
174
+ // const actions = document.createElement('div');
175
+ // actions.classList.add('message-actions');
176
+ // actions.innerHTML = '<span class="message-action">Copy output</span>';
177
+ // messageContainer.appendChild(actions);
178
+
179
+}
180
+
181
+export function drawMessageCodeExe(messageContainer, id, type, heading, content, kvps = null) {
182
+
183
+ const messageDiv = document.createElement('div');
184
+ messageDiv.classList.add('message', 'message-ai', 'message-code-exe', 'message-fw');
185
+
186
+ if (heading) messageDiv.appendChild(document.createElement('h4')).textContent = heading
187
+
188
+ drawKvps(messageDiv, kvps);
189
+
190
+ const textNode = document.createElement('pre');
191
+ textNode.textContent = content;
192
+ textNode.style.whiteSpace = 'pre-wrap';
193
+ textNode.style.wordBreak = 'break-word';
194
+ messageDiv.appendChild(textNode);
195
+
196
+ messageContainer.appendChild(messageDiv);
197
+
198
+ // const actions = document.createElement('div');
199
+ // actions.classList.add('message-actions');
200
+ // actions.innerHTML = '<span class="message-action">Copy code</span> · <span class="message-action">Copy output</span>';
201
+ // messageContainer.appendChild(actions);
202
+}
203
+
204
+export function drawMessageAgentPlain(classes, messageContainer, id, type, heading, content, kvps = null) {
205
+
206
+ // const agentStart = document.createElement('div');
207
+ // agentStart.classList.add('agent-start');
208
+ // agentStart.textContent = 'System warning...';
209
+ // messageContainer.appendChild(agentStart);
210
+
211
+ const messageDiv = document.createElement('div');
212
+ messageDiv.classList.add('message', 'message-ai', ...classes);
213
+
214
+ drawKvps(messageDiv, kvps);
215
+
216
+ const textNode = document.createElement('pre');
217
+ textNode.textContent = content;
218
+ textNode.style.whiteSpace = 'pre-wrap';
219
+ textNode.style.wordBreak = 'break-word';
220
+ messageDiv.appendChild(textNode);
221
+
222
+ messageContainer.appendChild(messageDiv);
223
+
224
+}
225
+
226
+export function drawMessageAdhoc(messageContainer, id, type, heading, content, kvps = null) {
227
+ return drawMessageAgentPlain(['message-adhoc'], messageContainer, id, type, heading, content, kvps);
228
+}
229
+
230
+export function drawMessageInfo(messageContainer, id, type, heading, content, kvps = null) {
231
+ return drawMessageAgentPlain(['message-info'], messageContainer, id, type, heading, content, kvps);
232
+}
233
+
234
+export function drawMessageWarning(messageContainer, id, type, heading, content, kvps = null) {
235
+ return drawMessageAgentPlain(['message-warning'], messageContainer, id, type, heading, content, kvps);
236
+}
237
+
238
+export function drawMessageError(messageContainer, id, type, heading, content, kvps = null) {
239
+ return drawMessageAgentPlain(['message-error'], messageContainer, id, type, heading, content, kvps);
240
+}
241
+
242
+function drawKvps(container, kvps) {
243
+ if (kvps) {
244
+ const table = document.createElement('table');
245
+ table.classList.add('ai-info');
246
+ for (let [key, value] of Object.entries(kvps)) {
247
+ const row = table.insertRow();
248
+ if (key == "thoughts") row.classList.add('msg-thoughts');
249
+
250
+ const th = row.insertCell();
251
+ th.textContent = convertToTitleCase(key);
252
+ th.classList.add('kvps-key');
253
+
254
+ const td = row.insertCell();
255
+ const pre = document.createElement('pre');
256
+
257
+ // if value is array, join it with new line
258
+ if (Array.isArray(value)) value = value.join('\n');
259
+
260
+ pre.textContent = value;
261
+ pre.classList.add('kvps-val');
262
+ td.appendChild(pre);
263
+ }
264
+ container.appendChild(table);
265
+ }
266
+}
267
+
268
+function convertToTitleCase(str) {
269
+ return str
270
+ .replace(/_/g, ' ') // Replace underscores with spaces
271
+ .toLowerCase() // Convert the entire string to lowercase
272
+ .replace(/\b\w/g, function (match) {
273
+ return match.toUpperCase(); // Capitalize the first letter of each word
274
+ });
275
+}
\ No newline at end of file