agentify-core 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ from typing import Any, Dict, List, Optional, Protocol
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ class AgentCallbackHandler(Protocol):
8
+ """Interface for agent callbacks to allow observability and side-effects."""
9
+
10
+ def on_agent_start(self, agent_name: str, user_input: str) -> None:
11
+ """Called when the agent starts processing a request."""
12
+ ...
13
+
14
+ def on_agent_finish(self, agent_name: str, response: str) -> None:
15
+ """Called when the agent finishes processing a request."""
16
+ ...
17
+
18
+ def on_tool_start(self, tool_name: str, args: Dict[str, Any]) -> None:
19
+ """Called when a tool is about to be executed."""
20
+ ...
21
+
22
+ def on_tool_finish(self, tool_name: str, output: str) -> None:
23
+ """Called when a tool finishes execution."""
24
+ ...
25
+
26
+ def on_llm_start(self, model_name: str, messages: List[Dict[str, Any]]) -> None:
27
+ """Called when the LLM is about to be called."""
28
+ ...
29
+
30
+ def on_llm_new_token(self, token: str) -> None:
31
+ """Called when a new token is generated (streaming only)."""
32
+ ...
33
+
34
+ def on_llm_end(self, response: Any) -> None:
35
+ """Called when the LLM finishes generating a response."""
36
+ ...
37
+
38
+ def on_error(self, error: Exception, context: str) -> None:
39
+ """Called when an error occurs."""
40
+ ...
41
+
42
+
43
+ class LoggingCallbackHandler(AgentCallbackHandler):
44
+ """Simple callback handler that logs events using the standard logging module."""
45
+
46
+ def __init__(self, logger_instance: Optional[logging.Logger] = None):
47
+ self.logger = logger_instance or logger
48
+
49
+ def on_agent_start(self, agent_name: str, user_input: str) -> None:
50
+ self.logger.info(f"Agent '{agent_name}' started. Input: {user_input[:100]}...")
51
+
52
+ def on_agent_finish(self, agent_name: str, response: str) -> None:
53
+ self.logger.info(
54
+ f"Agent '{agent_name}' finished. Response: {response[:100]}..."
55
+ )
56
+
57
+ def on_tool_start(self, tool_name: str, args: Dict[str, Any]) -> None:
58
+ self.logger.info(f"Tool '{tool_name}' started. Args: {args}")
59
+
60
+ def on_tool_finish(self, tool_name: str, output: str) -> None:
61
+ self.logger.info(f"Tool '{tool_name}' finished. Output: {output[:100]}...")
62
+
63
+ def on_llm_start(self, model_name: str, messages: List[Dict[str, Any]]) -> None:
64
+ self.logger.debug(f"LLM '{model_name}' started. Messages: {len(messages)}")
65
+
66
+ def on_llm_new_token(self, token: str) -> None:
67
+ pass # Too verbose for standard logging
68
+
69
+ def on_llm_end(self, response: Any) -> None:
70
+ self.logger.debug("LLM finished.")
71
+
72
+ def on_error(self, error: Exception, context: str) -> None:
73
+ self.logger.error(f"Error in {context}: {error}", exc_info=True)
@@ -0,0 +1,30 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, Any, Optional
3
+
4
+
5
+ @dataclass
6
+ class ImageConfig:
7
+ """Configuration for image processing."""
8
+ max_side_px: int = 1024
9
+ quality: int = 90
10
+ detail: str = "auto"
11
+
12
+
13
+ @dataclass
14
+ class AgentConfig:
15
+ """Configuration for the agent's behavior and model parameters."""
16
+ name: str
17
+ system_prompt: str
18
+ provider: str
19
+ model_name: str
20
+ temperature: float = 0.5
21
+ timeout: int = 60
22
+ stream: bool = False
23
+ max_retries: int = 3
24
+ max_tool_iter: int = 5
25
+ client_config_override: Optional[Dict[str, Any]] = None
26
+ callbacks: list = None
27
+
28
+ def __post_init__(self):
29
+ if self.callbacks is None:
30
+ self.callbacks = []
agentify/core/tool.py ADDED
@@ -0,0 +1,30 @@
1
+ import json
2
+ from typing import Dict, Any, Callable
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class Tool:
8
+ """Wrapper for binding JSON-schema with its Python function."""
9
+
10
+ schema: Dict[str, Any]
11
+ func: Callable[..., Any]
12
+
13
+ def __post_init__(self) -> None:
14
+ if "name" not in self.schema:
15
+ raise ValueError("Tool schema must include 'name'")
16
+
17
+ @property
18
+ def name(self) -> str:
19
+ return self.schema["name"]
20
+
21
+ def __call__(self, **kwargs: Any) -> str:
22
+ """Executes the function and returns JSON or string; captures generic errors."""
23
+ try:
24
+ result = self.func(**kwargs)
25
+ except Exception as exc: # noqa: BLE001
26
+ return json.dumps({"error": str(exc)}, ensure_ascii=False)
27
+
28
+ if isinstance(result, (dict, list)):
29
+ return json.dumps(result, ensure_ascii=False)
30
+ return str(result)
File without changes
@@ -0,0 +1,3 @@
1
+ from agentify.extensions.prompts.assistant import assistant_prompt
2
+
3
+ __all__ = ["assistant_prompt"]
@@ -0,0 +1,16 @@
1
+ assistant_prompt = """
2
+ You are an AI assistant with access to tools whose goal is to resolve user queries in a professional, accurate, and friendly manner. Follow these guidelines:
3
+
4
+ 1. Response Style
5
+ - Respond clearly and concisely, using natural punctuation.
6
+ - Do not include production notes, annotations, or internal instructions.
7
+
8
+ 2. User Interaction
9
+ - Maintain a professional, helpful, and empathetic tone.
10
+ - Ask clarifying questions if you need more context or details.
11
+ - Acknowledge and value the user's effort.
12
+
13
+ 3. Tool Usage
14
+ - Detect when it is relevant to use the available tools and do so efficiently.
15
+ - Integrate the results of the tools into your final explanation.
16
+ """
@@ -0,0 +1,9 @@
1
+ from agentify.extensions.tools.time import get_current_time_tool
2
+ from agentify.extensions.tools.calculator import calculate_expression_tool
3
+ from agentify.extensions.tools.weather import get_weather_tool
4
+
5
+ __all__ = [
6
+ "get_current_time_tool",
7
+ "calculate_expression_tool",
8
+ "get_weather_tool",
9
+ ]
@@ -0,0 +1,56 @@
1
+ from agentify.core.tool import Tool
2
+ import ast
3
+ import operator as op
4
+
5
+
6
+ calculate_expression_schema = {
7
+ "name": "calculate_expression",
8
+ "description": "Evalúa una expresión matemática segura y devuelve el resultado.",
9
+ "parameters": {
10
+ "type": "object",
11
+ "properties": {
12
+ "expression": {
13
+ "type": "string",
14
+ "description": "Expresión matemática a calcular, por ejemplo '2 + 2 * (3 - 1)'.",
15
+ }
16
+ },
17
+ "required": ["expression"],
18
+ },
19
+ }
20
+
21
+
22
+ _allowed_ops = {
23
+ ast.Add: op.add,
24
+ ast.Sub: op.sub,
25
+ ast.Mult: op.mul,
26
+ ast.Div: op.truediv,
27
+ ast.Pow: op.pow,
28
+ ast.Mod: op.mod,
29
+ ast.UAdd: op.pos,
30
+ ast.USub: op.neg,
31
+ }
32
+
33
+
34
+ def _eval_node(node):
35
+ if isinstance(node, ast.Num):
36
+ return node.n
37
+ if isinstance(node, ast.BinOp):
38
+ left = _eval_node(node.left)
39
+ right = _eval_node(node.right)
40
+ return _allowed_ops[type(node.op)](left, right)
41
+ if isinstance(node, ast.UnaryOp):
42
+ operand = _eval_node(node.operand)
43
+ return _allowed_ops[type(node.op)](operand)
44
+ raise ValueError(f"Operador no permitido: {node}")
45
+
46
+
47
+ def calculate_expression(expression: str):
48
+ try:
49
+ tree = ast.parse(expression, mode="eval").body
50
+ result = _eval_node(tree)
51
+ return {"result": result}
52
+ except Exception as e:
53
+ return {"error": f"Expresión inválida: {e}"}
54
+
55
+
56
+ calculate_expression_tool = Tool(calculate_expression_schema, calculate_expression)
@@ -0,0 +1,21 @@
1
+ from agentify.core.tool import Tool
2
+ import datetime
3
+
4
+
5
+ get_current_time_schema = {
6
+ "name": "get_current_time",
7
+ "description": "Devuelve la hora y fecha actual en formato ISO 8601.",
8
+ "parameters": {
9
+ "type": "object",
10
+ "properties": {},
11
+ "required": [],
12
+ },
13
+ }
14
+
15
+
16
+ def get_current_time():
17
+ now = datetime.datetime.now().astimezone().isoformat()
18
+ return {"current_time": now}
19
+
20
+
21
+ get_current_time_tool = Tool(get_current_time_schema, get_current_time)
@@ -0,0 +1,51 @@
1
+ from agentify.core.tool import Tool
2
+ import os
3
+ import requests
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+
9
+ get_weather_schema = {
10
+ "name": "get_weather",
11
+ "description": "Obtiene el estado del tiempo o clima actual para una ciudad o zona especificada.",
12
+ "parameters": {
13
+ "type": "object",
14
+ "properties": {
15
+ "location": {
16
+ "type": "string",
17
+ "description": "Nombre de la ciudad o zona para consultar el clima.",
18
+ }
19
+ },
20
+ "required": ["location"],
21
+ },
22
+ }
23
+
24
+
25
+ def get_weather(location: str):
26
+ api_key = os.getenv("OPENWEATHER_API_KEY")
27
+ if not api_key:
28
+ return {"error": "Variable de entorno OPENWEATHER_API_KEY no configurada."}
29
+ try:
30
+ response = requests.get(
31
+ "https://api.openweathermap.org/data/2.5/weather",
32
+ params={"q": location, "appid": api_key, "units": "metric"},
33
+ )
34
+ data = response.json()
35
+ if response.status_code != 200:
36
+ return {
37
+ "error": data.get("message", "Error desconocido al obtener el clima.")
38
+ }
39
+ weather = {
40
+ "location": data["name"],
41
+ "description": data["weather"][0]["description"],
42
+ "temperature": data["main"]["temp"],
43
+ "humidity": data["main"]["humidity"],
44
+ "wind_speed": data["wind"]["speed"],
45
+ }
46
+ return {"weather": weather}
47
+ except Exception as e:
48
+ return {"error": f"Error al conectar con el servicio de clima: {e}"}
49
+
50
+
51
+ get_weather_tool = Tool(get_weather_schema, get_weather)
@@ -0,0 +1,6 @@
1
+ from agentify.llm.client import LLMClientFactory, LLMClientType
2
+
3
+ __all__ = [
4
+ "LLMClientFactory",
5
+ "LLMClientType",
6
+ ]
agentify/llm/client.py ADDED
@@ -0,0 +1,133 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from typing import Union, Dict, Any, Optional, Callable
4
+ from openai import OpenAI, AzureOpenAI
5
+
6
+ load_dotenv()
7
+
8
+ LLMClientType = Union[OpenAI, AzureOpenAI]
9
+
10
+ ClientBuilder = Callable[[Dict[str, Any], int], LLMClientType]
11
+
12
+
13
+ class LLMClientFactory:
14
+ """Factory class to create LLM client instances for different providers."""
15
+
16
+ SUPPORTED_PROVIDERS = ["azure", "openai", "deepseek", "gemini", "anthropic"]
17
+
18
+ def __init__(self, default_timeout: int = 30):
19
+ self.default_timeout = default_timeout
20
+ self._builders: Dict[str, ClientBuilder] = {
21
+ "openai": self._create_openai_client,
22
+ "deepseek": self._create_deepseek_client,
23
+ "gemini": self._create_gemini_client,
24
+ "azure": self._create_azure_client,
25
+ "anthropic": self._create_anthropic_client,
26
+ "llama": self._create_llama_client,
27
+ }
28
+
29
+ def _get_env_or_config(
30
+ self, key: str, env_var_name: str, config: Dict[str, Any], required: bool = True
31
+ ) -> Optional[str]:
32
+ """Helper to obtain a value from config_override or an environment variable."""
33
+ value = config.get(key, os.getenv(env_var_name))
34
+ if required and not value:
35
+ raise ValueError(
36
+ f"Parameter '{key}' (or environment variable '{env_var_name}') is required but was not found."
37
+ )
38
+ return value
39
+
40
+ def _create_openai_client(self, config: Dict[str, Any], timeout: int) -> OpenAI:
41
+ api_key = self._get_env_or_config("api_key", "OPENAI_API_KEY", config)
42
+ return OpenAI(
43
+ api_key=api_key,
44
+ timeout=timeout,
45
+ )
46
+
47
+ def _create_deepseek_client(self, config: Dict[str, Any], timeout: int) -> OpenAI:
48
+ api_key = self._get_env_or_config("api_key", "DEEPSEEK_API_KEY", config)
49
+ base_url = "https://api.deepseek.com"
50
+ return OpenAI(
51
+ api_key=api_key,
52
+ base_url=base_url,
53
+ timeout=timeout,
54
+ )
55
+
56
+ def _create_anthropic_client(self, config: Dict[str, Any], timeout: int) -> OpenAI:
57
+ api_key = self._get_env_or_config("api_key", "ANTHROPIC_API_KEY", config)
58
+ base_url = "https://api.anthropic.com/v1/"
59
+
60
+ return OpenAI(
61
+ api_key=api_key,
62
+ base_url=base_url,
63
+ timeout=timeout,
64
+ )
65
+
66
+ def _create_llama_client(self, config: Dict[str, Any], timeout: int) -> OpenAI:
67
+ api_key = self._get_env_or_config("api_key", "LLAMA_API_KEY", config)
68
+ base_url = "https://api.llama.com/compat/v1/"
69
+
70
+ return OpenAI(
71
+ api_key=api_key,
72
+ base_url=base_url,
73
+ timeout=timeout,
74
+ )
75
+
76
+ def _create_gemini_client(self, config: Dict[str, Any], timeout: int) -> OpenAI:
77
+ api_key = self._get_env_or_config("api_key", "GEMINI_API_KEY", config)
78
+ base_url = "https://generativelanguage.googleapis.com/v1beta/openai/"
79
+
80
+ client_args = {"api_key": api_key, "timeout": timeout}
81
+ if base_url:
82
+ client_args["base_url"] = base_url
83
+ else:
84
+ print(
85
+ "Warning: No GEMINI_URL provided for Gemini. Assuming the OpenAI SDK handles it or it's not needed."
86
+ )
87
+
88
+ return OpenAI(**client_args)
89
+
90
+ def _create_azure_client(self, config: Dict[str, Any], timeout: int) -> AzureOpenAI:
91
+ api_key = self._get_env_or_config("api_key", "AZURE_OPENAI_KEY", config)
92
+ api_version = self._get_env_or_config("api_version", "API_VERSION", config)
93
+ azure_endpoint = self._get_env_or_config(
94
+ "azure_endpoint", "AZURE_OPENAI_ENDPOINT", config
95
+ )
96
+ return AzureOpenAI(
97
+ api_key=api_key,
98
+ api_version=api_version,
99
+ azure_endpoint=azure_endpoint,
100
+ timeout=timeout,
101
+ )
102
+
103
+ def create_client(
104
+ self,
105
+ provider: str,
106
+ config_override: Optional[Dict[str, Any]] = None,
107
+ timeout: Optional[int] = None,
108
+ ) -> LLMClientType:
109
+ """Create an LLM client for the specified provider.
110
+
111
+ Args:
112
+ provider: Name of the provider (e.g., "openai", "azure").
113
+ config_override: Optional dict to override or provide configuration parameters (e.g., api_key, base_url) instead of using environment variables.
114
+ timeout: Specific timeout for this client; if not provided, the factory's default_timeout is used.
115
+
116
+ Returns:
117
+ An instance of the requested LLM client.
118
+
119
+ Raises:
120
+ ValueError: If the provider is not supported or required parameters are missing.
121
+ """
122
+ provider_lower = provider.lower()
123
+ if provider_lower not in self._builders:
124
+ raise ValueError(
125
+ f"Provider '{provider}' not supported. "
126
+ f"Supported: {', '.join(self.SUPPORTED_PROVIDERS)}"
127
+ )
128
+
129
+ builder_func = self._builders[provider_lower]
130
+ effective_timeout = timeout if timeout is not None else self.default_timeout
131
+ effective_config = config_override or {}
132
+
133
+ return builder_func(effective_config, effective_timeout)
@@ -0,0 +1,17 @@
1
+ from agentify.memory.interfaces import (
2
+ MemoryAddress,
3
+ Message,
4
+ ConversationStore,
5
+ TokenCounter,
6
+ )
7
+ from agentify.memory.service import MemoryService
8
+ from agentify.memory.policies import MemoryPolicy
9
+
10
+ __all__ = [
11
+ "MemoryAddress",
12
+ "Message",
13
+ "ConversationStore",
14
+ "TokenCounter",
15
+ "MemoryService",
16
+ "MemoryPolicy",
17
+ ]
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Dict, List, Optional, Protocol, Tuple
4
+ from datetime import datetime, timezone
5
+ import uuid
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class MemoryAddress:
10
+ """
11
+ Composite key that identifies *where* messages live.
12
+ You control these fields from your API boundary.
13
+ Add/remove fields to match your domain (e.g., team_id, channel_id).
14
+ """
15
+ api_version: Optional[str] = None
16
+ tenant_id: Optional[str] = None
17
+ user_id: Optional[str] = None
18
+ conversation_id: Optional[str] = None
19
+ agent_id: Optional[str] = None
20
+ # extras lets you pass stable routing dimensions if needed.
21
+ extras: Tuple[Tuple[str, str], ...] = ()
22
+
23
+ def as_tuple(self) -> Tuple:
24
+ """Stable, hashable representation for keys and indexing."""
25
+ return (
26
+ self.api_version,
27
+ self.tenant_id,
28
+ self.user_id,
29
+ self.conversation_id,
30
+ self.agent_id,
31
+ self.extras,
32
+ )
33
+
34
+ def key_str(self, prefix: str = "mem") -> str:
35
+ """Human-readable key for key-value backends (e.g., Redis)."""
36
+ parts = [
37
+ ("v", self.api_version),
38
+ ("t", self.tenant_id),
39
+ ("u", self.user_id),
40
+ ("c", self.conversation_id),
41
+ ("a", self.agent_id),
42
+ ] + list(self.extras)
43
+ joined = ":".join(f"{k}={v}" for k, v in parts if v)
44
+ return f"{prefix}:{joined}" if joined else prefix
45
+
46
+
47
+ @dataclass(slots=True)
48
+ class Message:
49
+ """
50
+ Canonical chat message stored in the memory layer.
51
+ - content can be str or a list of parts (vision/multimodal).
52
+ - metadata is free-form and backend-agnostic.
53
+ """
54
+ role: str # "system" | "user" | "assistant" | "tool"
55
+ content: Optional[Any] = None # str | list[dict]
56
+ name: Optional[str] = None # tool name when role="tool"
57
+ tool_call_id: Optional[str] = None
58
+ metadata: Dict[str, Any] = field(default_factory=dict)
59
+ id: str = field(default_factory=lambda: uuid.uuid4().hex)
60
+ ts: int = field(default_factory=lambda: int(datetime.now(timezone.utc).timestamp()))
61
+
62
+ def to_openai(self) -> Dict[str, Any]:
63
+ """Convert to OpenAI Chat Completions message format."""
64
+ d: Dict[str, Any] = {"role": self.role}
65
+ if self.content is not None:
66
+ d["content"] = self.content
67
+ if self.name is not None:
68
+ d["name"] = self.name
69
+ if self.tool_call_id is not None:
70
+ d["tool_call_id"] = self.tool_call_id
71
+ d.update(self.metadata) # carry extra fields (e.g., tool_calls)
72
+ return d
73
+
74
+ def to_dict(self) -> Dict[str, Any]:
75
+ """Backend-agnostic serialization (works with slots dataclasses)."""
76
+ return {
77
+ "role": self.role,
78
+ "content": self.content,
79
+ "name": self.name,
80
+ "tool_call_id": self.tool_call_id,
81
+ "metadata": self.metadata,
82
+ "id": self.id,
83
+ "ts": self.ts,
84
+ }
85
+
86
+ class ConversationStore(Protocol):
87
+ """
88
+ Minimal CRUD for conversation histories.
89
+ The store does NOT generate addresses; it only consumes them.
90
+ """
91
+
92
+ def append_message(self, addr: MemoryAddress, msg: Message) -> None: ...
93
+ def read_messages(self, addr: MemoryAddress, start: int = 0, end: int = -1) -> List[Message]: ...
94
+ def replace_messages(self, addr: MemoryAddress, messages: List[Message]) -> None: ...
95
+ def delete_conversation(self, addr: MemoryAddress) -> None: ...
96
+ def set_ttl(self, addr: MemoryAddress, seconds: int) -> None: ...
97
+
98
+
99
+ class TokenCounter(Protocol):
100
+ """Callable that returns token count for OpenAI-formatted messages."""
101
+ def __call__(self, openai_messages: List[Dict[str, Any]]) -> int: ...
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+ from typing import Callable, List, Optional
3
+ from .interfaces import ConversationStore, MemoryAddress, Message, TokenCounter
4
+
5
+
6
+ class MemoryPolicy:
7
+ """
8
+ Role-based sliding window, optional token budget and summarization.
9
+ Backend-agnostic; operates via ConversationStore.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ store: ConversationStore,
15
+ *,
16
+ ttl_seconds: Optional[int] = None,
17
+ max_user_msgs: int = 6,
18
+ max_assistant_msgs: int = 6,
19
+ tokenizer: Optional[TokenCounter] = None,
20
+ max_tokens: Optional[int] = None,
21
+ summarizer: Optional[Callable[[List[Message]], Message]] = None,
22
+ ) -> None:
23
+ self.store = store
24
+ self.ttl = ttl_seconds
25
+ self.max_user = max_user_msgs
26
+ self.max_assistant = max_assistant_msgs
27
+ self.tokenizer = tokenizer
28
+ self.max_tokens = max_tokens
29
+ self.summarizer = summarizer
30
+
31
+ def _apply_ttl(self, addr: MemoryAddress) -> None:
32
+ if self.ttl:
33
+ self.store.set_ttl(addr, self.ttl)
34
+
35
+ def on_append(self, addr: MemoryAddress, msg: Message) -> None:
36
+ self.store.append_message(addr, msg)
37
+ self._apply_ttl(addr)
38
+ self._prune_window(addr)
39
+ self._ensure_token_budget(addr)
40
+
41
+ def _prune_window(self, addr: MemoryAddress) -> None:
42
+ msgs = self.store.read_messages(addr)
43
+ if not msgs:
44
+ return
45
+
46
+ system = msgs[0] if msgs[0].role == "system" else None
47
+ users = [i for i, m in enumerate(msgs) if m.role == "user"]
48
+ assistants = [i for i, m in enumerate(msgs) if m.role == "assistant"]
49
+
50
+ if len(users) <= self.max_user and len(assistants) <= self.max_assistant:
51
+ return
52
+
53
+ cutoff_user = users[-self.max_user] if len(users) > self.max_user else 0
54
+ cutoff_ass = assistants[-self.max_assistant] if len(assistants) > self.max_assistant else 0
55
+ cutoff = min(cutoff_user, cutoff_ass)
56
+ if cutoff <= 0:
57
+ return
58
+
59
+ new_msgs = msgs[cutoff:]
60
+ if system and (not new_msgs or new_msgs[0].role != "system"):
61
+ new_msgs.insert(0, system)
62
+
63
+ self.store.replace_messages(addr, new_msgs)
64
+
65
+ def _ensure_token_budget(self, addr: MemoryAddress) -> None:
66
+ if not (self.tokenizer and self.max_tokens):
67
+ return
68
+ msgs = self.store.read_messages(addr)
69
+ oai = [m.to_openai() for m in msgs]
70
+ if self.tokenizer(oai) <= self.max_tokens:
71
+ return
72
+ if self.summarizer:
73
+ system = msgs[0] if msgs and msgs[0].role == "system" else None
74
+ core = msgs[1:-6] if system else msgs[:-6]
75
+ tail = msgs[-6:]
76
+ if core:
77
+ summary = self.summarizer(core)
78
+ new_msgs = ([system] if system else []) + [summary] + tail
79
+ self.store.replace_messages(addr, new_msgs)