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"