Agent Zero AI framework

models, settings, initializer refactor

Rate limiter fix Models initialized JIT Model call wrappers for agent Message compression fix Log progress update Settings frontend numeric fields

frdel committed Dec 15, 2024 at 14:55 UTC 9ef32085651bc02610e1317b29f8d0b4913ae49f
20 files changed +541 -340
.vscode/settings.json
+2
@@ -1,3 +1,5 @@
1 {
2 "python.analysis.typeCheckingMode": "standard",
3 + "windsurfPyright.analysis.diagnosticMode": "workspace",
4 + "windsurfPyright.analysis.typeCheckingMode": "standard",
5 }
\ No newline at end of file
agent.py
+117 -71
@@ -2,8 +2,12 @@ import asyncio
2 from collections import OrderedDict
3 from dataclasses import dataclass, field
4 import time, importlib, inspect, os, json
5 -from typing import Any, Optional, Dict, TypedDict
5 +import token
6 +from typing import Any, Awaitable, Optional, Dict, TypedDict
7 import uuid
8 +import models
9 +
10 +from langchain_core.prompt_values import ChatPromptValue
11 from python.helpers import extract_tools, rate_limiter, files, errors, history, tokens
12 from python.helpers.print_style import PrintStyle
13 from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
@@ -131,19 +135,25 @@ class AgentContext:
135 agent.handle_critical_exception(e)
136
137
138 +@dataclass
139 +class ModelConfig:
140 + provider: models.ModelProvider
141 + name: str
142 + ctx_length: int
143 + limit_requests: int
144 + limit_input: int
145 + limit_output: int
146 + kwargs: dict
147 +
148 +
149 @dataclass
150 class AgentConfig:
136 - chat_model: BaseChatModel | BaseLLM
137 - utility_model: BaseChatModel | BaseLLM
138 - embeddings_model: Embeddings
151 + chat_model: ModelConfig
152 + utility_model: ModelConfig
153 + embeddings_model: ModelConfig
154 prompts_subdir: str = ""
155 memory_subdir: str = ""
156 knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"])
142 - rate_limit_seconds: int = 60
143 - rate_limit_requests: int = 15
144 - rate_limit_input_tokens: int = 0
145 - rate_limit_output_tokens: int = 0
146 - response_timeout_seconds: int = 60
157 code_exec_docker_enabled: bool = False
158 code_exec_docker_name: str = "A0-dev"
159 code_exec_docker_image: str = "frdel/agent-zero-run:development"
@@ -222,13 +232,6 @@ class Agent:
232 self.history = history.History(self)
233 self.last_user_message: history.Message | None = None
234 self.intervention: UserMessage | None = None
225 - self.rate_limiter = rate_limiter.RateLimiter(
226 - self.context.log,
227 - max_calls=self.config.rate_limit_requests,
228 - max_input_tokens=self.config.rate_limit_input_tokens,
229 - max_output_tokens=self.config.rate_limit_output_tokens,
230 - window_seconds=self.config.rate_limit_seconds,
231 - )
235 self.data = {} # free data object all the tools can use
236
237 async def monologue(self):
@@ -245,20 +248,11 @@ class Agent:
248 while True:
249
250 self.context.streaming_agent = self # mark self as current streamer
248 - agent_response = ""
251 self.loop_data.iteration += 1
252
253 try:
254 # prepare LLM chain (model, system, history)
253 - chain, prompt = await self.prepare_chain(
254 - loop_data=self.loop_data
255 - )
256 -
257 - # rate limiter TODO - move to extension, make per-model
258 - formatted_inputs = prompt.format()
259 - self.set_data(self.DATA_NAME_CTX_WINDOW, formatted_inputs)
260 - token_count = tokens.approximate_tokens(formatted_inputs)
261 - self.rate_limiter.limit_call_and_input(token_count)
255 + prompt = await self.prepare_prompt(loop_data=self.loop_data)
256
257 # output that the agent is starting
258 PrintStyle(
@@ -271,27 +265,15 @@ class Agent:
265 type="agent", heading=f"{self.agent_name}: Generating"
266 )
267
274 - async for chunk in chain.astream({}):
275 - # wait for intervention and handle it, if paused
276 - await self.handle_intervention(agent_response)
277 -
278 - if isinstance(chunk, str):
279 - content = chunk
280 - elif hasattr(chunk, "content"):
281 - content = str(chunk.content)
282 - else:
283 - content = str(chunk)
284 -
285 - if content:
286 - # output the agent response stream
287 - printer.stream(content)
288 - # concatenate stream into the response
289 - agent_response += content
290 - self.log_from_stream(agent_response, log)
268 + async def stream_callback(chunk: str, full: str):
269 + # output the agent response stream
270 + if chunk:
271 + printer.stream(chunk)
272 + self.log_from_stream(full, log)
273
292 - self.rate_limiter.set_output_tokens(
293 - int(len(agent_response) / 4)
294 - ) # rough estimation
274 + agent_response = await self.call_chat_model(
275 + prompt, callback=stream_callback
276 + )
277
278 await self.handle_intervention(agent_response)
279
@@ -319,14 +301,14 @@ class Agent:
301 # exceptions inside message loop:
302 except InterventionException as e:
303 pass # intervention message has been handled in handle_intervention(), proceed with conversation loop
322 - except (
323 - RepairableException
324 - ) as e: # Forward repairable errors to the LLM, maybe it can fix them
304 + except RepairableException as e:
305 + # Forward repairable errors to the LLM, maybe it can fix them
306 error_message = errors.format_error(e)
307 await self.hist_add_warning(error_message)
308 PrintStyle(font_color="red", padding=True).print(error_message)
309 self.context.log.log(type="error", content=error_message)
329 - except Exception as e: # Other exception kill the loop
310 + except Exception as e:
311 + # Other exception kill the loop
312 self.handle_critical_exception(e)
313
314 finally:
@@ -345,7 +327,7 @@ class Agent:
327 # call monologue_end extensions
328 await self.call_extensions("monologue_end", loop_data=self.loop_data) # type: ignore
329
348 - async def prepare_chain(self, loop_data: LoopData):
330 + async def prepare_prompt(self, loop_data: LoopData) -> ChatPromptTemplate:
331 # set system prompt and message history
332 loop_data.system = await self.get_system_prompt(self.loop_data)
333 loop_data.history_output = self.history.output()
@@ -374,10 +356,7 @@ class Agent:
356 *history_langchain,
357 ]
358 )
377 -
378 - # return callable chain
379 - chain = prompt | self.config.chat_model
380 - return chain, prompt
359 + return prompt
360
361 def handle_critical_exception(self, exception: Exception):
362 if isinstance(exception, HandledException):
@@ -498,39 +477,106 @@ class Agent:
477 ): # TODO add param for message range, topic, history
478 return self.history.output_text(human_label="user", ai_label="assistant")
479
501 - async def call_utility_llm(
502 - self, system: str, msg: str, callback: Callable[[str], None] | None = None
480 + async def call_utility_model(
481 + self,
482 + system: str,
483 + message: str,
484 + callback: Callable[[str], Awaitable[None]] | None = None,
485 + background: bool = False,
486 ):
487 prompt = ChatPromptTemplate.from_messages(
505 - [SystemMessage(content=system), HumanMessage(content=msg)]
488 + [SystemMessage(content=system), HumanMessage(content=message)]
489 )
490
508 - chain = prompt | self.config.utility_model
491 response = ""
492
511 - formatted_inputs = prompt.format()
512 - token_count = tokens.approximate_tokens(formatted_inputs)
513 - self.rate_limiter.limit_call_and_input(token_count)
493 + # model class
494 + model = models.get_model(
495 + models.ModelType.CHAT,
496 + self.config.utility_model.provider,
497 + self.config.utility_model.name,
498 + **self.config.utility_model.kwargs,
499 + )
500 +
501 + # rate limiter
502 + limiter = await self.rate_limiter(
503 + self.config.utility_model, prompt.format(), background
504 + )
505
515 - async for chunk in chain.astream({}):
506 + async for chunk in (prompt | model).astream({}):
507 await self.handle_intervention() # wait for intervention and handle it, if paused
508
518 - if isinstance(chunk, str):
519 - content = chunk
520 - elif hasattr(chunk, "content"):
521 - content = str(chunk.content)
522 - else:
523 - content = str(chunk)
509 + content = models.parse_chunk(chunk)
510 + limiter.add(output=tokens.approximate_tokens(content))
511 + response += content
512
513 if callback:
526 - callback(content)
514 + await callback(content)
515
516 + return response
517 +
518 + async def call_chat_model(
519 + self,
520 + prompt: ChatPromptTemplate,
521 + callback: Callable[[str, str], Awaitable[None]] | None = None,
522 + ):
523 + response = ""
524 +
525 + # model class
526 + model = models.get_model(
527 + models.ModelType.CHAT,
528 + self.config.chat_model.provider,
529 + self.config.chat_model.name,
530 + **self.config.chat_model.kwargs,
531 + )
532 +
533 + # rate limiter
534 + limiter = await self.rate_limiter(self.config.chat_model, prompt.format())
535 +
536 + async for chunk in (prompt | model).astream({}):
537 + await self.handle_intervention() # wait for intervention and handle it, if paused
538 +
539 + content = models.parse_chunk(chunk)
540 + limiter.add(output=tokens.approximate_tokens(content))
541 response += content
542
530 - self.rate_limiter.set_output_tokens(int(len(response) / 4))
543 + if callback:
544 + await callback(content, response)
545
546 return response
547
548 + async def rate_limiter(
549 + self, model_config: ModelConfig, input: str, background: bool = False
550 + ):
551 + # rate limiter log
552 + wait_log = None
553 +
554 + async def wait_callback(msg: str, key: str, total: int, limit: int):
555 + nonlocal wait_log
556 + if not wait_log:
557 + wait_log = self.context.log.log(
558 + type="util",
559 + update_progress="none",
560 + heading=msg,
561 + model=f"{model_config.provider.value}\\{model_config.name}",
562 + )
563 + wait_log.update(heading=msg, key=key, value=total, limit=limit)
564 + if not background:
565 + self.context.log.set_progress(msg, -1)
566 +
567 + # rate limiter
568 + limiter = models.get_rate_limiter(
569 + model_config.provider,
570 + model_config.name,
571 + model_config.limit_requests,
572 + model_config.limit_input,
573 + model_config.limit_output,
574 + )
575 + limiter.add(input=tokens.approximate_tokens(input))
576 + limiter.add(requests=1)
577 + await limiter.wait(callback=wait_callback)
578 + return limiter
579 +
580 async def handle_intervention(self, progress: str = ""):
581 while self.context.paused:
582 await asyncio.sleep(0.1) # wait if paused
initialize.py
+40 -36
@@ -1,6 +1,6 @@
1 import asyncio
2 import models
3 -from agent import AgentConfig
3 +from agent import AgentConfig, ModelConfig
4 from python.helpers import dotenv, files, rfc_exchange, runtime, settings, docker, log
5
6
@@ -8,36 +8,45 @@ def initialize():
8
9 current_settings = settings.get_settings()
10
11 - # main chat model used by agents (smarter, more accurate)
12 - # chat_llm = models.get_openai_chat(model_name="gpt-4o-mini", temperature=0)
13 - # chat_llm = models.get_ollama_chat(model_name="llama3.2:3b-instruct-fp16", temperature=0)
14 - # chat_llm = models.get_lmstudio_chat(model_name="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF", temperature=0)
15 - # chat_llm = models.get_openrouter_chat(model_name="openai/o1-mini-2024-09-12")
16 - # chat_llm = models.get_azure_openai_chat(deployment_name="gpt-4o-mini", temperature=0)
17 - # chat_llm = models.get_anthropic_chat(model_name="claude-3-5-sonnet-20240620", temperature=0)
18 - # chat_llm = models.get_google_chat(model_name="gemini-1.5-flash", temperature=0)
19 - # chat_llm = models.get_mistral_chat(model_name="mistral-small-latest", temperature=0)
20 - # chat_llm = models.get_groq_chat(model_name="llama-3.2-90b-text-preview", temperature=0)
21 - # chat_llm = models.get_sambanova_chat(model_name="Meta-Llama-3.1-70B-Instruct-8k", temperature=0)
22 - chat_llm = settings.get_chat_model(
23 - current_settings
24 - ) # chat model from user settings
25 -
26 - # utility model used for helper functions (cheaper, faster)
27 - # utility_llm = chat_llm
28 - utility_llm = settings.get_utility_model(
29 - current_settings
30 - ) # utility model from user settings
31 -
32 - # embedding model used for memory
33 - # embedding_llm = models.get_openai_embedding(model_name="text-embedding-3-small")
34 - # embedding_llm = models.get_ollama_embedding(model_name="nomic-embed-text")
35 - # embedding_llm = models.get_huggingface_embedding(model_name="sentence-transformers/all-MiniLM-L6-v2")
36 - # embedding_llm = models.get_lmstudio_embedding(model_name="nomic-ai/nomic-embed-text-v1.5-GGUF")
37 - embedding_llm = settings.get_embedding_model(
38 - current_settings
39 - ) # embedding model from user settings
11 + # chat model from user settings
12 + chat_llm = ModelConfig(
13 + provider=models.ModelProvider[current_settings["chat_model_provider"]],
14 + name=current_settings["chat_model_name"],
15 + ctx_length=current_settings["chat_model_ctx_length"],
16 + limit_requests=current_settings["chat_model_rl_requests"],
17 + limit_input=current_settings["chat_model_rl_input"],
18 + limit_output=current_settings["chat_model_rl_output"],
19 + kwargs={
20 + "temperature": current_settings["chat_model_temperature"],
21 + **current_settings["chat_model_kwargs"],
22 + },
23 + )
24
25 + # utility model from user settings
26 + utility_llm = ModelConfig(
27 + provider=models.ModelProvider[current_settings["util_model_provider"]],
28 + name=current_settings["util_model_name"],
29 + ctx_length=current_settings["util_model_ctx_length"],
30 + limit_requests=current_settings["util_model_rl_requests"],
31 + limit_input=current_settings["util_model_rl_input"],
32 + limit_output=current_settings["util_model_rl_output"],
33 + kwargs={
34 + "temperature": current_settings["util_model_temperature"],
35 + **current_settings["util_model_kwargs"],
36 + },
37 + )
38 + # embedding model from user settings
39 + embedding_llm = ModelConfig(
40 + provider=models.ModelProvider[current_settings["embed_model_provider"]],
41 + name=current_settings["embed_model_name"],
42 + ctx_length=0,
43 + limit_requests=current_settings["embed_model_rl_requests"],
44 + limit_input=0,
45 + limit_output=0,
46 + kwargs={
47 + **current_settings["embed_model_kwargs"],
48 + },
49 + )
50 # agent configuration
51 config = AgentConfig(
52 chat_model=chat_llm,
@@ -46,12 +55,7 @@ def initialize():
55 prompts_subdir=current_settings["agent_prompts_subdir"],
56 memory_subdir=current_settings["agent_memory_subdir"],
57 knowledge_subdirs=["default", current_settings["agent_knowledge_subdir"]],
49 - # rate_limit_seconds = 60,
50 - rate_limit_requests=30,
51 - # rate_limit_input_tokens = 0,
52 - # rate_limit_output_tokens = 0,
53 - # response_timeout_seconds = 60,
54 - code_exec_docker_enabled = False,
58 + code_exec_docker_enabled=False,
59 # code_exec_docker_name = "A0-dev",
60 # code_exec_docker_image = "frdel/agent-zero-run:development",
61 # code_exec_docker_ports = { "22/tcp": 55022, "80/tcp": 55080 }
models.py
+36 -2
@@ -1,5 +1,6 @@
1 from enum import Enum
2 import os
3 +from typing import Any
4 from langchain_openai import (
5 ChatOpenAI,
6 OpenAI,
@@ -28,6 +29,7 @@ from langchain_mistralai import ChatMistralAI
29 from pydantic.v1.types import SecretStr
30 from python.helpers import dotenv, runtime
31 from python.helpers.dotenv import load_dotenv
32 +from python.helpers.rate_limiter import RateLimiter
33
34 # environment variables
35 load_dotenv()
@@ -56,6 +58,9 @@ class ModelProvider(Enum):
58 OTHER = "Other"
59
60
61 +rate_limiters: dict[str, RateLimiter] = {}
62 +
63 +
64 # Utility function to get API keys from environment variables
65 def get_api_key(service):
66 return (
@@ -71,11 +76,36 @@ def get_model(type: ModelType, provider: ModelProvider, name: str, **kwargs):
76 return model
77
78
79 +def get_rate_limiter(
80 + provider: ModelProvider, name: str, requests: int, input: int, output: int
81 +) -> RateLimiter:
82 + # get or create
83 + key = f"{provider.name}\\{name}"
84 + rate_limiters[key] = limiter = rate_limiters.get(key, RateLimiter(seconds=60))
85 + # always update
86 + limiter.limits["requests"] = requests
87 + limiter.limits["input"] = input
88 + limiter.limits["output"] = output
89 + return limiter
90 +
91 +
92 +def parse_chunk(chunk: Any):
93 + if isinstance(chunk, str):
94 + content = chunk
95 + elif hasattr(chunk, "content"):
96 + content = str(chunk.content)
97 + else:
98 + content = str(chunk)
99 + return content
100
101
102 # Ollama models
103 def get_ollama_base_url():
78 - return dotenv.get_dotenv_value("OLLAMA_BASE_URL") or f"http://{runtime.get_local_url()}:11434"
104 + return (
105 + dotenv.get_dotenv_value("OLLAMA_BASE_URL")
106 + or f"http://{runtime.get_local_url()}:11434"
107 + )
108 +
109
110 def get_ollama_chat(
111 model_name: str,
@@ -138,7 +168,11 @@ def get_huggingface_embedding(model_name: str, **kwargs):
168
169 # LM Studio and other OpenAI compatible interfaces
170 def get_lmstudio_base_url():
141 - return dotenv.get_dotenv_value("LM_STUDIO_BASE_URL") or f"http://{runtime.get_local_url()}:1234/v1"
171 + return (
172 + dotenv.get_dotenv_value("LM_STUDIO_BASE_URL")
173 + or f"http://{runtime.get_local_url()}:1234/v1"
174 + )
175 +
176
177 def get_lmstudio_chat(
178 model_name: str,
python/extensions/message_loop_prompts/_50_recall_memories.py
+3 -3
@@ -53,13 +53,13 @@ class RecallMemories(Extension):
53 )
54
55 # log query streamed by LLM
56 - def log_callback(content):
56 + async def log_callback(content):
57 log_item.stream(query=content)
58
59 # call util llm to summarize conversation
60 - query = await self.agent.call_utility_llm(
60 + query = await self.agent.call_utility_model(
61 system=system,
62 - msg=loop_data.user_message.output_text() if loop_data.user_message else "",
62 + message=loop_data.user_message.output_text() if loop_data.user_message else "",
63 callback=log_callback,
64 )
65
python/extensions/message_loop_prompts/_51_recall_solutions.py
+3 -3
@@ -53,12 +53,12 @@ class RecallSolutions(Extension):
53 )
54
55 # log query streamed by LLM
56 - def log_callback(content):
56 + async def log_callback(content):
57 log_item.stream(query=content)
58
59 # call util llm to summarize conversation
60 - query = await self.agent.call_utility_llm(
61 - system=system, msg=loop_data.user_message.output_text() if loop_data.user_message else "", callback=log_callback
60 + query = await self.agent.call_utility_model(
61 + system=system, message=loop_data.user_message.output_text() if loop_data.user_message else "", callback=log_callback
62 )
63
64 # get solutions database
python/extensions/message_loop_prompts/_91_recall_wait.py
+2 -2
@@ -9,11 +9,11 @@ class RecallWait(Extension):
9
10 task = self.agent.get_data(DATA_NAME_TASK_MEMORIES)
11 if task and not task.done():
12 - self.agent.context.log.set_progress("Recalling memories...")
12 + # self.agent.context.log.set_progress("Recalling memories...")
13 await task
14
15 task = self.agent.get_data(DATA_NAME_TASK_SOLUTIONS)
16 if task and not task.done():
17 - self.agent.context.log.set_progress("Recalling solutions...")
17 + # self.agent.context.log.set_progress("Recalling solutions...")
18 await task
19
python/extensions/monologue_end/_50_memorize_fragments.py
+5 -4
@@ -35,14 +35,15 @@ class MemorizeMemories(Extension):
35 msgs_text = self.agent.concat_messages(self.agent.history)
36
37 # log query streamed by LLM
38 - def log_callback(content):
38 + async def log_callback(content):
39 log_item.stream(content=content)
40
41 # call util llm to find info in history
42 - memories_json = await self.agent.call_utility_llm(
42 + memories_json = await self.agent.call_utility_model(
43 system=system,
44 - msg=msgs_text,
44 + message=msgs_text,
45 callback=log_callback,
46 + background=True,
47 )
48
49 memories = DirtyJson.parse_string(memories_json)
@@ -76,7 +77,7 @@ class MemorizeMemories(Extension):
77 log_item.update(replaced=rem_txt)
78
79 # insert new solution
79 - db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
80 + await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
81
82 log_item.update(
83 result=f"{len(memories)} entries memorized.",
python/extensions/monologue_end/_51_memorize_solutions.py
+5 -4
@@ -33,14 +33,15 @@ class MemorizeSolutions(Extension):
33 msgs_text = self.agent.concat_messages(self.agent.history)
34
35 # log query streamed by LLM
36 - def log_callback(content):
36 + async def log_callback(content):
37 log_item.stream(content=content)
38
39 # call util llm to find solutions in history
40 - solutions_json = await self.agent.call_utility_llm(
40 + solutions_json = await self.agent.call_utility_model(
41 system=system,
42 - msg=msgs_text,
42 + message=msgs_text,
43 callback=log_callback,
44 + background=True,
45 )
46
47 solutions = DirtyJson.parse_string(solutions_json)
@@ -75,7 +76,7 @@ class MemorizeSolutions(Extension):
76 log_item.update(replaced=rem_txt)
77
78 # insert new solution
78 - db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
79 + await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
80
81 solutions_txt = solutions_txt.strip()
82 log_item.update(solutions=solutions_txt)
python/helpers/history.py
+34 -40
@@ -130,12 +130,12 @@ class Topic(Record):
130 * LARGE_MESSAGE_TO_TOPIC_RATIO
131 )
132 large_msgs = []
133 - for m in self.messages:
133 + for m in (m for m in self.messages if not m.summary):
134 out = m.output()
135 text = output_text(out)
136 tok = tokens.approximate_tokens(text)
137 leng = len(text)
138 - if leng > msg_max_size:
138 + if tok > msg_max_size:
139 large_msgs.append((m, tok, leng, out))
140 large_msgs.sort(key=lambda x: x[1], reverse=True)
141 for msg, tok, leng, out in large_msgs:
@@ -173,12 +173,11 @@ class Topic(Record):
173
174 async def summarize_messages(self, messages: list[Message]):
175 msg_txt = [m.output_text() for m in messages]
176 - summary = await call_llm.call_llm(
176 + summary = await self.history.agent.call_utility_model(
177 system=self.history.agent.read_prompt("fw.topic_summary.sys.md"),
178 message=self.history.agent.read_prompt(
179 "fw.topic_summary.msg.md", content=msg_txt
180 ),
181 - model=settings.get_utility_model(),
181 )
182 return summary
183
@@ -218,12 +217,11 @@ class Bulk(Record):
217 return False
218
219 async def summarize(self):
221 - self.summary = await call_llm.call_llm(
220 + self.summary = await self.history.agent.call_utility_model(
221 system=self.history.agent.read_prompt("fw.topic_summary.sys.md"),
222 message=self.history.agent.read_prompt(
223 "fw.topic_summary.msg.md", content=self.output_text()
224 ),
226 - model=settings.get_utility_model(),
225 )
226 return self.summary
227
@@ -309,42 +307,38 @@ class History(Record):
307 return json.dumps(data)
308
309 async def compress(self):
312 - curr, hist, bulk = (
313 - self.get_current_topic_tokens(),
314 - self.get_topics_tokens(),
315 - self.get_bulks_tokens(),
316 - )
317 - total = get_ctx_size_for_history()
310 compressed = False
319 -
320 - # calculate ratios of individual parts
321 - ratios = [
322 - (curr, CURRENT_TOPIC_RATIO, "current_topic"),
323 - (hist, HISTORY_TOPIC_RATIO, "history_topic"),
324 - (bulk, HISTORY_BULK_RATIO, "history_bulk"),
325 - ]
326 - # start from the most oversized part and compress it
327 - ratios = sorted(ratios, key=lambda x: (x[0] / total) / x[1], reverse=True)
328 - for ratio in ratios:
329 - if ratio[0] > ratio[1] * total:
330 - over_part = ratio[2]
331 - if over_part == "current_topic":
332 - compressed = await self.current.compress()
333 - elif over_part == "history_topic":
334 - compressed = await self.compress_topics()
335 - else:
336 - compressed = await self.compress_bulks()
337 - # if part was compressed, stop the loop and try the whole function again, maybe no more compression is necessary
338 - if compressed:
339 - break
311 + while True:
312 + curr, hist, bulk = (
313 + self.get_current_topic_tokens(),
314 + self.get_topics_tokens(),
315 + self.get_bulks_tokens(),
316 + )
317 + total = get_ctx_size_for_history()
318 + ratios = [
319 + (curr, CURRENT_TOPIC_RATIO, "current_topic"),
320 + (hist, HISTORY_TOPIC_RATIO, "history_topic"),
321 + (bulk, HISTORY_BULK_RATIO, "history_bulk"),
322 + ]
323 + ratios = sorted(ratios, key=lambda x: (x[0] / total) / x[1], reverse=True)
324 + compressed_part = False
325 + for ratio in ratios:
326 + if ratio[0] > ratio[1] * total:
327 + over_part = ratio[2]
328 + if over_part == "current_topic":
329 + compressed_part = await self.current.compress()
330 + elif over_part == "history_topic":
331 + compressed_part = await self.compress_topics()
332 + else:
333 + compressed_part = await self.compress_bulks()
334 + if compressed_part:
335 + break
336 +
337 + if compressed_part:
338 + compressed = True
339 + continue
340 else:
341 - break
342 -
343 - # try the whole function again to see if there is still a need for compression
344 - if compressed:
345 - await self.compress()
346 -
347 - return compressed
341 + return compressed
342
343 async def compress_topics(self) -> bool:
344 # summarize topics one by one
python/helpers/log.py
+34 -9
@@ -19,6 +19,8 @@ Type = Literal[
19 "warning",
20 ]
21
22 +ProgressUpdate = Literal["persistent", "temporary", "none"]
23 +
24
25 @dataclass
26 class LogItem:
@@ -28,6 +30,7 @@ class LogItem:
30 heading: str
31 content: str
32 temp: bool
33 + update_progress: Optional[ProgressUpdate] = "persistent"
34 kvps: Optional[OrderedDict] = None # Use OrderedDict for kvps
35 id: Optional[str] = None # Add id field
36 guid: str = ""
@@ -42,20 +45,27 @@ class LogItem:
45 content: str | None = None,
46 kvps: dict | None = None,
47 temp: bool | None = None,
48 + update_progress: ProgressUpdate | None = None,
49 **kwargs,
50 ):
51 if self.guid == self.log.guid:
48 - self.log.update_item(
52 + self.log._update_item(
53 self.no,
54 type=type,
55 heading=heading,
56 content=content,
57 kvps=kvps,
58 temp=temp,
59 + update_progress=update_progress,
60 **kwargs,
61 )
62
58 - def stream(self, heading: str | None = None, content: str | None = None, **kwargs):
63 + def stream(
64 + self,
65 + heading: str | None = None,
66 + content: str | None = None,
67 + **kwargs,
68 + ):
69 if heading is not None:
70 self.update(heading=self.heading + heading)
71 if content is not None:
@@ -92,7 +102,9 @@ class Log:
102 content: str | None = None,
103 kvps: dict | None = None,
104 temp: bool | None = None,
105 + update_progress: ProgressUpdate | None = None,
106 id: Optional[str] = None, # Add id parameter
107 + **kwargs,
108 ) -> LogItem:
109 # Use OrderedDict if kvps is provided
110 if kvps is not None:
@@ -103,17 +115,19 @@ class Log:
115 type=type,
116 heading=heading or "",
117 content=content or "",
106 - kvps=kvps,
107 - temp=temp or False,
118 + kvps=OrderedDict({**(kvps or {}), **(kwargs or {})}),
119 + update_progress=(
120 + update_progress if update_progress is not None else "persistent"
121 + ),
122 + temp=temp if temp is not None else False,
123 id=id, # Pass id to LogItem
124 )
125 self.logs.append(item)
126 self.updates += [item.no]
112 - if heading and item.no >= self.progress_no:
113 - self.set_progress(heading, item.no)
127 + self._update_progress_from_item(item)
128 return item
129
116 - def update_item(
130 + def _update_item(
131 self,
132 no: int,
133 type: str | None = None,
@@ -121,15 +135,16 @@ class Log:
135 content: str | None = None,
136 kvps: dict | None = None,
137 temp: bool | None = None,
138 + update_progress: ProgressUpdate | None = None,
139 **kwargs,
140 ):
141 item = self.logs[no]
142 if type is not None:
143 item.type = type
144 + if update_progress is not None:
145 + item.update_progress = update_progress
146 if heading is not None:
147 item.heading = heading
131 - if no >= self.progress_no:
132 - self.set_progress(heading, no)
148 if content is not None:
149 item.content = content
150 if kvps is not None:
@@ -145,6 +160,7 @@ class Log:
160 item.kvps[k] = v
161
162 self.updates += [item.no]
163 + self._update_progress_from_item(item)
164
165 def set_progress(self, progress: str, no: int = 0, active: bool = True):
166 self.progress = progress
@@ -176,3 +192,12 @@ class Log:
192 self.updates = []
193 self.logs = []
194 self.set_initial_progress()
195 +
196 + def _update_progress_from_item(self, item: LogItem):
197 + if item.heading and item.update_progress != "none":
198 + if item.no >= self.progress_no:
199 + self.set_progress(
200 + item.heading,
201 + (item.no if item.update_progress == "persistent" else -1),
202 + )
203 +
python/helpers/memory.py
+33 -21
@@ -10,6 +10,8 @@ from langchain_community.docstore.in_memory import InMemoryDocstore
10 from langchain_community.vectorstores.utils import (
11 DistanceStrategy,
12 )
13 +from langchain_core.embeddings import Embeddings
14 +
15 import os, json
16
17 import numpy as np
@@ -22,6 +24,7 @@ from python.helpers import knowledge_import
24 from python.helpers.log import Log, LogItem
25 from enum import Enum
26 from agent import Agent
27 +import models
28
29
30 class MyFaiss(FAISS):
@@ -54,7 +57,12 @@ class Memory:
57 )
58 db = Memory.initialize(
59 log_item,
57 - agent.config.embeddings_model,
60 + models.get_model(
61 + models.ModelType.EMBEDDING,
62 + agent.config.embeddings_model.provider,
63 + agent.config.embeddings_model.name,
64 + **agent.config.embeddings_model.kwargs,
65 + ),
66 memory_subdir,
67 False,
68 )
@@ -82,7 +90,7 @@ class Memory:
90 @staticmethod
91 def initialize(
92 log_item: LogItem | None,
85 - embeddings_model,
93 + embeddings_model: Embeddings,
94 memory_subdir: str,
95 in_memory=False,
96 ) -> MyFaiss:
@@ -187,7 +195,7 @@ class Memory:
195 index[file]["ids"]
196 ) # remove original version
197 if index[file]["state"] == "changed":
190 - index[file]["ids"] = self.insert_documents(
198 + index[file]["ids"] = await self.insert_documents(
199 index[file]["documents"]
200 ) # insert new version
201
@@ -234,6 +242,11 @@ class Memory:
242 self, query: str, limit: int, threshold: float, filter: str = ""
243 ):
244 comparator = Memory._get_comparator(filter) if filter else None
245 +
246 + #rate limiter
247 + await self.agent.rate_limiter(
248 + model_config=self.agent.config.embeddings_model, input=query)
249 +
250 return await self.db.asearch(
251 query,
252 search_type="similarity_score_threshold",
@@ -287,30 +300,28 @@ class Memory:
300 self._save_db() # persist
301 return rem_docs
302
290 - def insert_text(self, text, metadata: dict = {}):
291 - id = str(uuid.uuid4())
292 - if not metadata.get("area", ""):
293 - metadata["area"] = Memory.Area.MAIN.value
303 + async def insert_text(self, text, metadata: dict = {}):
304 + doc = Document(text, metadata=metadata)
305 + ids = await self.insert_documents([doc])
306 + return ids[0]
307
295 - self.db.add_documents(
296 - documents=[
297 - Document(
298 - text,
299 - metadata={"id": id, "timestamp": self.get_timestamp(), **metadata},
300 - )
301 - ],
302 - ids=[id],
303 - )
304 - self._save_db() # persist
305 - return id
306 -
307 - def insert_documents(self, docs: list[Document]):
308 + async def insert_documents(self, docs: list[Document]):
309 ids = [str(uuid.uuid4()) for _ in range(len(docs))]
310 timestamp = self.get_timestamp()
311 +
312 +
313 if ids:
314 for doc, id in zip(docs, ids):
315 doc.metadata["id"] = id # add ids to documents metadata
316 doc.metadata["timestamp"] = timestamp # add timestamp
317 + if not doc.metadata.get("area", ""):
318 + doc.metadata["area"] = Memory.Area.MAIN.value
319 +
320 + #rate limiter
321 + docs_txt = "".join(self.format_docs_plain(docs))
322 + await self.agent.rate_limiter(
323 + model_config=self.agent.config.embeddings_model, input=docs_txt)
324 +
325 self.db.add_documents(documents=docs, ids=ids)
326 self._save_db() # persist
327 return ids
@@ -365,8 +376,9 @@ class Memory:
376 def get_memory_subdir_abs(agent: Agent) -> str:
377 return files.get_abs_path("memory", agent.config.memory_subdir or "default")
378
379 +
380 def get_custom_knowledge_subdir_abs(agent: Agent) -> str:
381 for dir in agent.config.knowledge_subdirs:
370 - if dir != "default":
382 + if dir != "default":
383 return files.get_abs_path("knowledge", dir)
384 raise Exception("No custom knowledge subdir set")
python/helpers/persist_chat.py
+1 -1
@@ -174,7 +174,7 @@ def _deserialize_log(data: dict[str, Any]) -> "Log":
174 log.logs.append(
175 LogItem(
176 log=log, # restore the log reference
177 - no=item_data["no"],
177 + no=i, #item_data["no"],
178 type=item_data["type"],
179 heading=item_data.get("heading", ""),
180 content=item_data.get("content", ""),
python/helpers/rate_limiter.py
+46 -58
@@ -1,68 +1,56 @@
1 +import asyncio
2 import time
2 -from collections import deque
3 -from dataclasses import dataclass
4 -from typing import List, Tuple
5 -from .print_style import PrintStyle
6 -from .log import Log
3 +from typing import Callable, Awaitable
4
8 -@dataclass
9 -class CallRecord:
10 - timestamp: float
11 - input_tokens: int
12 - output_tokens: int = 0 # Default to 0, will be set separately
5
6 class RateLimiter:
15 - def __init__(self, logger: Log, max_calls: int, max_input_tokens: int, max_output_tokens: int, window_seconds: int = 60):
16 - self.logger = logger
17 - self.max_calls = max_calls
18 - self.max_input_tokens = max_input_tokens
19 - self.max_output_tokens = max_output_tokens
20 - self.window_seconds = window_seconds
21 - self.call_records: deque = deque()
7 + def __init__(self, seconds: int = 60, **limits: int):
8 + self.timeframe = seconds
9 + self.limits = limits or {}
10 + self.values = {key: [] for key in limits.keys()}
11 + self._lock = asyncio.Lock()
12
23 - def _clean_old_records(self, current_time: float):
24 - while self.call_records and current_time - self.call_records[0].timestamp > self.window_seconds:
25 - self.call_records.popleft()
13 + def add(self, **kwargs: int):
14 + now = time.time()
15 + for key, value in kwargs.items():
16 + if not key in self.values:
17 + self.values[key] = []
18 + self.values[key].append((now, value))
19
27 - def _get_counts(self) -> Tuple[int, int, int]:
28 - calls = len(self.call_records)
29 - input_tokens = sum(record.input_tokens for record in self.call_records)
30 - output_tokens = sum(record.output_tokens for record in self.call_records)
31 - return calls, input_tokens, output_tokens
20 + async def cleanup(self):
21 + async with self._lock:
22 + now = time.time()
23 + cutoff = now - self.timeframe
24 + for key in self.values:
25 + self.values[key] = [(t, v) for t, v in self.values[key] if t > cutoff]
26
33 - def _wait_if_needed(self, current_time: float, new_input_tokens: int):
27 + async def get_total(self, key: str) -> int:
28 + async with self._lock:
29 + if not key in self.values:
30 + return 0
31 + return sum(value for _, value in self.values[key])
32 +
33 + async def wait(
34 + self,
35 + callback: Callable[[str, str, int, int], Awaitable[None]] | None = None,
36 + ):
37 while True:
35 - self._clean_old_records(current_time)
36 - calls, input_tokens, output_tokens = self._get_counts()
37 -
38 - wait_reasons = []
39 - if self.max_calls > 0 and calls >= self.max_calls:
40 - wait_reasons.append("max calls")
41 - if self.max_input_tokens > 0 and input_tokens + new_input_tokens > self.max_input_tokens:
42 - wait_reasons.append("max input tokens")
43 - if self.max_output_tokens > 0 and output_tokens >= self.max_output_tokens:
44 - wait_reasons.append("max output tokens")
45 -
46 - if not wait_reasons:
47 - break
48 -
49 - oldest_record = self.call_records[0]
50 - wait_time = oldest_record.timestamp + self.window_seconds - current_time
51 - if wait_time > 0:
52 - PrintStyle(font_color="yellow", padding=True).print(f"Rate limit exceeded. Waiting for {wait_time:.2f} seconds due to: {', '.join(wait_reasons)}")
53 - self.logger.log("rate_limit","Rate limit exceeded",f"Rate limit exceeded. Waiting for {wait_time:.2f} seconds due to: {', '.join(wait_reasons)}")
54 - # TODO rate limit log type
55 - time.sleep(wait_time)
56 - current_time = time.time()
38 + await self.cleanup()
39 + should_wait = False
40 +
41 + for key, limit in self.limits.items():
42 + if limit <= 0: # Skip if no limit set
43 + continue
44
58 - def limit_call_and_input(self, input_token_count: int) -> CallRecord:
59 - current_time = time.time()
60 - self._wait_if_needed(current_time, input_token_count)
61 - new_record = CallRecord(current_time, input_token_count)
62 - self.call_records.append(new_record)
63 - return new_record
45 + total = await self.get_total(key)
46 + if total > limit:
47 + if callback:
48 + msg = f"Rate limit exceeded for {key} ({total}/{limit}), waiting..."
49 + await callback(msg, key, total, limit)
50 + should_wait = True
51 + break
52 +
53 + if not should_wait:
54 + break
55
65 - def set_output_tokens(self, output_token_count: int):
66 - if self.call_records:
67 - self.call_records[-1].output_tokens += output_token_count
68 - return self
56 + await asyncio.sleep(1)
python/helpers/settings.py
+165 -80
@@ -8,10 +8,6 @@ from typing import Any, Literal, TypedDict
8 import models
9 from python.helpers import runtime, whisper, defer
10 from . import files, dotenv
11 -from models import get_model, ModelProvider, ModelType
12 -from langchain_core.language_models.chat_models import BaseChatModel
13 -from langchain_core.embeddings import Embeddings
14 -
11
12 class Settings(TypedDict):
13 chat_model_provider: str
@@ -20,15 +16,26 @@ class Settings(TypedDict):
16 chat_model_kwargs: dict[str, str]
17 chat_model_ctx_length: int
18 chat_model_ctx_history: float
19 + chat_model_rl_requests: int
20 + chat_model_rl_input: int
21 + chat_model_rl_output: int
22
23 util_model_provider: str
24 util_model_name: str
25 util_model_temperature: float
26 util_model_kwargs: dict[str, str]
27 + util_model_ctx_length: int
28 + util_model_ctx_input: float
29 + util_model_rl_requests: int
30 + util_model_rl_input: int
31 + util_model_rl_output: int
32
33 +
34 embed_model_provider: str
35 embed_model_name: str
36 embed_model_kwargs: dict[str, str]
37 + embed_model_rl_requests: int
38 + embed_model_rl_input: int
39
40 agent_prompts_subdir: str
41 agent_memory_subdir: str
@@ -66,7 +73,7 @@ class SettingsField(TypedDict, total=False):
73 id: str
74 title: str
75 description: str
69 - type: Literal["input", "select", "range", "textarea", "password"]
76 + type: Literal["text", "number", "select", "range", "textarea", "password"]
77 value: Any
78 min: float
79 max: float
@@ -91,6 +98,8 @@ _settings: Settings | None = None
98
99
100 def convert_out(settings: Settings) -> SettingsOutput:
101 + from models import ModelProvider
102 +
103
104 # main model section
105 chat_model_fields: list[SettingsField] = []
@@ -109,7 +118,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
118 "id": "chat_model_name",
119 "title": "Chat model name",
120 "description": "Exact name of model from selected provider",
112 - "type": "input",
121 + "type": "text",
122 "value": settings["chat_model_name"],
123 }
124 )
@@ -132,7 +141,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
141 "id": "chat_model_ctx_length",
142 "title": "Chat model context length",
143 "description": "Maximum number of tokens in the context window for LLM. System prompt, chat history, RAG and response all count towards this limit.",
135 - "type": "input",
144 + "type": "number",
145 "value": settings["chat_model_ctx_length"],
146 }
147 )
@@ -150,6 +159,36 @@ def convert_out(settings: Settings) -> SettingsOutput:
159 }
160 )
161
162 + chat_model_fields.append(
163 + {
164 + "id": "chat_model_rl_requests",
165 + "title": "Requests per minute limit",
166 + "description": "Limits the number of requests per minute to the chat model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
167 + "type": "number",
168 + "value": settings["chat_model_rl_requests"],
169 + }
170 + )
171 +
172 + chat_model_fields.append(
173 + {
174 + "id": "chat_model_rl_input",
175 + "title": "Input tokens per minute limit",
176 + "description": "Limits the number of input tokens per minute to the chat model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
177 + "type": "number",
178 + "value": settings["chat_model_rl_input"],
179 + }
180 + )
181 +
182 + chat_model_fields.append(
183 + {
184 + "id": "chat_model_rl_output",
185 + "title": "Output tokens per minute limit",
186 + "description": "Limits the number of output tokens per minute to the chat model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
187 + "type": "number",
188 + "value": settings["chat_model_rl_output"],
189 + }
190 + )
191 +
192 chat_model_fields.append(
193 {
194 "id": "chat_model_kwargs",
@@ -183,7 +222,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
222 "id": "util_model_name",
223 "title": "Utility model name",
224 "description": "Exact name of model from selected provider",
186 - "type": "input",
225 + "type": "text",
226 "value": settings["util_model_name"],
227 }
228 )
@@ -200,6 +239,58 @@ def convert_out(settings: Settings) -> SettingsOutput:
239 "value": settings["util_model_temperature"],
240 }
241 )
242 +
243 + # util_model_fields.append(
244 + # {
245 + # "id": "util_model_ctx_length",
246 + # "title": "Utility model context length",
247 + # "description": "Maximum number of tokens in the context window for LLM. System prompt, message and response all count towards this limit.",
248 + # "type": "number",
249 + # "value": settings["util_model_ctx_length"],
250 + # }
251 + # )
252 + # util_model_fields.append(
253 + # {
254 + # "id": "util_model_ctx_input",
255 + # "title": "Context window space for input tokens",
256 + # "description": "Portion of context window dedicated to input tokens. The remaining space can be filled with response.",
257 + # "type": "range",
258 + # "min": 0.01,
259 + # "max": 1,
260 + # "step": 0.01,
261 + # "value": settings["util_model_ctx_input"],
262 + # }
263 + # )
264 +
265 + util_model_fields.append(
266 + {
267 + "id": "util_model_rl_requests",
268 + "title": "Requests per minute limit",
269 + "description": "Limits the number of requests per minute to the utility model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
270 + "type": "number",
271 + "value": settings["util_model_rl_requests"],
272 + }
273 + )
274 +
275 + util_model_fields.append(
276 + {
277 + "id": "util_model_rl_input",
278 + "title": "Input tokens per minute limit",
279 + "description": "Limits the number of input tokens per minute to the utility model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
280 + "type": "number",
281 + "value": settings["util_model_rl_input"],
282 + }
283 + )
284 +
285 + util_model_fields.append(
286 + {
287 + "id": "util_model_rl_output",
288 + "title": "Output tokens per minute limit",
289 + "description": "Limits the number of output tokens per minute to the utility model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
290 + "type": "number",
291 + "value": settings["util_model_rl_output"],
292 + }
293 + )
294
295 util_model_fields.append(
296 {
@@ -234,46 +325,28 @@ def convert_out(settings: Settings) -> SettingsOutput:
325 "id": "embed_model_name",
326 "title": "Embedding model name",
327 "description": "Exact name of model from selected provider",
237 - "type": "input",
328 + "type": "text",
329 "value": settings["embed_model_name"],
330 }
331 )
241 -
332 +
333 embed_model_fields.append(
334 {
244 - "id": "embed_model_kwargs",
245 - "title": "Embedding model additional parameters",
246 - "description": "Any other parameters supported by the model. Format is KEY=VALUE on individual lines, just like .env file.",
247 - "type": "textarea",
248 - "value": _dict_to_env(settings["embed_model_kwargs"]),
335 + "id": "embed_model_rl_requests",
336 + "title": "Requests per minute limit",
337 + "description": "Limits the number of requests per minute to the embedding model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
338 + "type": "number",
339 + "value": settings["embed_model_rl_requests"],
340 }
341 )
342
252 - embed_model_section: SettingsSection = {
253 - "title": "Embedding Model",
254 - "description": "Settings for the embedding model used by Agent Zero.",
255 - "fields": embed_model_fields,
256 - }
257 -
258 - # embedding model section
259 - embed_model_fields: list[SettingsField] = []
260 - embed_model_fields.append(
261 - {
262 - "id": "embed_model_provider",
263 - "title": "Embedding model provider",
264 - "description": "Select provider for embedding model used by the framework",
265 - "type": "select",
266 - "value": settings["embed_model_provider"],
267 - "options": [{"value": p.name, "label": p.value} for p in ModelProvider],
268 - }
269 - )
343 embed_model_fields.append(
344 {
272 - "id": "embed_model_name",
273 - "title": "Embedding model name",
274 - "description": "Exact name of model from selected provider",
275 - "type": "input",
276 - "value": settings["embed_model_name"],
345 + "id": "embed_model_rl_input",
346 + "title": "Input tokens per minute limit",
347 + "description": "Limits the number of input tokens per minute to the embedding model. Waits if the limit is exceeded. Set to 0 to disable rate limiting.",
348 + "type": "number",
349 + "value": settings["embed_model_rl_input"],
350 }
351 )
352
@@ -301,7 +374,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
374 "id": "auth_login",
375 "title": "UI Login",
376 "description": "Set user name for web UI",
304 - "type": "input",
377 + "type": "text",
378 "value": dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or "",
379 }
380 )
@@ -423,7 +496,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
496 # "id": "rfc_auto_docker",
497 # "title": "RFC Auto Docker Management",
498 # "description": "Automatically create dockerized instance of A0 for RFCs using this instance's code base and, settings and .env.",
426 - # "type": "input",
499 + # "type": "text",
500 # "value": settings["rfc_auto_docker"],
501 # }
502 # )
@@ -433,7 +506,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
506 "id": "rfc_url",
507 "title": "RFC Destination URL",
508 "description": "URL of dockerized A0 instance for remote function calls. Do not specify port here.",
436 - "type": "input",
509 + "type": "text",
510 "value": settings["rfc_url"],
511 }
512 )
@@ -458,7 +531,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
531 "id": "rfc_port_http",
532 "title": "RFC HTTP port",
533 "description": "HTTP port for dockerized instance of A0.",
461 - "type": "input",
534 + "type": "text",
535 "value": settings["rfc_port_http"],
536 }
537 )
@@ -468,7 +541,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
541 "id": "rfc_port_ssh",
542 "title": "RFC SSH port",
543 "description": "SSH port for dockerized instance of A0.",
471 - "type": "input",
544 + "type": "text",
545 "value": settings["rfc_port_ssh"],
546 }
547 )
@@ -505,7 +578,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
578 "id": "stt_language",
579 "title": "Language Code",
580 "description": "Language code (e.g. en, fr, it)",
508 - "type": "input",
581 + "type": "text",
582 "value": settings["stt_language"],
583 }
584 )
@@ -528,7 +601,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
601 "id": "stt_silence_duration",
602 "title": "Silence duration (ms)",
603 "description": "Duration of silence before the server considers speaking to have ended.",
531 - "type": "input",
604 + "type": "text",
605 "value": settings["stt_silence_duration"],
606 }
607 )
@@ -538,7 +611,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
611 "id": "stt_waiting_timeout",
612 "title": "Waiting timeout (ms)",
613 "description": "Duration before the server closes the microphone.",
541 - "type": "input",
614 + "type": "text",
615 "value": settings["stt_waiting_timeout"],
616 }
617 )
@@ -621,39 +694,39 @@ def normalize_settings(settings: Settings) -> Settings:
694 return copy
695
696
624 -def get_chat_model(settings: Settings | None = None) -> BaseChatModel:
625 - if not settings:
626 - settings = get_settings()
627 - return get_model(
628 - type=ModelType.CHAT,
629 - provider=ModelProvider[settings["chat_model_provider"]],
630 - name=settings["chat_model_name"],
631 - temperature=settings["chat_model_temperature"],
632 - **settings["chat_model_kwargs"],
633 - )
634 -
635 -
636 -def get_utility_model(settings: Settings | None = None) -> BaseChatModel:
637 - if not settings:
638 - settings = get_settings()
639 - return get_model(
640 - type=ModelType.CHAT,
641 - provider=ModelProvider[settings["util_model_provider"]],
642 - name=settings["util_model_name"],
643 - temperature=settings["util_model_temperature"],
644 - **settings["util_model_kwargs"],
645 - )
646 -
647 -
648 -def get_embedding_model(settings: Settings | None = None) -> Embeddings:
649 - if not settings:
650 - settings = get_settings()
651 - return get_model(
652 - type=ModelType.EMBEDDING,
653 - provider=ModelProvider[settings["embed_model_provider"]],
654 - name=settings["embed_model_name"],
655 - **settings["embed_model_kwargs"],
656 - )
697 +# def get_chat_model(settings: Settings | None = None) -> BaseChatModel:
698 +# if not settings:
699 +# settings = get_settings()
700 +# return get_model(
701 +# type=ModelType.CHAT,
702 +# provider=ModelProvider[settings["chat_model_provider"]],
703 +# name=settings["chat_model_name"],
704 +# temperature=settings["chat_model_temperature"],
705 +# **settings["chat_model_kwargs"],
706 +# )
707 +
708 +
709 +# def get_utility_model(settings: Settings | None = None) -> BaseChatModel:
710 +# if not settings:
711 +# settings = get_settings()
712 +# return get_model(
713 +# type=ModelType.CHAT,
714 +# provider=ModelProvider[settings["util_model_provider"]],
715 +# name=settings["util_model_name"],
716 +# temperature=settings["util_model_temperature"],
717 +# **settings["util_model_kwargs"],
718 +# )
719 +
720 +
721 +# def get_embedding_model(settings: Settings | None = None) -> Embeddings:
722 +# if not settings:
723 +# settings = get_settings()
724 +# return get_model(
725 +# type=ModelType.EMBEDDING,
726 +# provider=ModelProvider[settings["embed_model_provider"]],
727 +# name=settings["embed_model_name"],
728 +# **settings["embed_model_kwargs"],
729 +# )
730
731
732 def _read_settings_file() -> Settings | None:
@@ -697,20 +770,32 @@ def _write_sensitive_settings(settings: Settings):
770
771
772 def get_default_settings() -> Settings:
773 + from models import ModelProvider
774 +
775 return Settings(
776 chat_model_provider=ModelProvider.OPENAI.name,
777 chat_model_name="gpt-4o-mini",
778 chat_model_temperature=0,
779 chat_model_kwargs={},
705 - chat_model_ctx_length=8192,
780 + chat_model_ctx_length=120000,
781 chat_model_ctx_history=0.7,
782 + chat_model_rl_requests=0,
783 + chat_model_rl_input=0,
784 + chat_model_rl_output=0,
785 util_model_provider=ModelProvider.OPENAI.name,
786 util_model_name="gpt-4o-mini",
787 util_model_temperature=0,
788 + util_model_ctx_length=120000,
789 + util_model_ctx_input=0.7,
790 util_model_kwargs={},
791 + util_model_rl_requests=60,
792 + util_model_rl_input=0,
793 + util_model_rl_output=0,
794 embed_model_provider=ModelProvider.OPENAI.name,
795 embed_model_name="text-embedding-3-small",
796 embed_model_kwargs={},
797 + embed_model_rl_requests=0,
798 + embed_model_rl_input=0,
799 api_keys={},
800 auth_login="",
801 auth_password="",
python/tools/behaviour_adjustment.py
+3 -3
@@ -21,15 +21,15 @@ async def update_behaviour(agent: Agent, log_item: LogItem, adjustments: str):
21 current_rules = read_rules(agent)
22
23 # log query streamed by LLM
24 - def log_callback(content):
24 + async def log_callback(content):
25 log_item.stream(ruleset=content)
26
27 msg = agent.read_prompt("behaviour.merge.msg.md", current_rules=current_rules, adjustments=adjustments)
28
29 # call util llm to find solutions in history
30 - adjustments_merge = await agent.call_utility_llm(
30 + adjustments_merge = await agent.call_utility_model(
31 system=system,
32 - msg=msg,
32 + message=msg,
33 callback=log_callback,
34 )
35
python/tools/response.py
-1
@@ -3,7 +3,6 @@ from python.helpers.tool import Tool, Response
3 class ResponseTool(Tool):
4
5 async def execute(self,**kwargs):
6 - self.agent.set_data("timeout", self.agent.config.response_timeout_seconds)
6 return Response(message=self.args["text"], break_loop=True)
7
8 async def before_execution(self, **kwargs):
run_ui.py
+1 -1
@@ -92,7 +92,7 @@ def run():
92
93 server = None
94
95 - def register_api_handler(app, handler):
95 + def register_api_handler(app, handler: type[ApiHandler]):
96 name = handler.__module__.split(".")[-1]
97 instance = handler(app, lock)
98 @requires_auth
webui/css/settings.css
+1
@@ -42,6 +42,7 @@
42 /* Input Styles */
43 input[type="text"],
44 input[type="password"],
45 +input[type="number"],
46 textarea,
47 select {
48 width: 100%;
webui/index.html
+10 -1
@@ -451,12 +451,21 @@
451
452 <div class="field-control">
453 <!-- Input field -->
454 - <template x-if="field.type === 'input'">
454 + <template x-if="field.type === 'text'">
455 <input type="text" :class="field.classes" :value="field.value"
456 :readonly="field.readonly === true"
457 @input="field.value = $event.target.value">
458 </template>
459
460 + <!-- Number field -->
461 + <template x-if="field.type === 'number'">
462 + <input type="number" :class="field.classes" :value="field.value"
463 + :readonly="field.readonly === true"
464 + @input="field.value = $event.target.value"
465 + :min="field.min" :max="field.max" :step="field.step">
466 + </template>
467 +
468 +
469 <!-- Password field -->
470 <template x-if="field.type === 'password'">
471 <input type="password" :class="field.classes" :value="field.value"