agent-runtime-core 0.7.0__py3-none-any.whl → 0.8.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.
Files changed (38) hide show
  1. agent_runtime_core/__init__.py +109 -2
  2. agent_runtime_core/agentic_loop.py +254 -0
  3. agent_runtime_core/config.py +54 -4
  4. agent_runtime_core/config_schema.py +307 -0
  5. agent_runtime_core/files/__init__.py +88 -0
  6. agent_runtime_core/files/base.py +343 -0
  7. agent_runtime_core/files/ocr.py +406 -0
  8. agent_runtime_core/files/processors.py +508 -0
  9. agent_runtime_core/files/tools.py +317 -0
  10. agent_runtime_core/files/vision.py +360 -0
  11. agent_runtime_core/interfaces.py +106 -0
  12. agent_runtime_core/json_runtime.py +509 -0
  13. agent_runtime_core/llm/__init__.py +80 -7
  14. agent_runtime_core/llm/anthropic.py +133 -12
  15. agent_runtime_core/llm/models_config.py +180 -0
  16. agent_runtime_core/memory/__init__.py +70 -0
  17. agent_runtime_core/memory/manager.py +554 -0
  18. agent_runtime_core/memory/mixin.py +294 -0
  19. agent_runtime_core/multi_agent.py +569 -0
  20. agent_runtime_core/persistence/__init__.py +2 -0
  21. agent_runtime_core/persistence/file.py +277 -0
  22. agent_runtime_core/rag/__init__.py +65 -0
  23. agent_runtime_core/rag/chunking.py +224 -0
  24. agent_runtime_core/rag/indexer.py +253 -0
  25. agent_runtime_core/rag/retriever.py +261 -0
  26. agent_runtime_core/runner.py +193 -15
  27. agent_runtime_core/tool_calling_agent.py +88 -130
  28. agent_runtime_core/tools.py +179 -0
  29. agent_runtime_core/vectorstore/__init__.py +193 -0
  30. agent_runtime_core/vectorstore/base.py +138 -0
  31. agent_runtime_core/vectorstore/embeddings.py +242 -0
  32. agent_runtime_core/vectorstore/sqlite_vec.py +328 -0
  33. agent_runtime_core/vectorstore/vertex.py +295 -0
  34. {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/METADATA +236 -1
  35. agent_runtime_core-0.8.0.dist-info/RECORD +63 -0
  36. agent_runtime_core-0.7.0.dist-info/RECORD +0 -39
  37. {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/WHEEL +0 -0
  38. {agent_runtime_core-0.7.0.dist-info → agent_runtime_core-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,15 +6,38 @@ Provides:
6
6
  - OpenAIClient: OpenAI API client
7
7
  - AnthropicClient: Anthropic API client
8
8
  - LiteLLMClient: LiteLLM adapter (optional)
9
+ - get_llm_client: Factory with auto-detection from model name
10
+ - get_llm_client_for_model: Get client for a specific model
9
11
  """
10
12
 
13
+ from typing import Optional
14
+
11
15
  from agent_runtime_core.interfaces import LLMClient, LLMResponse, LLMStreamChunk
16
+ from agent_runtime_core.llm.models_config import (
17
+ ModelInfo,
18
+ SUPPORTED_MODELS,
19
+ DEFAULT_MODEL,
20
+ get_model_info,
21
+ get_provider_for_model,
22
+ list_models_for_ui,
23
+ )
12
24
 
13
25
  __all__ = [
26
+ # Interfaces
14
27
  "LLMClient",
15
28
  "LLMResponse",
16
29
  "LLMStreamChunk",
30
+ # Factory functions
17
31
  "get_llm_client",
32
+ "get_llm_client_for_model",
33
+ # Model config
34
+ "ModelInfo",
35
+ "SUPPORTED_MODELS",
36
+ "DEFAULT_MODEL",
37
+ "get_model_info",
38
+ "get_provider_for_model",
39
+ "list_models_for_ui",
40
+ # Exceptions
18
41
  "OpenAIConfigurationError",
19
42
  "AnthropicConfigurationError",
20
43
  ]
@@ -30,37 +53,56 @@ class AnthropicConfigurationError(Exception):
30
53
  pass
31
54
 
32
55
 
33
- def get_llm_client(provider: str = None, **kwargs) -> LLMClient:
56
+ def get_llm_client(
57
+ provider: Optional[str] = None,
58
+ model: Optional[str] = None,
59
+ **kwargs
60
+ ) -> LLMClient:
34
61
  """
35
62
  Factory function to get an LLM client.
36
63
 
64
+ Can auto-detect provider from model name if model is provided.
65
+
37
66
  Args:
38
- provider: "openai", "anthropic", "litellm", etc.
67
+ provider: "openai", "anthropic", "litellm", etc. (optional if model provided)
68
+ model: Model ID - if provided, auto-detects provider
39
69
  **kwargs: Provider-specific configuration (e.g., api_key, default_model)
40
70
 
41
71
  Returns:
42
72
  LLMClient instance
43
-
73
+
44
74
  Raises:
45
75
  OpenAIConfigurationError: If OpenAI is selected but API key is not configured
46
76
  AnthropicConfigurationError: If Anthropic is selected but API key is not configured
47
77
  ValueError: If an unknown provider is specified
48
-
78
+
49
79
  Example:
50
- # Using config (recommended)
80
+ # Auto-detect from model name (recommended)
81
+ llm = get_llm_client(model="claude-sonnet-4-20250514")
82
+ llm = get_llm_client(model="gpt-4o")
83
+
84
+ # Using config
51
85
  from agent_runtime_core.config import configure
52
86
  configure(model_provider="openai", openai_api_key="sk-...")
53
87
  llm = get_llm_client()
54
-
88
+
55
89
  # Or with explicit API key
56
90
  llm = get_llm_client(api_key='sk-...')
57
-
91
+
58
92
  # Or with a different provider
59
93
  llm = get_llm_client(provider='anthropic', api_key='sk-ant-...')
60
94
  """
61
95
  from agent_runtime_core.config import get_config
62
96
 
63
97
  config = get_config()
98
+
99
+ # Auto-detect provider from model name if not explicitly provided
100
+ if provider is None and model:
101
+ detected_provider = get_provider_for_model(model)
102
+ if detected_provider:
103
+ provider = detected_provider
104
+
105
+ # Fall back to config
64
106
  provider = provider or config.model_provider
65
107
 
66
108
  if provider == "openai":
@@ -81,3 +123,34 @@ def get_llm_client(provider: str = None, **kwargs) -> LLMClient:
81
123
  f"Supported providers: 'openai', 'anthropic', 'litellm'\n"
82
124
  f"Set model_provider in your configuration."
83
125
  )
126
+
127
+
128
+ def get_llm_client_for_model(model: str, **kwargs) -> LLMClient:
129
+ """
130
+ Get an LLM client configured for a specific model.
131
+
132
+ This is a convenience function that auto-detects the provider
133
+ and sets the default model.
134
+
135
+ Args:
136
+ model: Model ID (e.g., "gpt-4o", "claude-sonnet-4-20250514")
137
+ **kwargs: Additional client configuration
138
+
139
+ Returns:
140
+ LLMClient configured for the specified model
141
+
142
+ Raises:
143
+ ValueError: If model provider cannot be determined
144
+
145
+ Example:
146
+ llm = get_llm_client_for_model("claude-sonnet-4-20250514")
147
+ response = await llm.generate(messages) # Uses claude-sonnet-4-20250514
148
+ """
149
+ provider = get_provider_for_model(model)
150
+ if not provider:
151
+ raise ValueError(
152
+ f"Cannot determine provider for model: {model}\n"
153
+ f"Known models: {', '.join(SUPPORTED_MODELS.keys())}"
154
+ )
155
+
156
+ return get_llm_client(provider=provider, default_model=model, **kwargs)
@@ -2,6 +2,7 @@
2
2
  Anthropic API client implementation.
3
3
  """
4
4
 
5
+ import json
5
6
  import os
6
7
  from typing import AsyncIterator, Optional
7
8
 
@@ -103,14 +104,17 @@ class AnthropicClient(LLMClient):
103
104
  """Generate a completion from Anthropic."""
104
105
  model = model or self.default_model
105
106
 
106
- # Extract system message
107
+ # Extract system message and convert other messages
107
108
  system_message = None
108
- chat_messages = []
109
+ converted_messages = []
109
110
  for msg in messages:
110
111
  if msg.get("role") == "system":
111
112
  system_message = msg.get("content", "")
112
113
  else:
113
- chat_messages.append(self._convert_message(msg))
114
+ converted_messages.append(self._convert_message(msg))
115
+
116
+ # Merge consecutive messages with the same role (required by Anthropic)
117
+ chat_messages = self._merge_consecutive_messages(converted_messages)
114
118
 
115
119
  request_kwargs = {
116
120
  "model": model,
@@ -152,14 +156,17 @@ class AnthropicClient(LLMClient):
152
156
  """Stream a completion from Anthropic."""
153
157
  model = model or self.default_model
154
158
 
155
- # Extract system message
159
+ # Extract system message and convert other messages
156
160
  system_message = None
157
- chat_messages = []
161
+ converted_messages = []
158
162
  for msg in messages:
159
163
  if msg.get("role") == "system":
160
164
  system_message = msg.get("content", "")
161
165
  else:
162
- chat_messages.append(self._convert_message(msg))
166
+ converted_messages.append(self._convert_message(msg))
167
+
168
+ # Merge consecutive messages with the same role (required by Anthropic)
169
+ chat_messages = self._merge_consecutive_messages(converted_messages)
163
170
 
164
171
  request_kwargs = {
165
172
  "model": model,
@@ -183,18 +190,130 @@ class AnthropicClient(LLMClient):
183
190
  yield LLMStreamChunk(finish_reason="stop")
184
191
 
185
192
  def _convert_message(self, msg: Message) -> dict:
186
- """Convert our message format to Anthropic format."""
193
+ """
194
+ Convert our message format to Anthropic format.
195
+
196
+ Handles:
197
+ - Regular user/assistant messages
198
+ - Assistant messages with tool_calls (need content blocks)
199
+ - Tool result messages (need tool_result content blocks)
200
+ """
187
201
  role = msg.get("role", "user")
188
- if role == "assistant":
189
- role = "assistant"
190
- elif role == "tool":
191
- role = "user" # Tool results go as user messages in Anthropic
192
202
 
203
+ # Handle tool result messages
204
+ if role == "tool":
205
+ # Tool results go as user messages with tool_result content blocks
206
+ return {
207
+ "role": "user",
208
+ "content": [
209
+ {
210
+ "type": "tool_result",
211
+ "tool_use_id": msg.get("tool_call_id", ""),
212
+ "content": msg.get("content", ""),
213
+ }
214
+ ],
215
+ }
216
+
217
+ # Handle assistant messages with tool_calls
218
+ if role == "assistant" and msg.get("tool_calls"):
219
+ content_blocks = []
220
+
221
+ # Add text content if present
222
+ text_content = msg.get("content", "")
223
+ if text_content:
224
+ content_blocks.append({
225
+ "type": "text",
226
+ "text": text_content,
227
+ })
228
+
229
+ # Add tool_use blocks for each tool call
230
+ for tool_call in msg.get("tool_calls", []):
231
+ # Handle both dict format and nested function format
232
+ if "function" in tool_call:
233
+ # OpenAI-style format: {"id": ..., "function": {"name": ..., "arguments": ...}}
234
+ func = tool_call["function"]
235
+ tool_id = tool_call.get("id", "")
236
+ tool_name = func.get("name", "")
237
+ # Arguments might be a string (JSON) or already a dict
238
+ args = func.get("arguments", {})
239
+ if isinstance(args, str):
240
+ try:
241
+ args = json.loads(args)
242
+ except json.JSONDecodeError:
243
+ args = {}
244
+ else:
245
+ # Direct format: {"id": ..., "name": ..., "arguments": ...}
246
+ tool_id = tool_call.get("id", "")
247
+ tool_name = tool_call.get("name", "")
248
+ args = tool_call.get("arguments", {})
249
+ if isinstance(args, str):
250
+ try:
251
+ args = json.loads(args)
252
+ except json.JSONDecodeError:
253
+ args = {}
254
+
255
+ content_blocks.append({
256
+ "type": "tool_use",
257
+ "id": tool_id,
258
+ "name": tool_name,
259
+ "input": args,
260
+ })
261
+
262
+ return {
263
+ "role": "assistant",
264
+ "content": content_blocks,
265
+ }
266
+
267
+ # Regular user or assistant message
193
268
  return {
194
269
  "role": role,
195
270
  "content": msg.get("content", ""),
196
271
  }
197
272
 
273
+ def _merge_consecutive_messages(self, messages: list[dict]) -> list[dict]:
274
+ """
275
+ Merge consecutive messages with the same role.
276
+
277
+ Anthropic requires that messages alternate between user and assistant roles.
278
+ When we have multiple tool results (which become user messages), they need
279
+ to be combined into a single user message with multiple content blocks.
280
+ """
281
+ if not messages:
282
+ return messages
283
+
284
+ merged = []
285
+ for msg in messages:
286
+ if not merged:
287
+ merged.append(msg)
288
+ continue
289
+
290
+ last_msg = merged[-1]
291
+
292
+ # If same role, merge the content
293
+ if msg["role"] == last_msg["role"]:
294
+ last_content = last_msg["content"]
295
+ new_content = msg["content"]
296
+
297
+ # Convert to list format if needed
298
+ if isinstance(last_content, str):
299
+ if last_content:
300
+ last_content = [{"type": "text", "text": last_content}]
301
+ else:
302
+ last_content = []
303
+
304
+ if isinstance(new_content, str):
305
+ if new_content:
306
+ new_content = [{"type": "text", "text": new_content}]
307
+ else:
308
+ new_content = []
309
+
310
+ # Merge content blocks
311
+ last_msg["content"] = last_content + new_content
312
+ else:
313
+ merged.append(msg)
314
+
315
+ return merged
316
+
198
317
  def _convert_tools(self, tools: list[dict]) -> list[dict]:
199
318
  """Convert OpenAI tool format to Anthropic format."""
200
319
  result = []
@@ -217,12 +336,14 @@ class AnthropicClient(LLMClient):
217
336
  if block.type == "text":
218
337
  content += block.text
219
338
  elif block.type == "tool_use":
339
+ # Convert input to JSON string (not Python str() which gives wrong format)
340
+ arguments = json.dumps(block.input) if isinstance(block.input, dict) else str(block.input)
220
341
  tool_calls.append({
221
342
  "id": block.id,
222
343
  "type": "function",
223
344
  "function": {
224
345
  "name": block.name,
225
- "arguments": str(block.input),
346
+ "arguments": arguments,
226
347
  },
227
348
  })
228
349
 
@@ -0,0 +1,180 @@
1
+ """
2
+ Supported LLM models configuration.
3
+
4
+ Provides a central registry of supported models with their providers,
5
+ capabilities, and metadata. Used for:
6
+ - Auto-detecting provider from model name
7
+ - Populating model selectors in UI
8
+ - Validating model choices
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Optional
13
+
14
+
15
+ @dataclass
16
+ class ModelInfo:
17
+ """Information about a supported model."""
18
+ id: str # Model identifier (e.g., "gpt-4o", "claude-sonnet-4-20250514")
19
+ name: str # Display name (e.g., "GPT-4o", "Claude Sonnet 4")
20
+ provider: str # "openai" or "anthropic"
21
+ context_window: int # Max context in tokens
22
+ supports_tools: bool = True
23
+ supports_vision: bool = False
24
+ supports_streaming: bool = True
25
+ description: str = ""
26
+
27
+
28
+ # Registry of supported models
29
+ SUPPORTED_MODELS: dict[str, ModelInfo] = {
30
+ # OpenAI Models
31
+ "gpt-4o": ModelInfo(
32
+ id="gpt-4o",
33
+ name="GPT-4o",
34
+ provider="openai",
35
+ context_window=128000,
36
+ supports_vision=True,
37
+ description="Most capable OpenAI model, multimodal",
38
+ ),
39
+ "gpt-4o-mini": ModelInfo(
40
+ id="gpt-4o-mini",
41
+ name="GPT-4o Mini",
42
+ provider="openai",
43
+ context_window=128000,
44
+ supports_vision=True,
45
+ description="Fast and affordable, good for most tasks",
46
+ ),
47
+ "gpt-4-turbo": ModelInfo(
48
+ id="gpt-4-turbo",
49
+ name="GPT-4 Turbo",
50
+ provider="openai",
51
+ context_window=128000,
52
+ supports_vision=True,
53
+ description="Previous generation flagship",
54
+ ),
55
+ "o1": ModelInfo(
56
+ id="o1",
57
+ name="o1",
58
+ provider="openai",
59
+ context_window=200000,
60
+ supports_tools=False,
61
+ description="Advanced reasoning model",
62
+ ),
63
+ "o1-mini": ModelInfo(
64
+ id="o1-mini",
65
+ name="o1 Mini",
66
+ provider="openai",
67
+ context_window=128000,
68
+ supports_tools=False,
69
+ description="Fast reasoning model",
70
+ ),
71
+ "o3-mini": ModelInfo(
72
+ id="o3-mini",
73
+ name="o3 Mini",
74
+ provider="openai",
75
+ context_window=200000,
76
+ supports_tools=True,
77
+ description="Latest reasoning model with tool use",
78
+ ),
79
+
80
+ # Anthropic Models - Claude 4.5 (latest)
81
+ "claude-sonnet-4-5-20250929": ModelInfo(
82
+ id="claude-sonnet-4-5-20250929",
83
+ name="Claude Sonnet 4.5",
84
+ provider="anthropic",
85
+ context_window=200000,
86
+ supports_vision=True,
87
+ description="Best balance of speed and capability for agents and coding",
88
+ ),
89
+ "claude-opus-4-5-20251101": ModelInfo(
90
+ id="claude-opus-4-5-20251101",
91
+ name="Claude Opus 4.5",
92
+ provider="anthropic",
93
+ context_window=200000,
94
+ supports_vision=True,
95
+ description="Premium model - maximum intelligence with practical performance",
96
+ ),
97
+ "claude-haiku-4-5-20251001": ModelInfo(
98
+ id="claude-haiku-4-5-20251001",
99
+ name="Claude Haiku 4.5",
100
+ provider="anthropic",
101
+ context_window=200000,
102
+ supports_vision=True,
103
+ description="Fastest model with near-frontier intelligence",
104
+ ),
105
+ # Anthropic Models - Claude 4 (previous generation)
106
+ "claude-sonnet-4-20250514": ModelInfo(
107
+ id="claude-sonnet-4-20250514",
108
+ name="Claude Sonnet 4",
109
+ provider="anthropic",
110
+ context_window=200000,
111
+ supports_vision=True,
112
+ description="Previous generation Sonnet",
113
+ ),
114
+ "claude-opus-4-20250514": ModelInfo(
115
+ id="claude-opus-4-20250514",
116
+ name="Claude Opus 4",
117
+ provider="anthropic",
118
+ context_window=200000,
119
+ supports_vision=True,
120
+ description="Previous generation Opus",
121
+ ),
122
+ # Anthropic Models - Claude 3.5 (legacy)
123
+ "claude-3-5-sonnet-20241022": ModelInfo(
124
+ id="claude-3-5-sonnet-20241022",
125
+ name="Claude 3.5 Sonnet",
126
+ provider="anthropic",
127
+ context_window=200000,
128
+ supports_vision=True,
129
+ description="Legacy model, still excellent",
130
+ ),
131
+ "claude-3-5-haiku-20241022": ModelInfo(
132
+ id="claude-3-5-haiku-20241022",
133
+ name="Claude 3.5 Haiku",
134
+ provider="anthropic",
135
+ context_window=200000,
136
+ supports_vision=True,
137
+ description="Legacy fast model",
138
+ ),
139
+ }
140
+
141
+ # Default model to use
142
+ DEFAULT_MODEL = "claude-sonnet-4-5-20250929"
143
+
144
+
145
+ def get_model_info(model_id: str) -> Optional[ModelInfo]:
146
+ """Get info for a model by ID."""
147
+ return SUPPORTED_MODELS.get(model_id)
148
+
149
+
150
+ def get_provider_for_model(model_id: str) -> Optional[str]:
151
+ """
152
+ Detect the provider for a model ID.
153
+
154
+ Returns "openai", "anthropic", or None if unknown.
155
+ """
156
+ # Check registry first
157
+ if model_id in SUPPORTED_MODELS:
158
+ return SUPPORTED_MODELS[model_id].provider
159
+
160
+ # Fallback heuristics for unlisted models
161
+ if model_id.startswith("gpt-") or model_id.startswith("o1") or model_id.startswith("o3"):
162
+ return "openai"
163
+ if model_id.startswith("claude"):
164
+ return "anthropic"
165
+
166
+ return None
167
+
168
+
169
+ def list_models_for_ui() -> list[dict]:
170
+ """Get list of models formatted for UI dropdowns."""
171
+ return [
172
+ {
173
+ "id": m.id,
174
+ "name": m.name,
175
+ "provider": m.provider,
176
+ "description": m.description,
177
+ }
178
+ for m in SUPPORTED_MODELS.values()
179
+ ]
180
+
@@ -0,0 +1,70 @@
1
+ """
2
+ Cross-conversation memory system for AI agents.
3
+
4
+ This module provides automatic memory extraction and recall across conversations,
5
+ allowing agents to remember facts, preferences, and context about users.
6
+
7
+ Key Components:
8
+ - MemoryManager: Extracts and recalls memories using LLM
9
+ - MemoryConfig: Configuration for memory behavior
10
+ - MemoryEnabledAgent: Mixin for adding memory to agents
11
+
12
+ Example usage:
13
+ from agent_runtime_core.memory import MemoryManager, MemoryConfig
14
+ from agent_runtime_core.persistence import FileKnowledgeStore
15
+ from agent_runtime_core.llm import get_llm_client
16
+
17
+ # Setup memory manager
18
+ knowledge_store = FileKnowledgeStore()
19
+ llm = get_llm_client()
20
+ memory = MemoryManager(knowledge_store, llm)
21
+
22
+ # Extract memories from a conversation
23
+ messages = [
24
+ {"role": "user", "content": "My name is Alice and I prefer dark mode."},
25
+ {"role": "assistant", "content": "Nice to meet you, Alice! I've noted your preference for dark mode."},
26
+ ]
27
+ await memory.extract_memories(messages, user_id="user-123")
28
+
29
+ # Recall memories for a new conversation
30
+ relevant_memories = await memory.recall_memories(
31
+ query="What theme does the user prefer?",
32
+ user_id="user-123",
33
+ )
34
+
35
+ # Or use with ToolCallingAgent via mixin
36
+ from agent_runtime_core.memory import MemoryEnabledAgent
37
+ from agent_runtime_core import ToolCallingAgent
38
+
39
+ class MyAgent(MemoryEnabledAgent, ToolCallingAgent):
40
+ memory_enabled = True # Enable memory for this agent
41
+
42
+ @property
43
+ def key(self) -> str:
44
+ return "my-agent"
45
+
46
+ @property
47
+ def system_prompt(self) -> str:
48
+ return "You are a helpful assistant."
49
+
50
+ @property
51
+ def tools(self) -> ToolRegistry:
52
+ return ToolRegistry()
53
+ """
54
+
55
+ from agent_runtime_core.memory.manager import (
56
+ MemoryManager,
57
+ MemoryConfig,
58
+ ExtractedMemory,
59
+ RecalledMemory,
60
+ )
61
+ from agent_runtime_core.memory.mixin import MemoryEnabledAgent
62
+
63
+ __all__ = [
64
+ "MemoryManager",
65
+ "MemoryConfig",
66
+ "ExtractedMemory",
67
+ "RecalledMemory",
68
+ "MemoryEnabledAgent",
69
+ ]
70
+