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"