Memory extensions, UI updates

Auto memory extensions for main and solutions areas UI updates, error toasts, progress bar

frdel committed Sep 26, 2024 at 17:11 UTC d233e0c94fa3590f6382f5ce4d35527eafa8f722
41 files changed +1127 -928
.gitignore
+12 -3
@@ -26,7 +26,16 @@ tmp/*
26 # But do not ignore the directory itself
27 !tmp/.gitkeep
28
29 -# Ignore all contents of the directory "knowledge"
29 +# Ignore everything in the "knowledge" directory
30 knowledge/*
31 -# But do not ignore the directory itself
32 -!knowledge/.gitkeep
\ No newline at end of file
31 +
32 +# Do not ignore subdirectories (so we can track .gitkeep)
33 +!knowledge/*/
34 +
35 +# Ignore all files within subdirectories (except .gitkeep)
36 +knowledge/**/*.*
37 +!knowledge/**/.gitkeep
38 +
39 +# Explicitly allow the default folder and its contents
40 +!knowledge/default/
41 +!knowledge/default/**
\ No newline at end of file
.vscode/settings.json
-1
@@ -1,4 +1,3 @@
1 {
2 "python.analysis.typeCheckingMode": "standard",
3 - "deno.enable": true
3 }
\ No newline at end of file
agent.py
+34 -30
@@ -14,6 +14,7 @@ from langchain_core.embeddings import Embeddings
14 import python.helpers.log as Log
15 from python.helpers.dirty_json import DirtyJson
16 from python.helpers.defer import DeferredTask
17 +from typing import Callable
18
19
20 class AgentContext:
@@ -90,7 +91,7 @@ class AgentConfig:
91 embeddings_model: Embeddings
92 prompts_subdir: str = ""
93 memory_subdir: str = ""
93 - knowledge_subdir: str = ""
94 + knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"])
95 auto_memory_count: int = 3
96 auto_memory_skip: int = 2
97 rate_limit_seconds: int = 60
@@ -162,29 +163,31 @@ class Agent:
163 async def monologue(self, msg: str):
164 try:
165
165 - loop_data: dict[str, Any] = {"message": msg, "iteration": -1, "history_from": len(self.history) }
166 + # loop data dictionary to pass to extensions
167 + loop_data: dict[str, Any] = {
168 + "message": msg,
169 + "iteration": -1,
170 + "history_from": len(self.history),
171 + }
172
167 - await self.call_extensions(
168 - "monologue_start", loop_data=loop_data
169 - ) # call monologue_start extensions
173 + # call monologue_start extensions
174 + await self.call_extensions("monologue_start", loop_data=loop_data)
175
176 printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
177 user_message = self.read_prompt("fw.user_message.md", message=msg)
178 await self.append_message(user_message, human=True)
179
180 await self.call_extensions(
176 - "monologue_start", message=msg
181 + "monologue_start", loop_data=loop_data
182 ) # call monologue_end extensions
183
179 - while (
180 - True
181 - ): # let the agent iterate on his thoughts until he stops by using a tool
184 + # let the agent run message loop until he stops it with a response tool
185 + while True:
186
187 self.context.streaming_agent = self # mark self as current streamer
188 agent_response = ""
189 loop_data["iteration"] += 1
190
187 -
191 try:
192
193 # set system prompt and message history
@@ -219,9 +222,9 @@ class Agent:
222 font_color="green",
223 padding=True,
224 background_color="white",
222 - ).print(f"{self.agent_name}: Generating:")
225 + ).print(f"{self.agent_name}: Generating")
226 log = self.context.log.log(
224 - type="agent", heading=f"{self.agent_name}: Generating:"
227 + type="agent", heading=f"{self.agent_name}: Generating"
228 )
229
230 async for chunk in chain.astream(loop_data["history"]):
@@ -346,7 +349,7 @@ class Agent:
349 return "\n".join([f"{msg.type}: {msg.content}" for msg in messages])
350
351 async def call_utility_llm(
349 - self, system: str, msg: str, log_type: Log.Type = "util", output_label: str = ""
352 + self, system: str, msg: str, callback: Callable[[str], None] | None = None
353 ):
354 prompt = ChatPromptTemplate.from_messages(
355 [SystemMessage(content=system), HumanMessage(content=msg)]
@@ -354,17 +357,6 @@ class Agent:
357
358 chain = prompt | self.config.utility_model
359 response = ""
357 - printer = None
358 - logger = None
359 -
360 - if output_label:
361 - PrintStyle(
362 - bold=True, font_color="orange", padding=True, background_color="white"
363 - ).print(f"{self.agent_name}: {output_label}:")
364 - printer = PrintStyle(italic=True, font_color="orange", padding=False)
365 - logger = self.context.log.log(
366 - type=log_type, heading=f"{self.agent_name}: {output_label}:"
367 - )
360
361 formatted_inputs = prompt.format()
362 tokens = int(len(formatted_inputs) / 4)
@@ -380,11 +372,10 @@ class Agent:
372 else:
373 content = str(chunk)
374
383 - if printer:
384 - printer.stream(content)
375 + if callback:
376 + callback(content)
377 +
378 response += content
386 - if logger:
387 - logger.update(content=response)
379
380 self.rate_limiter.set_output_tokens(int(len(response) / 4))
381
@@ -396,10 +387,23 @@ class Agent:
387
388 async def replace_middle_messages(self, middle_messages):
389 cleanup_prompt = self.read_prompt("fw.msg_cleanup.md")
390 + log_item = self.context.log.log(
391 + type="util", heading="Mid messages cleanup summary"
392 + )
393 +
394 + PrintStyle(
395 + bold=True, font_color="orange", padding=True, background_color="white"
396 + ).print(f"{self.agent_name}: Mid messages cleanup summary")
397 + printer = PrintStyle(italic=True, font_color="orange", padding=False)
398 +
399 + def log_callback(content):
400 + printer.print(content)
401 + log_item.stream(content=content)
402 +
403 summary = await self.call_utility_llm(
404 system=cleanup_prompt,
405 msg=self.concat_messages(middle_messages),
402 - output_label="Mid messages cleanup summary",
406 + callback=log_callback,
407 )
408 new_human_message = HumanMessage(content=summary)
409 return [new_human_message]
@@ -473,7 +477,7 @@ class Agent:
477 await self.append_message(msg, human=True)
478 PrintStyle(font_color="red", padding=True).print(msg)
479 self.context.log.log(
476 - type="error", content=f"{self.agent_name}: Message misformat:"
480 + type="error", content=f"{self.agent_name}: Message misformat"
481 )
482
483 def log_from_stream(self, stream: str, logItem: Log.LogItem):
example.env
+4 -2
@@ -2,9 +2,10 @@ API_KEY_OPENAI=
2 API_KEY_ANTHROPIC=
3 API_KEY_GROQ=
4 API_KEY_PERPLEXITY=
5 -GOOGLE_API_KEY=
6 -MISTRAL_API_KEY=
5 +API_KEY_GOOGLE=
6 +API_KEY_MISTRAL=
7 API_KEY_OPENROUTER=
8 +API_KEY_SAMBANOVA=
9
10 API_KEY_OPENAI_AZURE=
11 OPENAI_AZURE_ENDPOINT=
@@ -22,3 +23,4 @@ PYDEVD_DISABLE_FILE_VALIDATION=1
23 OLLAMA_BASE_URL="http://127.0.0.1:11434"
24 LM_STUDIO_BASE_URL="http://127.0.0.1:1234/v1"
25 OPEN_ROUTER_BASE_URL="https://openrouter.ai/api/v1"
26 +SAMBANOVA_BASE_URL="https://fast-api.snova.ai/v1"
\ No newline at end of file
initialize.py
+5 -4
@@ -7,15 +7,16 @@ def initialize():
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="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF", temperature=0)
10 - # chat_llm = models.get_openrouter_chat(model_name="mattshumer/reflection-70b:free")
10 + # chat_llm = models.get_openrouter_chat(model_name="openai/o1-mini-2024-09-12")
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_mistral_chat(model_name="mistral-small-latest", temperature=0)
15 # chat_llm = models.get_groq_chat(model_name="llama-3.1-70b-versatile", temperature=0)
16 -
16 + # chat_llm = models.get_sambanova_chat(model_name="Meta-Llama-3.1-70B-Instruct-8k", temperature=0)
17 +
18 # utility model used for helper functions (cheaper, faster)
18 - utility_llm = chat_llm # change if you want to use a different utility model
19 + utility_llm = chat_llm
20
21 # embedding model used for memory
22 embedding_llm = models.get_openai_embedding(model_name="text-embedding-3-small")
@@ -30,7 +31,7 @@ def initialize():
31 embeddings_model = embedding_llm,
32 # prompts_subdir = "",
33 # memory_subdir = "",
33 - # knowledge_subdir: str = ""
34 + knowledge_subdirs = ["default","custom"],
35 auto_memory_count = 0,
36 # auto_memory_skip = 2,
37 # rate_limit_seconds = 60,
knowledge/custom/.gitkeep
knowledge/custom/main/.gitkeep
knowledge/custom/solutions/.gitkeep
knowledge/default/.gitkeep
knowledge/default/main/.gitkeep
knowledge/default/solutions/.gitkeep
knowledge/default/solutions/yt_download.md new
+6
@@ -0,0 +1,6 @@
1 +# Problem
2 +Download a YouTube video
3 +# Solution
4 +1. If you don't have exact URL, use knowledge_tool to get it
5 +2. Pip install yt-dlp and ffmpeg
6 +3. Download the video using yt-dlp command: 'yt-dlp YT_URL', replace YT_URL with your video URL.
\ No newline at end of file
models.py
+5
@@ -84,3 +84,8 @@ def get_openrouter_chat(model_name: str, api_key=get_api_key("openrouter"), temp
84
85 def get_openrouter_embedding(model_name: str, api_key=get_api_key("openrouter"), base_url=os.getenv("OPEN_ROUTER_BASE_URL") or "https://openrouter.ai/api/v1"):
86 return OpenAIEmbeddings(model=model_name, api_key=api_key, base_url=base_url) # type: ignore
87 +
88 +# Sambanova models
89 +def get_sambanova_chat(model_name: str, api_key=get_api_key("sambanova"), temperature=DEFAULT_TEMPERATURE, base_url=os.getenv("SAMBANOVA_BASE_URL") or "https://fast-api.snova.ai/v1", max_tokens=1024):
90 + return ChatOpenAI(api_key=api_key, model=model_name, temperature=temperature, base_url=base_url, max_tokens=max_tokens) # type: ignore
91 +
prompts/default/agent.system.memories.md new
+4
@@ -0,0 +1,4 @@
1 +# Memories on the topic
2 +- following are your memories about current topic:
3 +
4 +{{memories}}
\ No newline at end of file
prompts/default/memory.memories_query.sys.md new
+19
@@ -0,0 +1,19 @@
1 +# AI's job
2 +1. The AI receives a MESSAGE from USER and short conversation HISTORY for reference
3 +2. AI analyzes the MESSAGE and HISTORY for CONTEXT
4 +3. AI provide a search query for search engine where previous memories are stored based on CONTEXT
5 +
6 +# Format
7 +- The response format is a plain text string containing the query
8 +- No other text, no formatting
9 +
10 +# Example
11 +```json
12 +USER: "Write a song about my dog"
13 +AI: "user's dog"
14 +USER: "following the results of the biology project, summarize..."
15 +AI: "biology project results"
16 +```
17 +
18 +# HISTORY:
19 +{{history}}
\ No newline at end of file
prompts/default/memory.memories_sum.sys.md new
+22
@@ -0,0 +1,22 @@
1 +# Assistant's job
2 +1. The assistant receives a HISTORY of conversation between USER and AGENT
3 +2. Assistant searches for relevant information from the HISTORY
4 +3. Assistant writes notes about information worth memorizing for further use
5 +
6 +# Format
7 +- The response format is a JSON array of text notes containing facts to memorize
8 +- If the history does not contain any useful information, the response will be an empty JSON array.
9 +
10 +# Example
11 +~~~json
12 +[
13 + "User's name is John Doe",
14 + "User's age is 30"
15 +]
16 +~~~
17 +
18 +# Rules
19 +- Focus only on relevant details and facts like names, IDs, instructions, opinions etc.
20 +- Do not include irrelevant details that are of no use in the future
21 +- Do not memorize facts that change like time, date etc.
22 +- Do not add your own details that are not specifically mentioned in the history
\ No newline at end of file
python/extensions/message_loop_prompts/recall_memories.py new
+97
@@ -0,0 +1,97 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +from python.helpers.files import read_file
4 +from python.helpers.vector_db import Area
5 +import json
6 +from python.helpers import errors, files
7 +from python.tools.memory_tool import get_db
8 +
9 +
10 +class RecallMemories(Extension):
11 +
12 + INTERVAL = 3
13 + HISTORY = 5
14 + RESULTS = 3
15 + THRESHOLD = 0.1
16 +
17 + async def execute(self, loop_data={}, **kwargs):
18 +
19 + iter = loop_data.get("iteration", 0)
20 + if (
21 + iter % RecallMemories.INTERVAL == 0
22 + ): # every 3 iterations (or the first one) recall memories
23 + await self.search_memories(loop_data=loop_data, **kwargs)
24 +
25 + async def search_memories(self, loop_data={}, **kwargs):
26 + # try:
27 + # show temp info message
28 + self.agent.context.log.log(
29 + type="info", content="Searching memories...", temp=True
30 + )
31 +
32 + # show full util message, this will hide temp message immediately if turned on
33 + log_item = self.agent.context.log.log(
34 + type="util",
35 + heading="Searching memories...",
36 + )
37 +
38 + # get system message and chat history for util llm
39 + msgs_text = self.agent.concat_messages(
40 + self.agent.history[-RecallMemories.HISTORY :]
41 + ) # only last X messages
42 + system = self.agent.read_prompt(
43 + "memory.memories_query.sys.md", history=msgs_text
44 + )
45 +
46 + # log query streamed by LLM
47 + def log_callback(content):
48 + log_item.stream(query=content)
49 +
50 + # call util llm to summarize conversation
51 + query = await self.agent.call_utility_llm(
52 + system=system, msg=loop_data["message"], callback=log_callback
53 + )
54 +
55 + # get solutions database
56 + vdb = get_db(self.agent)
57 +
58 + memories = vdb.search_similarity_threshold(
59 + query=query,
60 + results=RecallMemories.RESULTS,
61 + threshold=RecallMemories.THRESHOLD,
62 + filter=f"area != '{Area.SOLUTIONS.value}'" # exclude solutions
63 + )
64 +
65 + # log the short result
66 + if not isinstance(memories, list) or len(memories) == 0:
67 + log_item.update(
68 + heading="No useful memories found.",
69 + )
70 + return
71 + else:
72 + log_item.update(
73 + heading=f"\n\n{len(memories)} memories found.",
74 + )
75 +
76 + # concatenate memory.page_content in memories:
77 + memories_text = ""
78 + for memory in memories:
79 + memories_text += memory.page_content + "\n\n"
80 + memories_text = memories_text.strip()
81 +
82 + # log the full results
83 + log_item.update(memories=memories_text)
84 +
85 + # place to prompt
86 + memories_prompt = self.agent.read_prompt(
87 + "agent.system.memories.md", memories=memories_text
88 + )
89 +
90 + # append to system message
91 + loop_data["system"] += memories_prompt
92 +
93 + # except Exception as e:
94 + # err = errors.format_error(e)
95 + # self.agent.context.log.log(
96 + # type="error", heading="Recall memories extension error:", content=err
97 + # )
python/extensions/message_loop_prompts/recall_solutions.py
+72 -48
@@ -1,8 +1,10 @@
1 from agent import Agent
2 from python.helpers.extension import Extension
3 from python.helpers.files import read_file
4 -from python.helpers.vector_db import get_or_create
4 +from python.helpers.vector_db import Area
5 import json
6 +from python.helpers import errors, files
7 +from python.tools.memory_tool import get_db
8
9
10 class RecallSolutions(Extension):
@@ -15,59 +17,81 @@ class RecallSolutions(Extension):
17 async def execute(self, loop_data={}, **kwargs):
18
19 iter = loop_data.get("iteration", 0)
18 - if iter % RecallSolutions.INTERVAL == 0: # every 3 iterations (or the first one) recall solution memories
20 + if (
21 + iter % RecallSolutions.INTERVAL == 0
22 + ): # every 3 iterations (or the first one) recall solution memories
23 await self.search_solutions(loop_data=loop_data, **kwargs)
24
21 -
25 async def search_solutions(self, loop_data={}, **kwargs):
23 - self.agent.context.log.log(
24 - type="info", content="Searching memory for solutions...", temp=False
25 - )
26 -
27 - # get system message and chat history for util llm
28 - msgs_text = self.agent.concat_messages(
29 - self.agent.history[-RecallSolutions.HISTORY:]
30 - ) # only last X messages
31 - system = self.agent.read_prompt(
32 - "memory.solutions_query.sys.md", history=msgs_text
33 - )
34 -
35 - # call util llm to summarize conversation
36 - query = await self.agent.call_utility_llm(
37 - system=system, msg=loop_data["message"]
38 - )
39 -
40 - # get solutions database
41 - vdb = get_or_create(
42 - logger=self.agent.context.log,
43 - embeddings_model=self.agent.config.embeddings_model,
44 - memory_dir="./memory/solutions",
45 - knowledge_dir="",
46 - )
47 -
48 - solutions = vdb.search_similarity_threshold(
49 - query=query, results=RecallSolutions.RESULTS, threshold=RecallSolutions.THRESHOLD
50 - )
51 -
52 - if not isinstance(solutions, list) or len(solutions) == 0:
26 + # try:
27 + # show temp info message
28 self.agent.context.log.log(
54 - type="info", content="No successful solution memories found.", temp=False
29 + type="info", content="Searching memory for solutions...", temp=True
30 )
56 - return
57 - else:
58 - self.agent.context.log.log(
59 - type="info",
60 - content=f"{len(solutions)} successful solution memories found.",
61 - temp=False,
31 +
32 + # show full util message, this will hide temp message immediately if turned on
33 + log_item = self.agent.context.log.log(
34 + type="util",
35 + heading="Searching memory for solutions...",
36 + )
37 +
38 + # get system message and chat history for util llm
39 + msgs_text = self.agent.concat_messages(
40 + self.agent.history[-RecallSolutions.HISTORY :]
41 + ) # only last X messages
42 + system = self.agent.read_prompt(
43 + "memory.solutions_query.sys.md", history=msgs_text
44 + )
45 +
46 + # log query streamed by LLM
47 + def log_callback(content):
48 + log_item.stream(query=content)
49 +
50 + # call util llm to summarize conversation
51 + query = await self.agent.call_utility_llm(
52 + system=system, msg=loop_data["message"], callback=log_callback
53 )
54
64 - # concatenate solution.page_content in solutions:
65 - solutions_text = ""
66 - for solution in solutions:
67 - solutions_text += solution.page_content + "\n\n"
55 + # get solutions database
56 + vdb = get_db(self.agent)
57 +
58 + solutions = vdb.search_similarity_threshold(
59 + query=query,
60 + results=RecallSolutions.RESULTS,
61 + threshold=RecallSolutions.THRESHOLD,
62 + filter=f"area == '{Area.SOLUTIONS.value}'"
63 + )
64 +
65 + # log the short result
66 + if not isinstance(solutions, list) or len(solutions) == 0:
67 + log_item.update(
68 + heading="No successful solution memories found.",
69 + )
70 + return
71 + else:
72 + log_item.update(
73 + heading=f"\n\n{len(solutions)} successful solution memories found.",
74 + )
75 +
76 + # concatenate solution.page_content in solutions:
77 + solutions_text = ""
78 + for solution in solutions:
79 + solutions_text += solution.page_content + "\n\n"
80 + solutions_text = solutions_text.strip()
81 +
82 + # log the full results
83 + log_item.update(solutions=solutions_text)
84 +
85 + # place to prompt
86 + solutions_prompt = self.agent.read_prompt(
87 + "agent.system.solutions.md", solutions=solutions_text
88 + )
89
69 - # place to prompt
70 - solutions_prompt = self.agent.read_prompt("agent.system.solutions.md", solutions=solutions_text)
90 + # append to system message
91 + loop_data["system"] += solutions_prompt
92
72 - # append to system message
73 - loop_data["system"] += solutions_prompt
93 + # except Exception as e:
94 + # err = errors.format_error(e)
95 + # self.agent.context.log.log(
96 + # type="error", heading="Recall solutions extension error:", content=err
97 + # )
python/extensions/monologue_end/50_memorize_memories.py new
+74
@@ -0,0 +1,74 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +import python.helpers.files as files
4 +from python.helpers.vector_db import Area
5 +import json
6 +from python.helpers.dirty_json import DirtyJson
7 +from python.helpers import errors
8 +from python.tools.memory_tool import get_db
9 +
10 +class MemorizeMemories(Extension):
11 +
12 + async def execute(self, loop_data={}, **kwargs):
13 + # try:
14 +
15 + # show temp info message
16 + self.agent.context.log.log(
17 + type="info", content="Memorizing new information...", temp=True
18 + )
19 +
20 + # show full util message, this will hide temp message immediately if turned on
21 + log_item = self.agent.context.log.log(
22 + type="util",
23 + heading="Memorizing new information...",
24 + )
25 +
26 + # get system message and chat history for util llm
27 + system = self.agent.read_prompt("memory.memories_sum.sys.md")
28 + msgs_text = self.agent.concat_messages(self.agent.history)
29 +
30 + # log query streamed by LLM
31 + def log_callback(content):
32 + log_item.stream(content=content)
33 +
34 + # call util llm to find info in history
35 + memories_json = await self.agent.call_utility_llm(
36 + system=system,
37 + msg=msgs_text,
38 + callback=log_callback,
39 + )
40 +
41 + memories = DirtyJson.parse_string(memories_json)
42 +
43 + if not isinstance(memories, list) or len(memories) == 0:
44 + log_item.update(heading="No useful information to memorize.")
45 + return
46 + else:
47 + log_item.update(
48 + heading=f"{len(memories)} entries to memorize."
49 + )
50 +
51 + # save chat history
52 + vdb = get_db(self.agent)
53 +
54 + memories_txt = ""
55 + for memory in memories:
56 + # solution to plain text:
57 + txt = f"{memory}"
58 + memories_txt += txt + "\n\n"
59 + vdb.insert_text(
60 + text=txt, metadata={"area": Area.MAIN.value}
61 + )
62 +
63 + memories_txt = memories_txt.strip()
64 + log_item.update(memories=memories_txt)
65 + log_item.update(
66 + result=f"{len(memories)} entries memorized.",
67 + heading=f"{len(memories)} entries memorized.",
68 + )
69 +
70 + # except Exception as e:
71 + # err = errors.format_error(e)
72 + # self.agent.context.log.log(
73 + # type="error", heading="Memorize memories extension error:", content=err
74 + # )
python/extensions/monologue_end/51_memorize_solutions.py new
+74
@@ -0,0 +1,74 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +import python.helpers.files as files
4 +from python.helpers.vector_db import Area
5 +import json
6 +from python.helpers.dirty_json import DirtyJson
7 +from python.helpers import errors
8 +from python.tools.memory_tool import get_db
9 +
10 +class MemorizeSolutions(Extension):
11 +
12 + async def execute(self, loop_data={}, **kwargs):
13 + # try:
14 +
15 + # show temp info message
16 + self.agent.context.log.log(
17 + type="info", content="Memorizing succesful solutions...", temp=True
18 + )
19 +
20 + # show full util message, this will hide temp message immediately if turned on
21 + log_item = self.agent.context.log.log(
22 + type="util",
23 + heading="Memorizing succesful solutions...",
24 + )
25 +
26 + # get system message and chat history for util llm
27 + system = self.agent.read_prompt("memory.solutions_sum.sys.md")
28 + msgs_text = self.agent.concat_messages(self.agent.history)
29 +
30 + # log query streamed by LLM
31 + def log_callback(content):
32 + log_item.stream(content=content)
33 +
34 + # call util llm to find solutions in history
35 + solutions_json = await self.agent.call_utility_llm(
36 + system=system,
37 + msg=msgs_text,
38 + callback=log_callback,
39 + )
40 +
41 + solutions = DirtyJson.parse_string(solutions_json)
42 +
43 + if not isinstance(solutions, list) or len(solutions) == 0:
44 + log_item.update(heading="No successful solutions to memorize.")
45 + return
46 + else:
47 + log_item.update(
48 + heading=f"{len(solutions)} successful solutions to memorize."
49 + )
50 +
51 + # save chat history
52 + vdb = get_db(self.agent)
53 +
54 + solutions_txt = ""
55 + for solution in solutions:
56 + # solution to plain text:
57 + txt = f"# Problem\n {solution['problem']}\n# Solution\n {solution['solution']}"
58 + solutions_txt += txt + "\n\n"
59 + vdb.insert_text(
60 + text=txt, metadata={"area": Area.SOLUTIONS.value}
61 + )
62 +
63 + solutions_txt = solutions_txt.strip()
64 + log_item.update(solutions=solutions_txt)
65 + log_item.update(
66 + result=f"{len(solutions)} solutions memorized.",
67 + heading=f"{len(solutions)} solutions memorized.",
68 + )
69 +
70 + # except Exception as e:
71 + # err = errors.format_error(e)
72 + # self.agent.context.log.log(
73 + # type="error", heading="Memorize solutions extension error:", content=err
74 + # )
python/extensions/monologue_end/90_waiting_for_input_msg.py new
+12
@@ -0,0 +1,12 @@
1 +from agent import Agent
2 +from python.helpers.extension import Extension
3 +
4 +class WaitingForInputMsg(Extension):
5 +
6 + async def execute(self, loop_data={}, **kwargs):
7 + # show temp info message
8 + if self.agent.number == 0:
9 + self.agent.context.log.log(
10 + type="util", heading="Waiting for input", temp=True
11 + )
12 +
python/extensions/monologue_end/memorize_solutions.py deleted
-70
@@ -1,70 +0,0 @@
1 -from agent import Agent
2 -from python.helpers.extension import Extension
3 -from python.helpers.files import read_file
4 -from python.helpers.vector_db import get_or_create
5 -import json
6 -from python.helpers.dirty_json import DirtyJson
7 -from python.helpers import errors
8 -
9 -
10 -class MemorizeSolutions(Extension):
11 -
12 - async def execute(self, loop_data={}, **kwargs):
13 - try:
14 - self.agent.context.log.log(
15 - type="info", content="Memorizing succesful solutions...", temp=True
16 - )
17 -
18 - # get system message and chat history for util llm
19 - system = self.agent.read_prompt("memory.solutions_sum.sys.md")
20 - msgs_text = self.agent.concat_messages(self.agent.history)
21 -
22 - # call util llm to find solutions in history
23 - solutions_json = await self.agent.call_utility_llm(
24 - system=system,
25 - msg=msgs_text,
26 - log_type="util",
27 - )
28 -
29 - solutions = DirtyJson.parse_string(solutions_json)
30 -
31 - if not isinstance(solutions, list) or len(solutions) == 0:
32 - self.agent.context.log.log(
33 - type="info", content="No succesful solutions found.", temp=False
34 - )
35 - return
36 - else:
37 - self.agent.context.log.log(
38 - type="info",
39 - content=f"{len(solutions)} succesful solutions found.",
40 - temp=True,
41 - )
42 -
43 - # save chat history
44 - vdb = get_or_create(
45 - logger=self.agent.context.log,
46 - embeddings_model=self.agent.config.embeddings_model,
47 - memory_dir="./memory/solutions",
48 - knowledge_dir="",
49 - )
50 -
51 - for solution in solutions:
52 - # solution to plain text:
53 - txt = (
54 - f"Problem: {solution['problem']}\nSolution: {solution['solution']}"
55 - )
56 - vdb.insert_text(
57 - text=txt,
58 - ) # metadata={"full": msgs_text})
59 -
60 - self.agent.context.log.log(
61 - type="info",
62 - content=f"{len(solutions)} solutions memorized.",
63 - temp=False,
64 - )
65 -
66 - except Exception as e:
67 - err = errors.format_error(e)
68 - self.agent.context.log.log(
69 - type="error", heading="Memorize solutions extension error:", content=err
70 - )
python/extensions/msg_loop_break/zero_memorize_history.py deleted
-35
@@ -1,35 +0,0 @@
1 -from agent import Agent
2 -from python.helpers.extension import Extension
3 -from python.helpers.files import read_file
4 -from python.helpers.vector_db import VectorDB
5 -import json
6 -
7 -class MemorizeHistory(Extension):
8 -
9 - async def execute(self, **kwargs):
10 - if self.agent.number != 0: return #only agent 0 will memorize chat history with user
11 -
12 - self.agent.context.log.log(type="info", content="Memorizing chat history...", temp=True)
13 -
14 - #get system message and chat history for util llm
15 - system = self.agent.read_prompt("fw.memory.hist_sum.sys")
16 - msgs = []
17 - for msg in self.agent.history:
18 - content = msg.get("content", "")
19 - if content:
20 - msgs.append(content)
21 - msgs_json = json.dumps(msgs)
22 -
23 - #call util llm to summarize conversation
24 - summary = await self.agent.call_utility_llm(system=system,msg=msgs_json,output_label="")
25 -
26 - #save chat history
27 - vdb = VectorDB(
28 - logger=self.agent.context.log,
29 - embeddings_model=self.agent.config.embeddings_model,
30 - memory_dir="./memory/history",
31 - knowledge_dir=""
32 - )
33 -
34 - self.agent.context.log.log(type="info", content="Chat history memorized.", temp=True)
35 -
\ No newline at end of file
python/helpers/knowledge_import.py
+77 -47
@@ -4,13 +4,17 @@ 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
7 + CSVLoader,
8 + JSONLoader,
9 + PyPDFLoader,
10 + TextLoader,
11 + UnstructuredHTMLLoader,
12 + UnstructuredMarkdownLoader,
13 )
14 from python.helpers import files
11 -from python.helpers.log import Log
15 +from python.helpers.log import LogItem
16
13 -text_loader_kwargs = {'autodetect_encoding': True}
17 +text_loader_kwargs = {"autodetect_encoding": True}
18
19
20 class KnowledgeImport(TypedDict):
@@ -23,65 +27,91 @@ class KnowledgeImport(TypedDict):
27
28 def calculate_checksum(file_path: str) -> str:
29 hasher = hashlib.md5()
26 - with open(file_path, 'rb') as f:
30 + with open(file_path, "rb") as f:
31 buf = f.read()
32 hasher.update(buf)
33 return hasher.hexdigest()
34
31 -def load_knowledge(logger: Log, knowledge_dir: str, index: Dict[str, KnowledgeImport]) -> Dict[str, KnowledgeImport]:
35 +
36 +def load_knowledge(
37 + log_item: LogItem | None, knowledge_dir: str, index: Dict[str, KnowledgeImport]
38 +) -> Dict[str, KnowledgeImport]:
39 knowledge_dir = files.get_abs_path(knowledge_dir)
33 -
40 +
41 + from python.helpers.vector_db import Area
42
43 # Mapping file extensions to corresponding loader classes
44 file_types_loaders = {
37 - 'txt': TextLoader,
38 - 'pdf': PyPDFLoader,
39 - 'csv': CSVLoader,
40 - 'html': UnstructuredHTMLLoader,
41 - 'json': JSONLoader,
42 - 'md': UnstructuredMarkdownLoader
45 + "txt": TextLoader,
46 + "pdf": PyPDFLoader,
47 + "csv": CSVLoader,
48 + "html": UnstructuredHTMLLoader,
49 + "json": JSONLoader,
50 + "md": UnstructuredMarkdownLoader,
51 }
52
53 cnt_files = 0
54 cnt_docs = 0
55
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 - logger.log(type="info", content=f"Found {len(kn_files)} knowledge files in {knowledge_dir}, processing...")
53 -
54 - for file_path in kn_files:
55 - ext = file_path.split('.')[-1].lower()
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
56 + for area in Area:
57 + subdir = files.get_abs_path(knowledge_dir, area.value)
58 +
59 + if not os.path.exists(subdir):
60 + os.makedirs(subdir)
61 + continue
62 +
63 + # Fetch all files in the directory with specified extensions
64 + kn_files = glob.glob(subdir + "/**/*", recursive=True)
65 + if kn_files:
66 + print(f"Found {len(kn_files)} knowledge files in {subdir}, processing...")
67 + if log_item:
68 + log_item.stream(
69 + progress=f"\nFound {len(kn_files)} knowledge files in {subdir}, processing...",
70 + )
71 +
72 + for file_path in kn_files:
73 + ext = file_path.split(".")[-1].lower()
74 + if ext in file_types_loaders:
75 + checksum = calculate_checksum(file_path)
76 + file_key = file_path # os.path.relpath(file_path, knowledge_dir)
77 +
78 + # Load existing data from the index or create a new entry
79 + file_data = index.get(file_key, {})
80 +
81 + if file_data.get("checksum") == checksum:
82 + file_data["state"] = "original"
83 + else:
84 + file_data["state"] = "changed"
85 +
86 + if file_data["state"] == "changed":
87 + file_data["checksum"] = checksum
88 + loader_cls = file_types_loaders[ext]
89 + loader = loader_cls(
90 + file_path,
91 + **(
92 + text_loader_kwargs
93 + if ext in ["txt", "csv", "html", "md"]
94 + else {}
95 + ),
96 + )
97 + file_data["documents"] = loader.load_and_split()
98 + for doc in file_data["documents"]:
99 + doc.metadata["area"] = area.value
100 + cnt_files += 1
101 + cnt_docs += len(file_data["documents"])
102 + # print(f"Imported {len(file_data['documents'])} documents from {file_path}")
103 +
104 + # Update the index
105 + index[file_key] = file_data # type: ignore
106
107 # loop index where state is not set and mark it as removed
108 for file_key, file_data in index.items():
82 - if not file_data.get('state', ''):
83 - index[file_key]['state'] = 'removed'
109 + if not file_data.get("state", ""):
110 + index[file_key]["state"] = "removed"
111
112 print(f"Processed {cnt_docs} documents from {cnt_files} files.")
86 - logger.log(type="info", content=f"Processed {cnt_docs} documents from {cnt_files} files.")
113 + if log_item:
114 + log_item.stream(
115 + progress=f"\nProcessed {cnt_docs} documents from {cnt_files} files."
116 + )
117 return index
python/helpers/log.py
+85 -21
@@ -5,20 +5,23 @@ import uuid
5
6
7 type Type = Literal[
8 - 'agent',
9 - 'code_exe',
10 - 'error',
11 - 'hint',
12 - 'info',
13 - 'tool',
14 - 'user',
15 - 'util',
16 - 'warning',
17 - ]
8 + "agent",
9 + "code_exe",
10 + "error",
11 + "hint",
12 + "info",
13 + "progress",
14 + "response",
15 + "tool",
16 + "user",
17 + "util",
18 + "warning",
19 +]
20 +
21
22 @dataclass
23 class LogItem:
21 - log: 'Log'
24 + log: "Log"
25 no: int
26 type: str
27 heading: str
@@ -30,9 +33,35 @@ class LogItem:
33 def __post_init__(self):
34 self.guid = self.log.guid
35
33 - def update(self, type: Type | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None, temp: bool | None = None):
36 + def update(
37 + self,
38 + type: Type | None = None,
39 + heading: str | None = None,
40 + content: str | None = None,
41 + kvps: dict | None = None,
42 + temp: bool | None = None,
43 + **kwargs,
44 + ):
45 if self.guid == self.log.guid:
35 - self.log.update_item(self.no, type=type, heading=heading, content=content, kvps=kvps, temp=temp)
46 + self.log.update_item(
47 + self.no,
48 + type=type,
49 + heading=heading,
50 + content=content,
51 + kvps=kvps,
52 + temp=temp,
53 + **kwargs,
54 + )
55 +
56 + def stream(self, heading: str | None = None, content: str | None = None, **kwargs):
57 + if heading is not None:
58 + self.update(heading=self.heading + heading)
59 + if content is not None:
60 + self.update(content=self.content + content)
61 +
62 + for k, v in kwargs.items():
63 + prev = self.kvps.get(k, "") if self.kvps else ""
64 + self.update(**{k: prev + v})
65
66 def output(self):
67 return {
@@ -41,34 +70,70 @@ class LogItem:
70 "heading": self.heading,
71 "content": self.content,
72 "temp": self.temp,
44 - "kvps": self.kvps
73 + "kvps": self.kvps,
74 }
75
76 +
77 class Log:
78
79 def __init__(self):
80 self.guid: str = str(uuid.uuid4())
81 self.updates: list[int] = []
82 self.logs: list[LogItem] = []
83 + self.progress = ""
84
54 - def log(self, type: Type, heading: str | None = None, content: str | None = None, kvps: dict | None = None, temp: bool | None = None) -> LogItem:
55 - item = LogItem(log=self,no=len(self.logs), type=type, heading=heading or "", content=content or "", kvps=kvps, temp=temp or False)
85 + def log(
86 + self,
87 + type: Type,
88 + heading: str | None = None,
89 + content: str | None = None,
90 + kvps: dict | None = None,
91 + temp: bool | None = None,
92 + ) -> LogItem:
93 + item = LogItem(
94 + log=self,
95 + no=len(self.logs),
96 + type=type,
97 + heading=heading or "",
98 + content=content or "",
99 + kvps=kvps,
100 + temp=temp or False,
101 + )
102 self.logs.append(item)
103 self.updates += [item.no]
104 + if heading:
105 + self.progress = heading
106 return item
107
60 - def update_item(self, no: int, type: str | None = None, heading: str | None = None, content: str | None = None, kvps: dict | None = None, temp: bool | None = None):
108 + def update_item(
109 + self,
110 + no: int,
111 + type: str | None = None,
112 + heading: str | None = None,
113 + content: str | None = None,
114 + kvps: dict | None = None,
115 + temp: bool | None = None,
116 + **kwargs,
117 + ):
118 item = self.logs[no]
119 if type is not None:
120 item.type = type
121 if heading is not None:
122 item.heading = heading
123 + self.progress = heading
124 if content is not None:
125 item.content = content
126 if kvps is not None:
127 item.kvps = kvps
128 if temp is not None:
129 item.temp = temp
130 +
131 + if kwargs:
132 + if item.kvps is None:
133 + item.kvps = {}
134 + for k, v in kwargs.items():
135 + item.kvps[k] = v
136 +
137 self.updates += [item.no]
138
139 def output(self, start=None, end=None):
@@ -76,19 +141,18 @@ class Log:
141 start = 0
142 if end is None:
143 end = len(self.updates)
79 -
144 +
145 out = []
146 seen = set()
147 for update in self.updates[start:end]:
148 if update not in seen:
149 out.append(self.logs[update].output())
150 seen.add(update)
86 -
87 - return out
88 -
151
152 + return out
153
154 def reset(self):
155 self.guid = str(uuid.uuid4())
156 self.updates = []
157 self.logs = []
158 + self.progress = ""
python/helpers/tool.py
+3 -3
@@ -22,8 +22,8 @@ class Tool:
22 pass
23
24 async def before_execution(self, **kwargs):
25 - PrintStyle(font_color="#1B4F72", padding=True, background_color="white", bold=True).print(f"{self.agent.agent_name}: Using tool '{self.name}':")
26 - self.log = self.agent.context.log.log(type="tool", heading=f"{self.agent.agent_name}: Using tool '{self.name}':", content="", kvps=self.args)
25 + PrintStyle(font_color="#1B4F72", padding=True, background_color="white", bold=True).print(f"{self.agent.agent_name}: Using tool '{self.name}'")
26 + self.log = self.agent.context.log.log(type="tool", heading=f"{self.agent.agent_name}: Using tool '{self.name}'", content="", kvps=self.args)
27 if self.args and isinstance(self.args, dict):
28 for key, value in self.args.items():
29 PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ")
@@ -34,7 +34,7 @@ class Tool:
34 text = messages.truncate_text(self.agent, response.message.strip(), self.agent.config.max_tool_response_length)
35 msg_response = self.agent.read_prompt("fw.tool_response.md", tool_name=self.name, tool_response=text)
36 await self.agent.append_message(msg_response, human=True)
37 - PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}':")
37 + PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}'")
38 PrintStyle(font_color="#85C1E9").print(response.message)
39 self.log.update(content=response.message)
40
python/helpers/vector_db.py
+51 -22
@@ -1,3 +1,4 @@
1 +from typing import Any
2 from langchain.storage import InMemoryByteStore, LocalFileStore
3 from langchain.embeddings import CacheBackedEmbeddings
4
@@ -11,21 +12,31 @@ from . import files
12 from langchain_core.documents import Document
13 import uuid
14 from python.helpers import knowledge_import
14 -from python.helpers.log import Log
15 +from python.helpers.log import Log, LogItem
16 +import pandas as pd
17 +from enum import Enum
18 +
19 +
20 +class Area(Enum):
21 + MAIN = "main"
22 + SOLUTIONS = "solutions"
23 +
24
25 index: dict[str, "VectorDB"] = {}
26
27
19 -def get_or_create(
20 - logger: Log,
28 +def get_or_create_db(
29 + logger: Log | None,
30 embeddings_model,
31 + memory_dir: str,
32 in_memory=False,
23 - memory_dir="./memory",
24 - knowledge_dir=None,
33 + knowledge_dirs: list[str] = [],
34 ):
35 if index.get(memory_dir) is None:
36 + log_item = None
37 + if(logger): log_item = logger.log(type="util", heading=f"Initializing VectorDB in {memory_dir}")
38 index[memory_dir] = VectorDB(
28 - logger, embeddings_model, in_memory, memory_dir, knowledge_dir
39 + log_item, embeddings_model, memory_dir, knowledge_dirs, in_memory
40 )
41 return index[memory_dir]
42
@@ -34,22 +45,23 @@ class VectorDB:
45
46 def __init__(
47 self,
37 - logger: Log,
48 + log_item: LogItem | None,
49 embeddings_model,
50 + memory_dir: str,
51 + knowledge_dirs: list[str] = [],
52 in_memory=False,
40 - memory_dir="./memory/default",
41 - knowledge_dir=None,
53 ):
43 - self.logger = logger
54 + self.log_item = log_item
55
56 print("Initializing VectorDB...")
46 - self.logger.log("info", content="Initializing VectorDB...", temp=True)
57 + if(self.log_item): self.log_item.stream(progress="\nInitializing VectorDB")
58
59 self.embeddings_model = embeddings_model
60
50 - self.em_dir = files.get_abs_path("./memory/embeddings") # just caching, no need to parameterize
61 + self.em_dir = files.get_abs_path(
62 + "./memory/embeddings"
63 + ) # just caching, no need to parameterize
64 self.db_dir = files.get_abs_path("./memory", memory_dir, "database")
52 - self.kn_dir = files.get_abs_path(knowledge_dir) if knowledge_dir else ""
65
66 # make sure embeddings and database directories exist
67 os.makedirs(self.db_dir, exist_ok=True)
@@ -93,10 +105,10 @@ class VectorDB:
105 )
106
107 # preload knowledge files
96 - if self.kn_dir:
97 - self.preload_knowledge(self.kn_dir, self.db_dir)
108 + if knowledge_dirs:
109 + self.preload_knowledge(knowledge_dirs, self.db_dir)
110
99 - def preload_knowledge(self, kn_dir: str, db_dir: str):
111 + def preload_knowledge(self, kn_dirs: list[str], db_dir: str):
112
113 # Load the index file if it exists
114 index_path = files.get_abs_path(db_dir, "knowledge_import.json")
@@ -110,7 +122,8 @@ class VectorDB:
122 with open(index_path, "r") as f:
123 index = json.load(f)
124
113 - index = knowledge_import.load_knowledge(self.logger, kn_dir, index)
125 + for kn_dir in kn_dirs:
126 + index = knowledge_import.load_knowledge(self.log_item, kn_dir, index)
127
128 for file in index:
129 if index[file]["state"] in ["changed", "removed"] and index[file].get(
@@ -139,12 +152,16 @@ class VectorDB:
152 def search_similarity(self, query, results=3):
153 return self.db.similarity_search(query, results)
154
142 - def search_similarity_threshold(self, query, results=3, threshold=0.5):
155 + def search_similarity_threshold(
156 + self, query: str, results=3, threshold=0.5, filter: str = ""
157 + ):
158 + comparator = VectorDB.get_comparator(filter) if filter else None
159 return self.db.search(
160 query,
161 search_type="similarity_score_threshold",
162 k=results,
163 score_threshold=threshold,
164 + filter=comparator,
165 )
166
167 def search_max_rel(self, query, results=3):
@@ -198,8 +215,20 @@ class VectorDB:
215
216 def insert_documents(self, docs: list[Document]):
217 ids = [str(uuid.uuid4()) for _ in range(len(docs))]
201 - for doc, id in zip(docs, ids):
202 - doc.metadata["id"] = id # add ids to documents metadata
203 - self.db.add_documents(documents=docs, ids=ids)
204 - self.db.save_local(folder_path=self.db_dir) # persist
218 + if ids:
219 + for doc, id in zip(docs, ids):
220 + doc.metadata["id"] = id # add ids to documents metadata
221 + self.db.add_documents(documents=docs, ids=ids)
222 + self.db.save_local(folder_path=self.db_dir) # persist
223 return ids
224 +
225 + @staticmethod
226 + def get_comparator(condition: str):
227 + def comparator(data: dict[str, Any]):
228 + try:
229 + return eval(condition, {}, data)
230 + except Exception as e:
231 + print(f"Error evaluating condition: {e}")
232 + return False
233 +
234 + return comparator
python/tools/code_execution_tool.py
+3 -3
@@ -53,10 +53,10 @@ class CodeExecution(Tool):
53 await self.agent.handle_intervention() # wait for intervention and handle it, if paused
54 PrintStyle(
55 font_color="#1B4F72", padding=True, background_color="white", bold=True
56 - ).print(f"{self.agent.agent_name}: Using tool '{self.name}':")
56 + ).print(f"{self.agent.agent_name}: Using tool '{self.name}'")
57 self.log = self.agent.context.log.log(
58 type="code_exe",
59 - heading=f"{self.agent.agent_name}: Using tool '{self.name}':",
59 + heading=f"{self.agent.agent_name}: Using tool '{self.name}'",
60 content="",
61 kvps=self.args,
62 )
@@ -132,7 +132,7 @@ class CodeExecution(Tool):
132 self.state.shell.send_command(command)
133
134 PrintStyle(background_color="white", font_color="#1B4F72", bold=True).print(
135 - f"{self.agent.agent_name} code execution output:"
135 + f"{self.agent.agent_name} code execution output"
136 )
137 return await self.get_terminal_output()
138
python/tools/knowledge_tool.py
+1 -3
@@ -25,9 +25,7 @@ class Knowledge(Tool):
25 duckduckgo = executor.submit(duckduckgo_search.search, question)
26
27 # manual memory search
28 - future_memory_man = executor.submit(memory_tool.search, self.agent, "manual", question)
29 - # history memory search
30 - # future_memory_man = executor.submit(memory_tool.search, self.agent, "history", question)
28 + future_memory_man = executor.submit(memory_tool.search, self.agent, question)
29
30 # Wait for both functions to complete
31 try:
python/tools/memory_tool.py
+19 -22
@@ -1,14 +1,11 @@
1 import re
2 -from typing import Literal
2 from agent import Agent
4 -from python.helpers.vector_db import get_or_create
3 +from python.helpers.vector_db import get_or_create_db
4 import os
5 from python.helpers.tool import Tool, Response
6 from python.helpers.print_style import PrintStyle
7 from python.helpers.errors import handle_error
9 -
10 -
11 -type Area = Literal['manual', 'history']
8 +from python.helpers import files
9
10 class Memory(Tool):
11 async def execute(self,**kwargs):
@@ -20,13 +17,13 @@ class Memory(Tool):
17 if "query" in kwargs:
18 threshold = float(kwargs.get("threshold", 0.1))
19 count = int(kwargs.get("count", 5))
23 - result = search(self.agent, area, kwargs["query"], count, threshold)
20 + result = search(self.agent, kwargs["query"], count, threshold)
21 elif "memorize" in kwargs:
25 - result = save(self.agent, area, kwargs["memorize"])
22 + result = save(self.agent, kwargs["memorize"])
23 elif "forget" in kwargs:
27 - result = forget(self.agent, area, kwargs["forget"])
24 + result = forget(self.agent, kwargs["forget"])
25 # elif "delete" in kwargs
29 - result = delete(self.agent, area, kwargs["delete"])
26 + result = delete(self.agent, kwargs["delete"])
27 except Exception as e:
28 handle_error(e)
29 # hint about embedding change with existing database
@@ -37,39 +34,39 @@ class Memory(Tool):
34 # result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
35 return Response(message=result, break_loop=False)
36
40 -def search(agent:Agent, area: Area, query:str, count:int=5, threshold:float=0.1):
41 - db = get_db(agent, area)
37 +def search(agent:Agent, query:str, count:int=5, threshold:float=0.1):
38 + db = get_db(agent)
39 # docs = db.search_similarity(query,count) # type: ignore
40 docs = db.search_similarity_threshold(query,count,threshold) # type: ignore
41 if len(docs)==0: return agent.read_prompt("fw.memories_not_found.md", query=query)
42 else: return str(docs)
43
47 -def save(agent:Agent, area: Area, text:str):
48 - db = get_db(agent, area)
44 +def save(agent:Agent, text:str):
45 + db = get_db(agent)
46 id = db.insert_text(text) # type: ignore
47 return agent.read_prompt("fw.memory_saved.md", memory_id=id)
48
52 -def delete(agent:Agent, area: Area, ids_str:str):
53 - db = get_db(agent, area)
49 +def delete(agent:Agent, ids_str:str):
50 + db = get_db(agent)
51 ids = extract_guids(ids_str)
52 deleted = db.delete_documents_by_ids(ids) # type: ignore
53 return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
54
58 -def forget(agent:Agent, area: Area, query:str):
59 - db = get_db(agent, area)
55 +def forget(agent:Agent, query:str):
56 + db = get_db(agent)
57 deleted = db.delete_documents_by_query(query) # type: ignore
58 return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
59
63 -def get_db(agent: Agent, area: Area):
64 - mem_dir = os.path.join("memory", agent.config.memory_subdir or "default", area)
65 - kn_dir = os.path.join("knowledge", agent.config.knowledge_subdir)
60 +def get_db(agent: Agent):
61 + mem_dir = files.get_abs_path("memory", agent.config.memory_subdir or "default")
62 + kn_dirs = [files.get_abs_path("knowledge", d) for d in agent.config.knowledge_subdirs or []]
63
67 - db = get_or_create(
64 + db = get_or_create_db(
65 agent.context.log,
66 embeddings_model=agent.config.embeddings_model,
67 in_memory=False,
68 memory_dir=mem_dir,
72 - knowledge_dir=kn_dir)
69 + knowledge_dirs=kn_dirs)
70
71 return db
72
python/tools/response.py
+1 -1
@@ -7,7 +7,7 @@ class ResponseTool(Tool):
7 return Response(message=self.args["text"], break_loop=True)
8
9 async def before_execution(self, **kwargs):
10 - self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding:", content=self.args.get("text", ""))
10 + self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding", content=self.args.get("text", ""))
11
12
13 async def after_execution(self, response, **kwargs):
python/tools/task_done.py
+1 -1
@@ -7,7 +7,7 @@ class TaskDone(Tool):
7 return Response(message=self.args["text"], break_loop=True)
8
9 async def before_execution(self, **kwargs):
10 - self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Task done:", content=self.args.get("text", ""))
10 + self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Task done", content=self.args.get("text", ""))
11
12 async def after_execution(self, response, **kwargs):
13 pass # do add anything to the history or output
\ No newline at end of file
requirements.txt
+4
@@ -7,10 +7,12 @@ langchain-openai==0.1.15
7 langchain-community==0.2.7
8 langchain-anthropic==0.1.19
9 langchain-google-genai==1.0.7
10 +Markdown==3.7
11 langchain_mistralai==0.1.8
12 webcolors==24.6.0
13 sentence-transformers==3.0.1
14 docker==7.1.0
15 +pandas==2.2.3
16 paramiko==3.4.0
17 duckduckgo_search==6.1.12
18 inputimeout==1.0.4
@@ -19,6 +21,8 @@ beautifulsoup4==4.12.3
21 lxml_html_clean==0.2.0
22 pynput==1.7.7
23 pypdf==4.3.1
24 +unstructured==0.15.13
25 +unstructured-client==0.25.9
26 Flask[async]==3.0.3
27 Flask-BasicAuth==0.2.0
28 faiss-cpu==1.8.0.post1
\ No newline at end of file
run_ui.py
+105 -73
@@ -13,93 +13,116 @@ from python.helpers.print_style import PrintStyle
13 from python.helpers.log import Log
14 from dotenv import load_dotenv
15
16 +load_dotenv()
17
17 -#initialize the internal Flask server
18 -app = Flask("app",static_folder=get_abs_path("./webui"),static_url_path="/")
18 +# initialize the internal Flask server
19 +app = Flask("app", static_folder=get_abs_path("./webui"), static_url_path="/")
20 lock = threading.Lock()
21
22 # Set up basic authentication, name and password from .env variables
22 -app.config['BASIC_AUTH_USERNAME'] = os.environ.get('BASIC_AUTH_USERNAME') or "admin" #default name
23 -app.config['BASIC_AUTH_PASSWORD'] = os.environ.get('BASIC_AUTH_PASSWORD') or "admin" #default pass
23 +app.config["BASIC_AUTH_USERNAME"] = (
24 + os.environ.get("BASIC_AUTH_USERNAME") or "admin"
25 +) # default name
26 +app.config["BASIC_AUTH_PASSWORD"] = (
27 + os.environ.get("BASIC_AUTH_PASSWORD") or "admin"
28 +) # default pass
29 basic_auth = BasicAuth(app)
30
31 +
32 # get context to run agent zero in
27 -def get_context(ctxid:str):
33 +def get_context(ctxid: str):
34 with lock:
29 - if not ctxid:
30 - first = AgentContext.first()
31 - if first: return first
35 + if not ctxid:
36 + first = AgentContext.first()
37 + if first:
38 + return first
39 return AgentContext(config=initialize())
40 got = AgentContext.get(ctxid)
34 - if got: return got
35 - return AgentContext(config=initialize(),id=ctxid)
41 + if got:
42 + return got
43 + return AgentContext(config=initialize(), id=ctxid)
44 +
45
46 # Now you can use @requires_auth function decorator to require login on certain pages
47 def requires_auth(f):
48 @wraps(f)
49 async def decorated(*args, **kwargs):
50 auth = request.authorization
42 - if not auth or not (auth.username == app.config['BASIC_AUTH_USERNAME'] and auth.password == app.config['BASIC_AUTH_PASSWORD']):
51 + if not auth or not (
52 + auth.username == app.config["BASIC_AUTH_USERNAME"]
53 + and auth.password == app.config["BASIC_AUTH_PASSWORD"]
54 + ):
55 return Response(
44 - 'Could not verify your access level for that URL.\n'
45 - 'You have to login with proper credentials', 401,
46 - {'WWW-Authenticate': 'Basic realm="Login Required"'})
56 + "Could not verify your access level for that URL.\n"
57 + "You have to login with proper credentials",
58 + 401,
59 + {"WWW-Authenticate": 'Basic realm="Login Required"'},
60 + )
61 return await f(*args, **kwargs)
62 +
63 return decorated
64
65 +
66 # handle default address, show demo html page from ./test_form.html
51 -@app.route('/', methods=['GET'])
67 +@app.route("/", methods=["GET"])
68 async def test_form():
69 return Path(get_abs_path("./webui/index.html")).read_text()
70
71 +
72 # simple health check, just return OK to see the server is running
56 -@app.route('/ok', methods=['GET','POST'])
73 +@app.route("/ok", methods=["GET", "POST"])
74 async def health_check():
75 return "OK"
76
77 +
78 # # secret page, requires authentication
79 # @app.route('/secret', methods=['GET'])
80 # @requires_auth
81 # async def secret_page():
82 # return Path("./secret_page.html").read_text()
83
84 +
85 # send message to agent (async UI)
67 -@app.route('/msg', methods=['POST'])
86 +@app.route("/msg", methods=["POST"])
87 async def handle_message_async():
88 return await handle_message(False)
89
90 +
91 # send message to agent (synchronous API)
72 -@app.route('/msg_sync', methods=['POST'])
92 +@app.route("/msg_sync", methods=["POST"])
93 async def handle_msg_sync():
94 return await handle_message(True)
95
76 -async def handle_message(sync:bool):
96 +
97 +async def handle_message(sync: bool):
98 try:
78 -
79 - #data sent to the server
99 +
100 + # data sent to the server
101 input = request.get_json()
102 text = input.get("text", "")
103 ctxid = input.get("context", "")
104 blev = input.get("broadcast", 1)
105
85 - #context instance - get or create
106 + # context instance - get or create
107 context = get_context(ctxid)
108
109 # print to console and log
89 - PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message:")
110 + PrintStyle(
111 + background_color="#6C3483", font_color="white", bold=True, padding=True
112 + ).print(f"User message:")
113 PrintStyle(font_color="white", padding=False).print(f"> {text}")
114 context.log.log(type="user", heading="User message", content=text)
115
116 if sync:
117 context.communicate(text)
95 - result = await context.process.result() #type: ignore
118 + result = await context.process.result() # type: ignore
119 response = {
120 "ok": True,
121 "message": result,
99 - }
122 + }
123 else:
124
102 - print("\n\n",(context.process and context.process.is_alive()))
125 + print("\n\n", (context.process and context.process.is_alive()))
126 context.communicate(text)
127 response = {
128 "ok": True,
@@ -111,21 +134,23 @@ async def handle_message(sync:bool):
134 "ok": False,
135 "message": str(e),
136 }
137 + PrintStyle.error(str(e))
138
115 - #respond with json
139 + # respond with json
140 return jsonify(response)
117 -
141 +
142 +
143 # pausing/unpausing the agent
119 -@app.route('/pause', methods=['POST'])
144 +@app.route("/pause", methods=["POST"])
145 async def pause():
146 try:
122 -
123 - #data sent to the server
147 +
148 + # data sent to the server
149 input = request.get_json()
150 paused = input.get("paused", False)
151 ctxid = input.get("context", "")
152
128 - #context instance - get or create
153 + # context instance - get or create
154 context = get_context(ctxid)
155
156 context.paused = paused
@@ -133,100 +158,107 @@ async def pause():
158 response = {
159 "ok": True,
160 "message": "Agent paused." if paused else "Agent unpaused.",
136 - "pause": paused
137 - }
138 -
161 + "pause": paused,
162 + }
163 +
164 except Exception as e:
165 response = {
166 "ok": False,
167 "message": str(e),
168 }
169 + PrintStyle.error(str(e))
170
145 - #respond with json
171 + # respond with json
172 return jsonify(response)
173
174 +
175 # restarting with new agent0
149 -@app.route('/reset', methods=['POST'])
176 +@app.route("/reset", methods=["POST"])
177 async def reset():
178 try:
179
153 - #data sent to the server
180 + # data sent to the server
181 input = request.get_json()
182 ctxid = input.get("context", "")
183
157 - #context instance - get or create
184 + # context instance - get or create
185 context = get_context(ctxid)
186 context.reset()
160 -
187 +
188 response = {
189 "ok": True,
190 "message": "Agent restarted.",
164 - }
165 -
191 + }
192 +
193 except Exception as e:
194 response = {
195 "ok": False,
196 "message": str(e),
197 }
198 + PrintStyle.error(str(e))
199
172 - #respond with json
200 + # respond with json
201 return jsonify(response)
202
203 +
204 # killing context
176 -@app.route('/remove', methods=['POST'])
205 +@app.route("/remove", methods=["POST"])
206 async def remove():
207 try:
208
180 - #data sent to the server
209 + # data sent to the server
210 input = request.get_json()
211 ctxid = input.get("context", "")
212
184 - #context instance - get or create
213 + # context instance - get or create
214 AgentContext.remove(ctxid)
186 -
215 +
216 response = {
217 "ok": True,
218 "message": "Context removed.",
190 - }
191 -
219 + }
220 +
221 except Exception as e:
222 response = {
223 "ok": False,
224 "message": str(e),
225 }
226 + PrintStyle.error(str(e))
227
198 - #respond with json
228 + # respond with json
229 return jsonify(response)
230
231 +
232 # Web UI polling
202 -@app.route('/poll', methods=['POST'])
233 +@app.route("/poll", methods=["POST"])
234 async def poll():
235 try:
205 -
206 - #data sent to the server
236 +
237 + # data sent to the server
238 input = request.get_json()
239 ctxid = input.get("context", uuid.uuid4())
240 from_no = input.get("log_from", 0)
241
211 - #context instance - get or create
242 + # context instance - get or create
243 context = get_context(ctxid)
244
214 -
245 logs = context.log.output(start=from_no)
246
247 # loop AgentContext._contexts
248 ctxs = []
249 for ctx in AgentContext._contexts.values():
220 - ctxs.append({
221 - "id": ctx.id,
222 - "no": ctx.no,
223 - "log_guid": ctx.log.guid,
224 - "log_version": len(ctx.log.updates),
225 - "log_length": len(ctx.log.logs),
226 - "paused": ctx.paused
227 - })
228 -
229 - #data from this server
250 + ctxs.append(
251 + {
252 + "id": ctx.id,
253 + "no": ctx.no,
254 + "log_guid": ctx.log.guid,
255 + "log_version": len(ctx.log.updates),
256 + "log_length": len(ctx.log.logs),
257 + "paused": ctx.paused,
258 + }
259 + )
260 +
261 + # data from this server
262 response = {
263 "ok": True,
264 "context": context.id,
@@ -234,7 +266,8 @@ async def poll():
266 "logs": logs,
267 "log_guid": context.log.guid,
268 "log_version": len(context.log.updates),
237 - "paused": context.paused
269 + "log_progress": context.log.progress,
270 + "paused": context.paused,
271 }
272
273 except Exception as e:
@@ -242,23 +275,22 @@ async def poll():
275 "ok": False,
276 "message": str(e),
277 }
278 + PrintStyle.error(str(e))
279
246 - #respond with json
280 + # respond with json
281 return jsonify(response)
282
283
250 -
251 -#run the internal server
284 +# run the internal server
285 if __name__ == "__main__":
286
254 - load_dotenv()
255 -
287 # Suppress only request logs but keep the startup messages
288 from werkzeug.serving import WSGIRequestHandler
289 +
290 class NoRequestLoggingWSGIRequestHandler(WSGIRequestHandler):
259 - def log_request(self, code='-', size='-'):
291 + def log_request(self, code="-", size="-"):
292 pass # Override to suppress request logging
293
294 # run the server on port from .env
295 port = int(os.environ.get("WEB_UI_PORT", 0)) or None
264 - app.run(request_handler=NoRequestLoggingWSGIRequestHandler,port=port)
296 + app.run(request_handler=NoRequestLoggingWSGIRequestHandler, port=port)
test.py deleted
-293
@@ -1,293 +0,0 @@
1 -from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError
2 -from python.helpers.print_style import PrintStyle
3 -import asyncio
4 -import random
5 -import re
6 -
7 -# List of user agents
8 -user_agents = [
9 - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
10 - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
11 - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
12 - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36',
13 - 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1',
14 - 'Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/91.0.4472.80 Mobile/15E148 Safari/604.1',
15 - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59',
16 - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
17 -]
18 -
19 -async def scrape_page(url, headless=True):
20 - try:
21 - async with async_playwright() as p:
22 - browser = await p.chromium.launch(headless=headless)
23 - context = await browser.new_context(
24 - user_agent=random.choice(user_agents)
25 - )
26 - page = await context.new_page()
27 - page.set_default_timeout(30000)
28 -
29 - await page.goto(url, wait_until='domcontentloaded', timeout=30000)
30 - await page.wait_for_selector('body', timeout=5000)
31 -
32 - result = await page.evaluate(r'''() => {
33 - const cleanText = (text) => {
34 - return text
35 - .replace(/class="[^"]*"/g, '')
36 - .replace(/id="[^"]*"/g, '')
37 - .replace(/<[^>]+>/g, '')
38 - .replace(/\s+/g, ' ')
39 - .trim();
40 - };
41 - const getMetaContent = (name) => {
42 - const meta = document.querySelector(`meta[name="${name}"], meta[property="${name}"]`);
43 - return meta ? cleanText(meta.content) : null;
44 - };
45 - const getMainContent = () => {
46 - const contentSelectors = [
47 - 'main', '#content', '#main-content', '.content',
48 - '.post-content', '.entry-content', '.article-content',
49 - '#mw-content-text', '.mw-parser-output'
50 - ];
51 - for (const selector of contentSelectors) {
52 - const element = document.querySelector(selector);
53 - if (element) {
54 - return cleanText(element.innerText);
55 - }
56 - }
57 - return cleanText(document.body.innerText);
58 - };
59 - const getProducts = () => {
60 - const productSelectors = [
61 - '.s-result-item', '.product-item', '[data-component-type="s-search-result"]', '.sg-col-inner'
62 - ];
63 - for (const selector of productSelectors) {
64 - const elements = document.querySelectorAll(selector);
65 - if (elements.length > 0) {
66 - return Array.from(elements).slice(0, 10).map(product => {
67 - const titleElement = product.querySelector('h2 a, .a-link-normal.a-text-normal');
68 - const priceElement = product.querySelector('.a-price .a-offscreen, .a-price');
69 - const imageElement = product.querySelector('img.s-image');
70 - let price = priceElement ? cleanText(priceElement.textContent) : null;
71 - // Remove duplicate price
72 - price = price ? price.replace(/(\$\d+(?:\.\d{2}?))\1+/, '$1') : null;
73 - return {
74 - title: titleElement ? cleanText(titleElement.textContent) : null,
75 - link: titleElement ? titleElement.href : null,
76 - price: price,
77 - image: imageElement ? imageElement.src : null
78 - };
79 - }).filter(product => product.title && product.link);
80 - }
81 - }
82 - return [];
83 - };
84 - const getNavigation = () => {
85 - const navSelectors = ['nav', 'header', '[data-cy="header-nav"]'];
86 - for (const selector of navSelectors) {
87 - const navElement = document.querySelector(selector);
88 - if (navElement) {
89 - return Array.from(navElement.querySelectorAll('a'))
90 - .filter(link => !link.href.endsWith('.svg'))
91 - .slice(0, 10) // Limit to 10 items
92 - .map(link => ({
93 - href: link.href,
94 - text: cleanText(link.textContent)
95 - }));
96 - }
97 - }
98 - return [];
99 - };
100 - const getImages = () => {
101 - return Array.from(document.querySelectorAll('img'))
102 - .filter(img => !img.src.endsWith('.svg'))
103 - .map(img => ({
104 - src: img.src,
105 - alt: img.alt,
106 - }));
107 - };
108 - const getLists = () => {
109 - const listSelectors = [
110 - '.mw-parser-output > ul', '.mw-parser-output > ol',
111 - '[data-cy="main-content"] ul', '[data-cy="main-content"] ol',
112 - 'main ul', 'main ol',
113 - '.content ul', '.content ol',
114 - '#content ul', '#content ol'
115 - ];
116 - let lists = [];
117 - for (const selector of listSelectors) {
118 - const elements = document.querySelectorAll(selector);
119 - if (elements.length > 0) {
120 - lists = Array.from(elements)
121 - .filter(list => list.children.length >= 3 && list.children.length <= 20)
122 - .map(list => ({
123 - type: list.tagName.toLowerCase(),
124 - items: Array.from(list.children)
125 - .filter(li => li.textContent.trim().length > 10)
126 - .map(li => {
127 - // Remove CSS classes, IDs, and inline styles
128 - let text = li.textContent.replace(/\s+/g, ' ').trim();
129 - // Remove any remaining HTML tags
130 - text = text.replace(/<[^>]+>/g, '');
131 - // Remove CSS-related content
132 - text = text.replace(/\.mw-parser-output[^{]+\{[^}]+\}/g, '');
133 - // Remove ISBN prefix if present
134 - text = text.replace(/^ISBN\s+/, '');
135 - return text;
136 - })
137 - .filter(text => text.length > 0 && !text.includes('mw-parser-output'))
138 - .slice(0, 10)
139 - }))
140 - .filter(list => list.items.length >= 3)
141 - .slice(0, 3);
142 - if (lists.length > 0) break;
143 - }
144 - }
145 - return lists;
146 - };
147 - const getSocialMediaLinks = () => {
148 - const socialSelectors = [
149 - 'a[href*="facebook.com"]',
150 - 'a[href*="twitter.com"]',
151 - 'a[href*="x.com"]',
152 - 'a[href*="github.com"]',
153 - 'a[href*="reddit.com"]',
154 - 'a[href*="tiktok.com"]',
155 - 'a[href*="discord.com"]',
156 - 'a[href*="instagram.com"]',
157 - 'a[href*="linkedin.com"]',
158 - 'a[href*="youtube.com"]',
159 - 'a[href*="pinterest.com"]',
160 - 'a[href*="snapchat.com"]',
161 - 'a[href*="tumblr.com"]',
162 - 'a[href*="medium.com"]',
163 - 'a[href*="whatsapp.com"]',
164 - 'a[href*="telegram.org"]',
165 - 'a[href*="vimeo.com"]',
166 - 'a[href*="flickr.com"]',
167 - 'a[href*="quora.com"]',
168 - 'a[href*="twitch.tv"]'
169 - ];
170 - return socialSelectors.map(selector => {
171 - const element = document.querySelector(selector);
172 - return element ? element.href : null;
173 - }).filter(Boolean);
174 - };
175 - const getCodeScripts = () => {
176 - const codeBlocks = document.querySelectorAll('pre code, .highlight pre, .code-block');
177 - return Array.from(codeBlocks).map(block => ({
178 - language: block.className.match(/language-(\w+)/)?.[1] || 'text',
179 - code: cleanText(block.textContent)
180 - })).filter(script => script.code.length > 0);
181 - };
182 - return {
183 - url: window.location.href,
184 - title: document.title,
185 - author: getMetaContent('author'),
186 - publishDate: getMetaContent('article:published_time') || getMetaContent('date'),
187 - lastModified: document.lastModified,
188 - keywords: getMetaContent('keywords'),
189 - metaDescription: getMetaContent('description') || getMetaContent('og:description'),
190 - mainContent: getMainContent(),
191 - navigation: getNavigation(),
192 - images: getImages(),
193 - lists: getLists(),
194 - products: getProducts(),
195 - socialMediaLinks: getSocialMediaLinks(),
196 - codeScripts: getCodeScripts(),
197 - };
198 - }''')
199 -
200 - await browser.close()
201 - return result
202 - except PlaywrightTimeoutError as e:
203 - PrintStyle(font_color="yellow", padding=True).print(f"Attempt {attempt + 1} failed due to timeout: {str(e)}")
204 - except Exception as e:
205 - PrintStyle(font_color="red", padding=True).print(f"Attempt {attempt + 1} failed: {str(e)}")
206 -
207 -
208 -async def scrape_url(url, headless):
209 - try:
210 - result = await scrape_page(url, headless)
211 -
212 - if result is None:
213 - raise ValueError("Scraping result is None")
214 -
215 - markdown_content = f"# {result.get('title', 'No Title')}\n\n"
216 -
217 - if result.get('url'):
218 - markdown_content += f"URL: {result['url']}\n\n"
219 -
220 - if result.get('author'):
221 - markdown_content += f"Author: {result['author']}\n\n"
222 -
223 - if result.get('publishDate'):
224 - markdown_content += f"Published: {result['publishDate']}\n\n"
225 -
226 - if result.get('keywords'):
227 - markdown_content += f"Keywords: {result['keywords']}\n\n"
228 -
229 - if result.get('metaDescription'):
230 - markdown_content += f"## Description\n\n{result['metaDescription']}\n\n"
231 -
232 - if result.get('mainContent'):
233 - markdown_content += "## Webpage Content:\n\n"
234 - markdown_content += result['mainContent'] + "\n\n"
235 -
236 - if result.get('lists'):
237 - markdown_content += "## Lists\n\n"
238 - for list_item in result['lists']:
239 - markdown_content += f"### {list_item['type'].upper()} List\n\n"
240 - for item in list_item['items']:
241 - markdown_content += f"- {item}\n"
242 - markdown_content += "\n"
243 -
244 - if result.get('products'):
245 - markdown_content += "## Products\n\n"
246 - for product in result['products']:
247 - markdown_content += f"### {product.get('title', 'Untitled Product')}\n\n"
248 - if product.get('price'):
249 - price = product['price']
250 - price = re.sub(r'(\$\d+(?:\.\d{2})?)\1+', r'\1', price)
251 - markdown_content += f"Price: {price}\n\n"
252 - if product.get('link'):
253 - markdown_content += f"[View Product]({product['link']})\n\n"
254 - if product.get('image'):
255 - markdown_content += f"![Product Image]({product['image']})\n\n"
256 - markdown_content += "---\n\n"
257 -
258 - if result.get('socialMediaLinks'):
259 - markdown_content += "## Social Media Links\n\n"
260 - for link in result['socialMediaLinks']:
261 - markdown_content += f"- [{link.split('.com')[0].split('/')[-1].capitalize()}]({link})\n"
262 - markdown_content += "\n"
263 -
264 - if result.get('codeScripts'):
265 - markdown_content += "## Code Snippet\n\n"
266 - for script in result['codeScripts']:
267 - markdown_content += f"```{script['language']}\n{script['code']}\n```\n\n"
268 -
269 - markdown_content = markdown_content.strip()
270 -
271 - return markdown_content
272 - except Exception as e:
273 - return f"Error: Failed to scrape URL. Reason: {str(e)}"
274 -
275 -async def fetch_page_content(url: str, max_retries: int = 2, headless: bool = True):
276 - for attempt in range(max_retries):
277 - try:
278 - content = await scrape_url(url, headless)
279 - if content.startswith("Error:"):
280 - raise Exception(content)
281 - return str(content)
282 - except Exception as e:
283 - PrintStyle(font_color="red", padding=True).print(f"Attempt {attempt + 1} failed: {str(e)}")
284 -
285 - raise Exception("Error: Webpage content is not available.")
286 -
287 -
288 -async def test():
289 - url = "https://github.com/frdel/agent-zero"
290 - content = await fetch_page_content(url)
291 - print(content)
292 -
293 -asyncio.run(test())
\ No newline at end of file
test2.py deleted
-120
@@ -1,120 +0,0 @@
1 -import requests
2 -from bs4 import BeautifulSoup
3 -import random
4 -import re
5 -
6 -# List of user agents
7 -user_agents = [
8 - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
9 - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15',
10 - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
11 - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36',
12 - 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1',
13 - 'Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/91.0.4472.80 Mobile/15E148 Safari/604.1',
14 - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59',
15 - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
16 -]
17 -
18 -def clean_text(text):
19 - """Utility function to clean HTML tags and whitespace."""
20 - text = re.sub(r'class="[^"]*"', '', text)
21 - text = re.sub(r'id="[^"]*"', '', text)
22 - text = re.sub(r'<[^>]+>', '', text)
23 - return re.sub(r'\s+', ' ', text).strip()
24 -
25 -def scrape_page(url):
26 - headers = {
27 - 'User-Agent': random.choice(user_agents)
28 - }
29 -
30 - try:
31 - response = requests.get(url, headers=headers, timeout=30)
32 - response.raise_for_status() # Check if the request was successful
33 - soup = BeautifulSoup(response.content, 'html.parser')
34 -
35 - # Extract title
36 - title = soup.title.string if soup.title else 'No Title'
37 -
38 - # Extract meta information
39 - def get_meta_content(name):
40 - meta = soup.find('meta', attrs={'name': name}) or soup.find('meta', property=name)
41 - return meta['content'] if meta else None
42 -
43 - author = get_meta_content('author')
44 - publish_date = get_meta_content('article:published_time') or get_meta_content('date')
45 - keywords = get_meta_content('keywords')
46 - description = get_meta_content('description') or get_meta_content('og:description')
47 -
48 - # Extract main content
49 - def get_main_content():
50 - content_selectors = ['main', '#content', '#main-content', '.content', '.post-content', '.entry-content', '.article-content', '#mw-content-text', '.mw-parser-output']
51 - for selector in content_selectors:
52 - element = soup.select_one(selector)
53 - if element:
54 - return clean_text(element.get_text())
55 - return clean_text(soup.body.get_text() if soup.body else "")
56 -
57 - # Extract images
58 - def get_images():
59 - images = []
60 - for img in soup.find_all('img'):
61 - if not img.get('src', '').endswith('.svg'):
62 - images.append({
63 - 'src': img['src'],
64 - 'alt': img.get('alt', '')
65 - })
66 - return images
67 -
68 - # Extract product listings
69 - def get_products():
70 - products = []
71 - product_selectors = ['.s-result-item', '.product-item', '[data-component-type="s-search-result"]', '.sg-col-inner']
72 - for selector in product_selectors:
73 - elements = soup.select(selector)
74 - if elements:
75 - for product in elements[:10]:
76 - title_element = product.select_one('h2 a, .a-link-normal.a-text-normal')
77 - price_element = product.select_one('.a-price .a-offscreen, .a-price')
78 - image_element = product.select_one('img.s-image')
79 - price = clean_text(price_element.text) if price_element else None
80 - price = re.sub(r'(\$\d+(?:\.\d{2})?)\1+', r'\1', price) if price else None
81 - products.append({
82 - 'title': clean_text(title_element.text) if title_element else None,
83 - 'link': title_element['href'] if title_element else None,
84 - 'price': price,
85 - 'image': image_element['src'] if image_element else None
86 - })
87 - return products
88 -
89 - # Extract lists
90 - def get_lists():
91 - lists = []
92 - list_selectors = ['.mw-parser-output > ul', '.mw-parser-output > ol', 'main ul', 'main ol', '.content ul', '.content ol']
93 - for selector in list_selectors:
94 - elements = soup.select(selector)
95 - for element in elements:
96 - list_items = [clean_text(li.get_text()) for li in element.find_all('li') if len(li.get_text().strip()) > 10]
97 - if 3 <= len(list_items) <= 20:
98 - lists.append({'type': element.name, 'items': list_items[:10]})
99 - return lists
100 -
101 - # Gather all extracted data
102 - return {
103 - 'url': url,
104 - 'title': title,
105 - 'author': author,
106 - 'publishDate': publish_date,
107 - 'keywords': keywords,
108 - 'metaDescription': description,
109 - 'mainContent': get_main_content(),
110 - 'images': get_images(),
111 - 'products': get_products(),
112 - 'lists': get_lists()
113 - }
114 -
115 - except requests.exceptions.RequestException as e:
116 - return {"error": str(e)}
117 -
118 -# Example usage:
119 -result = scrape_page('https://github.com/frdel/agent-zero')
120 -print(result)
webui/index.css
+77 -13
@@ -175,7 +175,8 @@ h3 {
175 margin-top: var(--spacing-lg);
176 }
177 h4 {
178 - margin-top: 0.625rem;
178 + margin-top: auto;
179 + margin-bottom: auto;
180 }
181
182 #a0version {
@@ -239,12 +240,44 @@ h4 {
240
241 #logo-container img {
242 border-radius: var(--spacing-xs);
242 - height: auto;
243 - width: 3rem;
243 + width: auto;
244 + height: 3rem;
245 filter: none;
246 transition: filter 0.3s ease;
247 }
248
249 +#progress-bar-box{
250 + background-color: var(--color-panel);
251 + /* padding-left: 1em;
252 + padding-right: 1em;
253 + padding-top: 0.5em;
254 + padding-bottom: 0; */
255 + padding: var(--spacing-sm) var(--spacing-md);
256 + padding-bottom: 0;
257 +}
258 +
259 +#progress-bar-h {
260 + color: var(--color-primary);
261 + display: flex;
262 + align-items: left;
263 + justify-content: flex-start;
264 + height: 1.2em;
265 + text-wrap: ellipsis;
266 + overflow: hidden;
267 + font-weight: normal;
268 +}
269 +
270 +#progress-bar-i {
271 + font-weight: bold;
272 + padding-right: 0.5em;
273 + color: var(--color-secondary)
274 +}
275 +
276 +.progress-bar h4{
277 + margin-left: 1em;
278 + margin-right: 1.2em;
279 +}
280 +
281 #right-panel.expanded #logo-container {
282 margin-left: 5.5rem;
283 }
@@ -271,6 +304,11 @@ h4 {
304 align-self: flex-start;
305 }
306
307 +.center-container {
308 + align-self: center;
309 + max-width: 80%;
310 +}
311 +
312 .message-user {
313 background-color: #4a4a4a;
314 border-bottom-right-radius: var(--spacing-xs);
@@ -281,10 +319,28 @@ h4 {
319 }
320
321 /* Message Types */
284 -.message-fw {
322 +/* .message-fw {
323 border-radius: var(--border-radius);
324 border-top-left-radius: var(--spacing-xs);
325 margin-left: var(--spacing-lg);
326 +} */
327 +
328 +.message-center{
329 + align-self: center;
330 + border-bottom-left-radius: unset;
331 +}
332 +
333 +.message-followup{
334 + margin-left: 2em;
335 + margin-bottom: 2em;
336 +}
337 +.message-followup .message{
338 + border-radius: 1.125em; /* 18px */
339 + border-top-left-radius: 0.3125em; /* 5px */
340 +}
341 +
342 +.message-followup + .message-followup {
343 + margin-bottom: 0;
344 }
345
346 /* Update message types for dark mode */
@@ -294,8 +350,8 @@ h4 {
350 .message-agent-delegation { background-color: #00695C; color: #E0E0E0; }
351 .message-tool { background-color: #5D4C6A; color: #E0E0E0; }
352 .message-code-exe { background-color: #3a147c; color: #E0E0E0; }
297 -.message-adhoc { background-color: #cf7b0e; color: #E0E0E0; }
353 .message-info { background-color: var(--color-panel); color: #E0E0E0; }
354 +.message-util { background-color: #23211a; color: #E0E0E0; display:none }
355 .message-warning { background-color: #c2771b; color: #E0E0E0; }
356 .message-error { background-color: #ab1313; color: #E0E0E0; }
357
@@ -306,22 +362,23 @@ h4 {
362 margin-bottom: var(--spacing-xs);
363 opacity: 0.7;
364 }
309 -
310 -.ai-info {
365 +.msg-kvps {
366 + font-size: 0.9em;
367 + margin-bottom: 0.625em; /* 10px */
368 border-collapse: collapse;
369 font-size: 0.9em;
370 margin-bottom: var(--spacing-sm);
371 width: 100%;
372 }
373
317 -.ai-info th,
318 -.ai-info td {
374 +.msg-kvps th,
375 +.msg-kvps td {
376 border-bottom: 1px solid rgba(255, 255, 255, 0.1);
377 padding: 0.25rem;
378 text-align: left;
379 }
380
324 -.ai-info th {
381 +.msg-kvps th {
382 color: var(--color-primary);
383 width: 40%;
384 }
@@ -564,8 +621,8 @@ input:checked + .slider:before {
621 .light-mode .message-agent-delegation { background-color: #E0F2F1; color: #00695C; }
622 .light-mode .message-tool { background-color: #EDE7F6; color: #5D4C6A; }
623 .light-mode .message-code-exe { background-color: #FCE4EC; color: #3a147c; }
567 -.light-mode .message-adhoc { background-color: #FFF3E0; color: #cf7b0e; }
624 .light-mode .message-info { background-color: #E8EAF6; color: #2C3E50; }
625 +.light-mode .message-util { background-color: #e8eaf6d6; color: #353c43; }
626 .light-mode .message-warning { background-color: #FFF3E0; color: #c2771b; }
627 .light-mode .message-error { background-color: #FFEBEE; color: #ab1313; }
628 .light-mode .message-user {
@@ -663,14 +720,21 @@ input:checked + .slider:before {
720 }
721
722 .msg-thoughts{
666 - display: auto;
723 + display: auto;
724 +}
725 +
726 +.message-util .msg-kvps{
727 +}
728 +
729 +.message-util .msg-content{
730 +
731 }
732
733 .message-temp {
734 display: none;
735 }
736
673 -.message-temp:last-child {
737 +.message-temp:not([style*="display: none"]):last-of-type {
738 display: block; /* or any style you want for visibility */
739 }
740
webui/index.html
+69 -46
@@ -6,6 +6,7 @@
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 + <link rel="stylesheet" href="toast.css">
10
11 <script>
12 window.safeCall = function (name, ...args) {
@@ -23,42 +24,44 @@
24 <!--Sidebar-->
25 <button id="toggle-sidebar" class="toggle-sidebar-button" aria-label="Toggle Sidebar" aria-expanded="false">
26 <span aria-hidden="true">
26 - <svg id="sidebar-hamburger-svg" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="CurrentColor">
27 - <path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/>
28 - </svg>
27 + <svg id="sidebar-hamburger-svg" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
28 + viewBox="0 0 24 24" fill="CurrentColor">
29 + <path
30 + d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z" />
31 + </svg>
32 </span>
30 - </button>
33 + </button>
34 </div>
35 <div id="left-panel" class="panel">
36 <div class="left-panel-top">
34 - <!--Sidebar upper elements-->
35 - <div class="config-section" id="status-section" x-data="{ connected: true }">
36 - <h3>Status</h3>
37 - <h4 class="connected" x-show="connected">&#10004; Connected</h4>
38 - <h4 class="disconnected" x-show="!connected">&#10008; Disconnected</h4>
39 - </div>
37 + <!--Sidebar upper elements-->
38 + <div class="config-section" id="status-section" x-data="{ connected: true }">
39 + <h3>Status</h3>
40 + <h4 class="connected" x-show="connected">&#10004; Connected</h4>
41 + <h4 class="disconnected" x-show="!connected">&#10008; Disconnected</h4>
42 + </div>
43
41 - <div class="config-section" x-data="{ showQuickActions: true }">
42 - <h3>Quick Actions</h3>
43 - <button class="config-button" id="resetChat" @click="resetChat()">Reset chat</button>
44 - <button class="config-button" id="newChat" @click="newChat()">New Chat</button>
45 - </div>
44 + <div class="config-section" x-data="{ showQuickActions: true }">
45 + <h3>Quick Actions</h3>
46 + <button class="config-button" id="resetChat" @click="resetChat()">Reset chat</button>
47 + <button class="config-button" id="newChat" @click="newChat()">New Chat</button>
48 + </div>
49
47 - <div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }" x-show="contexts.length > 0">
48 - <h3>Chats</h3>
49 - <ul class="config-list">
50 - <template x-for="context in contexts">
51 - <li>
52 - <span
53 - :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
54 - @click="selected = context.id; selectChat(context.id)">
55 - Chat #<span x-text="context.no"></span>
56 - </span>
57 - <button class="edit-button" @click="killChat(context.id)">X</button>
58 - </li>
59 - </template>
60 - </ul>
61 - </div>
50 + <div class="config-section" id="chats-section" x-data="{ contexts: [], selected: '' }"
51 + x-show="contexts.length > 0">
52 + <h3>Chats</h3>
53 + <ul class="config-list">
54 + <template x-for="context in contexts">
55 + <li>
56 + <span :class="{'chat-list-button': true, 'font-bold': context.id === selected}"
57 + @click="selected = context.id; selectChat(context.id)">
58 + Chat #<span x-text="context.no"></span>
59 + </span>
60 + <button class="edit-button" @click="killChat(context.id)">X</button>
61 + </li>
62 + </template>
63 + </ul>
64 + </div>
65 </div>
66 <!--Preferences-->
67 <div class="pref-section">
@@ -89,47 +92,67 @@
92 <span class="slider"></span>
93 </label>
94 </li>
92 - <li x-data="{ darkMode: localStorage.getItem('darkMode') === 'true' }" x-init="$watch('darkMode', val => toggleDarkMode(val))">
93 - <div class="switch-label">Light/Dark mode</div>
95 + <li x-data="{ darkMode: localStorage.getItem('darkMode') != 'false' }"
96 + x-init="$watch('darkMode', val => toggleDarkMode(val))">
97 + <div class="switch-label">Dark mode</div>
98 <label class="switch">
95 - <input type="checkbox" x-model="darkMode">
96 - <span class="slider"></span>
99 + <input type="checkbox" x-model="darkMode">
100 + <span class="slider"></span>
101 + </label>
102 + </li>
103 + <li x-data="{ showUtils: false }">
104 + <span>Show utility messages</span>
105 + <label class="switch">
106 + <input type="checkbox" x-model="showUtils"
107 + x-effect="window.safeCall('toggleUtils',showUtils)">
108 + <span class="slider"></span>
109 </label>
98 - </li>
110 + </li>
111 +
112 </ul>
100 - <span id="a0version">Agent Zero 0.6.5<br>built on 9/13/2024</span>
101 - </div>
113 + <span id="a0version">Agent Zero 0.7<br>built on 2024-09-25</span>
114 </div>
115 + </div>
116 <div id="right-panel" class="panel">
117 <div id="logo-container">
118 <a href="https://github.com/frdel/agent-zero" target="_blank" rel="noopener noreferrer">
119 <img src="splash.jpg" alt="a0" width="48" height="48">
120 </a>
108 - <div id="time-date"></div>
109 - </div>
121 + <div id="time-date"></div>
122 + </div>
123 <!--Chat-->
124 <div id="chat-history">
125 </div>
126 + <div id="progress-bar-box">
127 + <h4 id="progress-bar-h"><span id="progress-bar-i">|></span><span id="progress-bar"></span></h4>
128 + </div>
129 + <div id="toast" class="toast">
130 + <div class="toast__message"></div>
131 + <button class="toast__copy">Copy</button>
132 + <button class="toast__close">Close</button>
133 + </div>
134 <div id="input-section" x-data="{ paused: false }">
135 <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
136 <button class="chat-button" id="send-button" aria-label="Send message">
137 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
138 <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15" />
118 - </svg>
139 + </svg>
140 </button>
120 - <button class="chat-button pause-button" id="pause-button" @click="pauseAgent(true)" x-show="!paused" aria-label="Pause agent">
141 + <button class="chat-button pause-button" id="pause-button" @click="pauseAgent(true)" x-show="!paused"
142 + aria-label="Pause agent">
143 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
122 - <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
144 + <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
145 </svg>
146 </button>
125 - <button class="chat-button pause-button" id="unpause-button" @click="pauseAgent(false)" x-show="paused" aria-label="Resume agent">
147 + <button class="chat-button pause-button" id="unpause-button" @click="pauseAgent(false)" x-show="paused"
148 + aria-label="Resume agent">
149 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
127 - <path d="M8 5v14l11-7z"/>
150 + <path d="M8 5v14l11-7z" />
151 </svg>
152 </button>
153 + </div>
154 </div>
155 </div>
132 - </div>
156 </body>
157
135 -</html>
158 +</html>
\ No newline at end of file
webui/index.js
+126 -49
@@ -9,70 +9,87 @@ const sendButton = document.getElementById('send-button');
9 const inputSection = document.getElementById('input-section');
10 const statusSection = document.getElementById('status-section');
11 const chatsSection = document.getElementById('chats-section');
12 -const scrollbarThumb = document.querySelector('#chat-history::-webkit-scrollbar-thumb');
12 +const scrollbarThumb = document.querySelector('#chat-history::-webkit-scrollbar-thumb');
13 +const progressBar = document.getElementById('progress-bar');
14 +
15 +
16
17 let autoScroll = true;
18 let context = "";
19
20 // Initialize the toggle button
18 -setupSidebarToggle();
21 +setupSidebarToggle();
22
23 function isMobile() {
24 return window.innerWidth <= 768;
22 - }
23 -
24 - function toggleSidebar() {
25 +}
26 +
27 +function toggleSidebar() {
28 leftPanel.classList.toggle('hidden');
29 rightPanel.classList.toggle('expanded');
27 - }
28 -
29 - function handleResize() {
30 +}
31 +
32 +function handleResize() {
33 if (isMobile()) {
31 - leftPanel.classList.add('hidden');
32 - rightPanel.classList.add('expanded');
34 + leftPanel.classList.add('hidden');
35 + rightPanel.classList.add('expanded');
36 } else {
34 - leftPanel.classList.remove('hidden');
35 - rightPanel.classList.remove('expanded');
37 + leftPanel.classList.remove('hidden');
38 + rightPanel.classList.remove('expanded');
39 }
37 - }
38 -
39 - // Run on startup and window resize
40 - window.addEventListener('load', handleResize);
41 - window.addEventListener('resize', handleResize);
42 -
43 - function setupSidebarToggle() {
40 +}
41 +
42 +// Run on startup and window resize
43 +window.addEventListener('load', handleResize);
44 +window.addEventListener('resize', handleResize);
45 +
46 +function setupSidebarToggle() {
47 const leftPanel = document.getElementById('left-panel');
48 const rightPanel = document.getElementById('right-panel');
49 const toggleSidebarButton = document.getElementById('toggle-sidebar');
50 if (toggleSidebarButton) {
48 - toggleSidebarButton.addEventListener('click', toggleSidebar);
51 + toggleSidebarButton.addEventListener('click', toggleSidebar);
52 } else {
50 - console.error('Toggle sidebar button not found');
51 - setTimeout(setupSidebarToggle, 100);
53 + console.error('Toggle sidebar button not found');
54 + setTimeout(setupSidebarToggle, 100);
55 }
53 - }
54 - // Make sure to call this function
55 - document.addEventListener('DOMContentLoaded', setupSidebarToggle);
56 +}
57 +// Make sure to call this function
58 +document.addEventListener('DOMContentLoaded', setupSidebarToggle);
59
60 async function sendMessage() {
58 - const message = chatInput.value.trim();
59 - if (message) {
60 -
61 - const response = await sendJsonData("/msg", { text: message, context });
62 -
63 - //setMessage('user', message);
64 - chatInput.value = '';
65 - adjustTextareaHeight();
61 + try {
62 + const message = chatInput.value.trim();
63 + if (message) {
64 +
65 + const response = await sendJsonData("/msg", { text: message, context });
66 +
67 + if (!response) {
68 + toast("No response returned.", "error")
69 + } else if (!response.ok) {
70 + if (response.message) {
71 + toast(response.message, "error")
72 + } else {
73 + toast("Undefined error.", "error")
74 + }
75 + }
76 +
77 + //setMessage('user', message);
78 + chatInput.value = '';
79 + adjustTextareaHeight();
80 + }
81 + } catch (e) {
82 + toast(e.message, "error")
83 }
84 }
68 -
85 +
86 chatInput.addEventListener('keydown', (e) => {
87 if (e.key === 'Enter' && !e.shiftKey) {
88 e.preventDefault();
89 sendMessage();
90 }
91 });
75 -
92 +
93 sendButton.addEventListener('click', sendMessage);
94
95 function updateUserTime() {
@@ -81,7 +98,7 @@ function updateUserTime() {
98 const minutes = now.getMinutes();
99 const seconds = now.getSeconds();
100 const ampm = hours >= 12 ? 'pm' : 'am';
84 - const formattedHours = hours % 12 || 12;
101 + const formattedHours = hours % 12 || 12;
102
103 // Format the time
104 const timeString = `${formattedHours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')} ${ampm}`;
@@ -94,11 +111,11 @@ function updateUserTime() {
111 const userTimeElement = document.getElementById('time-date');
112 userTimeElement.innerHTML = `${timeString}<br><span id="user-date">${dateString}</span>`;
113 }
97 -
114 +
115 updateUserTime();
116 setInterval(updateUserTime, 1000);
100 -
101 -function setMessage(id, type, heading, content, kvps = null) {
117 +
118 +function setMessage(id, type, heading, content, temp, kvps = null) {
119 // Search for the existing message container by id
120 let messageContainer = document.getElementById(`message-${id}`);
121
@@ -181,6 +198,8 @@ async function poll() {
198 }
199 }
200
201 + updateProgress(response.log_progress)
202 +
203 //set ui model vars from backend
204 const inputAD = Alpine.$data(inputSection);
205 inputAD.paused = response.paused;
@@ -202,10 +221,18 @@ async function poll() {
221 }
222 }
223
224 +function updateProgress(progress) {
225 + if (!progress) progress = "Waiting for input"
226 +
227 + if (progressBar.innerHTML != progress) {
228 + progressBar.innerHTML = progress
229 + }
230 +}
231 +
232 function updatePauseButtonState(isPaused) {
233 const pauseButton = document.getElementById('pause-button');
234 const unpauseButton = document.getElementById('unpause-button');
208 -
235 +
236 if (isPaused) {
237 pauseButton.style.display = 'none';
238 unpauseButton.style.display = 'flex';
@@ -277,22 +304,38 @@ window.toggleThoughts = async function (showThoughts) {
304 toggleCssProperty('.msg-thoughts', 'display', showThoughts ? undefined : 'none');
305 }
306
307 +window.toggleUtils = async function (showUtils) {
308 + // add display:none to .msg-json class definition
309 + toggleCssProperty('.message-util', 'display', showUtils ? undefined : 'none');
310 + // toggleCssProperty('.message-util .msg-kvps', 'display', showUtils ? undefined : 'none');
311 + // toggleCssProperty('.message-util .msg-content', 'display', showUtils ? undefined : 'none');
312 +}
313
281 -window.toggleDarkMode = function(isDark) {
314 +window.toggleDarkMode = function (isDark) {
315 if (isDark) {
283 - document.body.classList.remove('light-mode');
316 + document.body.classList.remove('light-mode');
317 } else {
285 - document.body.classList.add('light-mode');
318 + document.body.classList.add('light-mode');
319 }
320 console.log("Dark mode:", isDark);
321 localStorage.setItem('darkMode', isDark);
322 };
290 -
291 - // Modify this part
292 - document.addEventListener('DOMContentLoaded', () => {
293 - const isDarkMode = localStorage.getItem('darkMode') === 'true';
323 +
324 +// Modify this part
325 +document.addEventListener('DOMContentLoaded', () => {
326 + const isDarkMode = localStorage.getItem('darkMode') !== 'false';
327 toggleDarkMode(isDarkMode);
295 - });
328 +});
329 +
330 +window.toggleDarkMode = function (isDark) {
331 + if (isDark) {
332 + document.body.classList.remove('light-mode');
333 + } else {
334 + document.body.classList.add('light-mode');
335 + }
336 + console.log("Dark mode:", isDark);
337 + localStorage.setItem('darkMode', isDark);
338 +};
339
340 function toggleCssProperty(selector, property, value) {
341 // Get the stylesheet that contains the class
@@ -310,7 +353,7 @@ function toggleCssProperty(selector, property, value) {
353 if (value === undefined) {
354 rule.style.removeProperty(property);
355 } else {
313 - rule.style.setProperty(property, value);
356 + rule.style.setProperty(property, value);
357 }
358 return;
359 }
@@ -318,6 +361,40 @@ function toggleCssProperty(selector, property, value) {
361 }
362 }
363
364 +function toast(text, type = 'info') {
365 + const toast = document.getElementById('toast');
366 +
367 + // Update the toast content and type
368 + toast.querySelector('#toast .toast__message').textContent = text;
369 + toast.className = `toast toast--${type}`;
370 + toast.style.display = 'flex';
371 +
372 + // Add the close button event listener
373 + const closeButton = toast.querySelector('#toast .toast__close');
374 + closeButton.onclick = () => {
375 + toast.style.display = 'none';
376 + clearTimeout(toast.timeoutId);
377 + };
378 +
379 + // Add the copy button event listener
380 + const copyButton = toast.querySelector('#toast .toast__copy');
381 + copyButton.onclick = () => {
382 + navigator.clipboard.writeText(text);
383 + copyButton.textContent = 'Copied!';
384 + setTimeout(() => {
385 + copyButton.textContent = 'Copy';
386 + }, 2000);
387 + };
388 +
389 + // Clear any existing timeout
390 + clearTimeout(toast.timeoutId);
391 +
392 + // Automatically close the toast after 5 seconds
393 + toast.timeoutId = setTimeout(() => {
394 + toast.style.display = 'none';
395 + }, 10000);
396 +}
397 +
398 chatInput.addEventListener('input', adjustTextareaHeight);
399
400 setInterval(poll, 250);
webui/messages.js
+23 -18
@@ -18,8 +18,8 @@ export function getHandler(type) {
18 return drawMessageError;
19 case 'info':
20 return drawMessageInfo;
21 - case 'adhoc':
22 - return drawMessageAdhoc;
21 + case 'util':
22 + return drawMessageUtil;
23 case 'hint':
24 return drawMessageInfo;
25 default:
@@ -27,7 +27,7 @@ export function getHandler(type) {
27 }
28 }
29
30 -export function _drawMessage(messageContainer, heading, content, temp, kvps = null, messageClasses = [], contentClasses = []) {
30 +export function _drawMessage(messageContainer, heading, content, temp, followUp, kvps = null, messageClasses = [], contentClasses = []) {
31
32
33 // if (type !== 'user') {
@@ -48,10 +48,12 @@ export function _drawMessage(messageContainer, heading, content, temp, kvps = nu
48 textNode.textContent = content;
49 textNode.style.whiteSpace = 'pre-wrap';
50 textNode.style.wordBreak = 'break-word';
51 - textNode.classList.add("message-content", ...contentClasses)
51 + textNode.classList.add("msg-content", ...contentClasses)
52 messageDiv.appendChild(textNode);
53 messageContainer.appendChild(messageDiv);
54
55 + if (followUp) messageContainer.classList.add("message-followup")
56 +
57 // if (type !== 'user') {
58 // const actions = document.createElement('div');
59 // actions.classList.add('message-actions');
@@ -63,51 +65,53 @@ export function _drawMessage(messageContainer, heading, content, temp, kvps = nu
65 }
66
67 export function drawMessageDefault(messageContainer, id, type, heading, content, temp, kvps = null) {
66 - _drawMessage(messageContainer, heading, content, temp, kvps, ['message-ai', 'message-default'], ['msg-json']);
68 + _drawMessage(messageContainer, heading, content, temp, false, kvps, ['message-ai', 'message-default'], ['msg-json']);
69 }
70
71 export function drawMessageAgent(messageContainer, id, type, heading, content, temp, kvps = null) {
70 - let kvpsFlat=null
72 + let kvpsFlat = null
73 if (kvps) {
74 kvpsFlat = { ...kvps, ...kvps['tool_args'] || {} }
75 delete kvpsFlat['tool_args']
76 }
77
76 - _drawMessage(messageContainer, heading, content, temp, kvpsFlat, ['message-ai', 'message-agent'], ['msg-json']);
78 + _drawMessage(messageContainer, heading, content, temp, false, kvpsFlat, ['message-ai', 'message-agent'], ['msg-json']);
79 }
80
81 export function drawMessageResponse(messageContainer, id, type, heading, content, temp, kvps = null) {
80 - _drawMessage(messageContainer, heading, content, temp, null, ['message-ai', 'message-agent-response', 'message-fw']);
82 + _drawMessage(messageContainer, heading, content, temp, true, null, ['message-ai', 'message-agent-response']);
83 }
84
85 export function drawMessageDelegation(messageContainer, id, type, heading, content, temp, kvps = null) {
84 - _drawMessage(messageContainer, heading, content, temp, kvps, ['message-ai', 'message-agent', 'message-fw', 'message-agent-delegation']);
86 + _drawMessage(messageContainer, heading, content, temp, true, kvps, ['message-ai', 'message-agent', 'message-agent-delegation']);
87 }
88
89 export function drawMessageUser(messageContainer, id, type, heading, content, temp, kvps = null) {
88 - _drawMessage(messageContainer, heading, content, temp, kvps, ['message-user']);
90 + _drawMessage(messageContainer, heading, content, temp, false, kvps, ['message-user']);
91 }
92
93 export function drawMessageTool(messageContainer, id, type, heading, content, temp, kvps = null) {
92 - _drawMessage(messageContainer, heading, content, temp, kvps, ['message-ai', 'message-tool', 'message-fw'], ['msg-output']);
94 + _drawMessage(messageContainer, heading, content, temp, true, kvps, ['message-ai', 'message-tool'], ['msg-output']);
95 }
96
97 export function drawMessageCodeExe(messageContainer, id, type, heading, content, temp, kvps = null) {
96 - _drawMessage(messageContainer, heading, content, temp, null, ['message-ai', 'message-code-exe', 'message-fw']);
98 + _drawMessage(messageContainer, heading, content, temp, true, null, ['message-ai', 'message-code-exe']);
99 }
100
101 export function drawMessageAgentPlain(classes, messageContainer, id, type, heading, content, temp, kvps = null) {
100 - _drawMessage(messageContainer, heading, content, temp, null, ['message-ai', ...classes]);
101 -}
102 -
103 -export function drawMessageAdhoc(messageContainer, id, type, heading, content, temp, kvps = null) {
104 - return drawMessageAgentPlain(['message-adhoc'], messageContainer, id, type, heading, content, temp, kvps);
102 + _drawMessage(messageContainer, heading, content, temp, false, null, [...classes]);
103 + messageContainer.classList.add('center-container')
104 }
105
106 export function drawMessageInfo(messageContainer, id, type, heading, content, temp, kvps = null) {
107 return drawMessageAgentPlain(['message-info'], messageContainer, id, type, heading, content, temp, kvps);
108 }
109
110 +export function drawMessageUtil(messageContainer, id, type, heading, content, temp, kvps = null) {
111 + _drawMessage(messageContainer, heading, content, temp, false, kvps, ['message-util'], ['msg-json']);
112 + messageContainer.classList.add('center-container')
113 +}
114 +
115 export function drawMessageWarning(messageContainer, id, type, heading, content, temp, kvps = null) {
116 return drawMessageAgentPlain(['message-warning'], messageContainer, id, type, heading, content, temp, kvps);
117 }
@@ -119,9 +123,10 @@ export function drawMessageError(messageContainer, id, type, heading, content, t
123 function drawKvps(container, kvps) {
124 if (kvps) {
125 const table = document.createElement('table');
122 - table.classList.add('ai-info');
126 + table.classList.add('msg-kvps');
127 for (let [key, value] of Object.entries(kvps)) {
128 const row = table.insertRow();
129 + row.classList.add('kvps-row');
130 if (key == "thoughts") row.classList.add('msg-thoughts');
131
132 const th = row.insertCell();
webui/toast.css new
+42
@@ -0,0 +1,42 @@
1 +#toast {
2 + /* position: fixed;
3 + bottom: 20px;
4 + left: 50%;
5 + transform: translateX(-50%); */
6 + margin: 0.5em;
7 + background-color: #333;
8 + color: #fff;
9 + padding: 0.3em;
10 + border-radius: 0.3125em;
11 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
12 + display: none;
13 + align-items: center;
14 + z-index: 9999;
15 + }
16 +
17 + #toast.toast--success {
18 + background-color: #4CAF50;
19 + }
20 +
21 + #toast.toast--error {
22 + background-color: #731811;
23 + }
24 +
25 + #toast.toast--info {
26 + background-color: #2196F3;
27 + }
28 +
29 + .toast__message {
30 + margin-right: 16px;
31 + flex-grow: 1;
32 + }
33 +
34 + .toast__close,
35 + .toast__copy {
36 + background-color: transparent;
37 + border: none;
38 + color: #fff;
39 + cursor: pointer;
40 + font-size: 16px;
41 + margin-left: 8px;
42 + }
\ No newline at end of file