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
@@ -34,16 +34,18 @@ Example usage:
34
34
  return RunResult(final_output={"message": "Hello!"})
35
35
  """
36
36
 
37
- __version__ = "0.7.0"
37
+ __version__ = "0.8.0"
38
38
 
39
39
  # Core interfaces
40
40
  from agent_runtime_core.interfaces import (
41
41
  AgentRuntime,
42
42
  EventType,
43
+ EventVisibility,
43
44
  ErrorInfo,
44
45
  LLMClient,
45
46
  LLMResponse,
46
47
  LLMStreamChunk,
48
+ LLMToolCall,
47
49
  Message,
48
50
  RunContext,
49
51
  RunResult,
@@ -57,6 +59,12 @@ from agent_runtime_core.interfaces import (
57
59
  # Tool Calling Agent base class
58
60
  from agent_runtime_core.tool_calling_agent import ToolCallingAgent
59
61
 
62
+ # Agentic loop helper
63
+ from agent_runtime_core.agentic_loop import (
64
+ run_agentic_loop,
65
+ AgenticLoopResult,
66
+ )
67
+
60
68
  # Configuration
61
69
  from agent_runtime_core.config import (
62
70
  RuntimeConfig,
@@ -128,6 +136,7 @@ from agent_runtime_core.persistence import (
128
136
  FileConversationStore,
129
137
  FileTaskStore,
130
138
  FilePreferencesStore,
139
+ FileKnowledgeStore,
131
140
  # Manager
132
141
  PersistenceManager,
133
142
  PersistenceConfig,
@@ -135,6 +144,69 @@ from agent_runtime_core.persistence import (
135
144
  configure_persistence,
136
145
  )
137
146
 
147
+ # Agent configuration schema (portable JSON format)
148
+ from agent_runtime_core.config_schema import (
149
+ AgentConfig,
150
+ ToolConfig,
151
+ KnowledgeConfig,
152
+ SubAgentToolConfig,
153
+ )
154
+
155
+ # JSON-based runtime (loads from AgentConfig)
156
+ from agent_runtime_core.json_runtime import (
157
+ JsonAgentRuntime,
158
+ ConfiguredTool,
159
+ SubAgentTool,
160
+ resolve_function,
161
+ )
162
+
163
+ # Vector store (optional - requires additional dependencies)
164
+ # Import these directly from agent_runtime_core.vectorstore when needed:
165
+ # from agent_runtime_core.vectorstore import (
166
+ # VectorStore, VectorRecord, VectorSearchResult,
167
+ # EmbeddingClient, OpenAIEmbeddings, VertexAIEmbeddings,
168
+ # get_vector_store, get_embedding_client,
169
+ # )
170
+
171
+ # RAG (Retrieval Augmented Generation)
172
+ from agent_runtime_core.rag import (
173
+ chunk_text,
174
+ ChunkingConfig,
175
+ TextChunk,
176
+ )
177
+
178
+ # RAG services are imported lazily to avoid circular dependencies
179
+ # Import directly when needed:
180
+ # from agent_runtime_core.rag import KnowledgeIndexer, KnowledgeRetriever
181
+
182
+ # Tool schema builder utilities
183
+ from agent_runtime_core.tools import (
184
+ ToolSchema,
185
+ ToolSchemaBuilder,
186
+ ToolParameter,
187
+ schemas_to_openai_format,
188
+ )
189
+
190
+ # Multi-agent support (agent-as-tool pattern)
191
+ from agent_runtime_core.multi_agent import (
192
+ AgentTool,
193
+ AgentInvocationResult,
194
+ InvocationMode,
195
+ ContextMode,
196
+ SubAgentContext,
197
+ invoke_agent,
198
+ create_agent_tool_handler,
199
+ register_agent_tools,
200
+ build_sub_agent_messages,
201
+ )
202
+
203
+ # Cross-conversation memory
204
+ # Import directly when needed for full functionality:
205
+ # from agent_runtime_core.memory import (
206
+ # MemoryManager, MemoryConfig, MemoryEnabledAgent,
207
+ # ExtractedMemory, RecalledMemory, with_memory,
208
+ # )
209
+
138
210
  __all__ = [
139
211
  # Version
140
212
  "__version__",
@@ -143,6 +215,7 @@ __all__ = [
143
215
  "LLMClient",
144
216
  "LLMResponse",
145
217
  "LLMStreamChunk",
218
+ "LLMToolCall",
146
219
  "Message",
147
220
  "RunContext",
148
221
  "RunResult",
@@ -151,8 +224,12 @@ __all__ = [
151
224
  "ToolDefinition",
152
225
  "TraceSink",
153
226
  "EventType",
227
+ "EventVisibility",
154
228
  "ErrorInfo",
155
- "ToolCallingAgent",
229
+ # Tool calling
230
+ "ToolCallingAgent",
231
+ "run_agentic_loop",
232
+ "AgenticLoopResult",
156
233
  # Configuration
157
234
  "RuntimeConfig",
158
235
  "configure",
@@ -204,9 +281,39 @@ __all__ = [
204
281
  "FileConversationStore",
205
282
  "FileTaskStore",
206
283
  "FilePreferencesStore",
284
+ "FileKnowledgeStore",
207
285
  # Persistence - Manager
208
286
  "PersistenceManager",
209
287
  "PersistenceConfig",
210
288
  "get_persistence_manager",
211
289
  "configure_persistence",
290
+ # Agent configuration schema
291
+ "AgentConfig",
292
+ "ToolConfig",
293
+ "KnowledgeConfig",
294
+ "SubAgentToolConfig",
295
+ # JSON-based runtime
296
+ "JsonAgentRuntime",
297
+ "ConfiguredTool",
298
+ "SubAgentTool",
299
+ "resolve_function",
300
+ # RAG (Retrieval Augmented Generation)
301
+ "chunk_text",
302
+ "ChunkingConfig",
303
+ "TextChunk",
304
+ # Tool schema builder
305
+ "ToolSchema",
306
+ "ToolSchemaBuilder",
307
+ "ToolParameter",
308
+ "schemas_to_openai_format",
309
+ # Multi-agent support
310
+ "AgentTool",
311
+ "AgentInvocationResult",
312
+ "InvocationMode",
313
+ "ContextMode",
314
+ "SubAgentContext",
315
+ "invoke_agent",
316
+ "create_agent_tool_handler",
317
+ "register_agent_tools",
318
+ "build_sub_agent_messages",
212
319
  ]
@@ -0,0 +1,254 @@
1
+ """
2
+ Reusable agentic loop for tool-calling agents.
3
+
4
+ This module provides a flexible `run_agentic_loop` function that handles
5
+ the standard tool-calling pattern:
6
+ 1. Call LLM with tools
7
+ 2. If tool calls, execute them and loop back
8
+ 3. If no tool calls, return final response
9
+
10
+ This can be used by any agent implementation without requiring inheritance.
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from typing import Any, Callable, Optional, Awaitable, Union
17
+
18
+ from agent_runtime_core.interfaces import (
19
+ RunContext,
20
+ EventType,
21
+ LLMClient,
22
+ LLMResponse,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Type alias for tool executor function
28
+ ToolExecutor = Callable[[str, dict], Awaitable[Any]]
29
+
30
+
31
+ @dataclass
32
+ class AgenticLoopResult:
33
+ """Result from running the agentic loop."""
34
+
35
+ final_content: str
36
+ """The final text response from the LLM."""
37
+
38
+ messages: list[dict]
39
+ """All messages including tool calls and results."""
40
+
41
+ iterations: int
42
+ """Number of iterations the loop ran."""
43
+
44
+ usage: dict
45
+ """Token usage from the final LLM call."""
46
+
47
+
48
+ async def run_agentic_loop(
49
+ llm: LLMClient,
50
+ messages: list[dict],
51
+ tools: Optional[list[dict]],
52
+ execute_tool: ToolExecutor,
53
+ ctx: RunContext,
54
+ *,
55
+ model: Optional[str] = None,
56
+ max_iterations: int = 15,
57
+ emit_events: bool = True,
58
+ **llm_kwargs,
59
+ ) -> AgenticLoopResult:
60
+ """
61
+ Run the standard agentic tool-calling loop.
62
+
63
+ This handles the common pattern of:
64
+ 1. Call LLM with available tools
65
+ 2. If LLM returns tool calls, execute them
66
+ 3. Add tool results to messages and loop back to step 1
67
+ 4. If LLM returns a text response (no tool calls), return it
68
+
69
+ Args:
70
+ llm: The LLM client to use for generation
71
+ messages: Initial messages (should include system prompt)
72
+ tools: List of tool schemas in OpenAI format, or None for no tools
73
+ execute_tool: Async function that executes a tool: (name, args) -> result
74
+ ctx: Run context for emitting events
75
+ model: Model to use (passed to LLM client)
76
+ max_iterations: Maximum loop iterations to prevent infinite loops
77
+ emit_events: Whether to emit TOOL_CALL and TOOL_RESULT events
78
+ **llm_kwargs: Additional kwargs passed to llm.generate()
79
+
80
+ Returns:
81
+ AgenticLoopResult with final content, messages, and metadata
82
+
83
+ Example:
84
+ async def my_tool_executor(name: str, args: dict) -> Any:
85
+ if name == "get_weather":
86
+ return {"temp": 72, "conditions": "sunny"}
87
+ raise ValueError(f"Unknown tool: {name}")
88
+
89
+ result = await run_agentic_loop(
90
+ llm=my_llm_client,
91
+ messages=[{"role": "system", "content": "You are helpful."}],
92
+ tools=[{"type": "function", "function": {...}}],
93
+ execute_tool=my_tool_executor,
94
+ ctx=ctx,
95
+ model="gpt-4o",
96
+ )
97
+ """
98
+ iteration = 0
99
+ final_content = ""
100
+ last_response: Optional[LLMResponse] = None
101
+ consecutive_errors = 0
102
+ max_consecutive_errors = 3 # Bail out if tool keeps failing
103
+
104
+ while iteration < max_iterations:
105
+ iteration += 1
106
+ print(f"[agentic-loop] Iteration {iteration}/{max_iterations}, messages={len(messages)}", flush=True)
107
+ logger.debug(f"Agentic loop iteration {iteration}/{max_iterations}")
108
+
109
+ # Call LLM
110
+ if tools:
111
+ response = await llm.generate(
112
+ messages,
113
+ model=model,
114
+ tools=tools,
115
+ **llm_kwargs,
116
+ )
117
+ else:
118
+ response = await llm.generate(
119
+ messages,
120
+ model=model,
121
+ **llm_kwargs,
122
+ )
123
+
124
+ last_response = response
125
+
126
+ # Check for tool calls
127
+ if response.tool_calls:
128
+ # Add assistant message with tool calls to conversation
129
+ messages.append(response.message)
130
+
131
+ # Execute each tool call
132
+ for tool_call in response.tool_calls:
133
+ # Debug: log raw tool call to help diagnose empty args issue
134
+ print(f"[agentic-loop] Raw tool_call type={type(tool_call).__name__}", flush=True)
135
+ if hasattr(tool_call, '_data'):
136
+ print(f"[agentic-loop] tool_call._data={tool_call._data}", flush=True)
137
+
138
+ # Handle both ToolCall objects (with .id, .name, .arguments) and dicts
139
+ if hasattr(tool_call, 'id') and not isinstance(tool_call, dict):
140
+ # ToolCall object
141
+ tool_call_id = tool_call.id
142
+ tool_name = tool_call.name
143
+ tool_args = tool_call.arguments
144
+ print(f"[agentic-loop] Parsed: name={tool_name}, args={tool_args}", flush=True)
145
+ else:
146
+ # Dict format
147
+ tool_call_id = tool_call.get("id")
148
+ tool_name = tool_call.get("function", {}).get("name")
149
+ tool_args_str = tool_call.get("function", {}).get("arguments", "{}")
150
+ logger.debug(f"Dict tool_call: id={tool_call_id}, name={tool_name}, args_str={tool_args_str}")
151
+ # Parse arguments (handle both string and dict)
152
+ if isinstance(tool_args_str, dict):
153
+ tool_args = tool_args_str
154
+ else:
155
+ try:
156
+ tool_args = json.loads(tool_args_str)
157
+ except json.JSONDecodeError:
158
+ logger.warning(f"Failed to parse tool args: {tool_args_str}")
159
+ tool_args = {}
160
+
161
+ # Emit tool call event
162
+ if emit_events:
163
+ await ctx.emit(EventType.TOOL_CALL, {
164
+ "id": tool_call_id,
165
+ "name": tool_name,
166
+ "arguments": tool_args,
167
+ })
168
+
169
+ # Execute the tool
170
+ try:
171
+ result = await execute_tool(tool_name, tool_args)
172
+ # Reset error counter on success
173
+ if not isinstance(result, dict) or "error" not in result:
174
+ consecutive_errors = 0
175
+ else:
176
+ consecutive_errors += 1
177
+ except Exception as e:
178
+ logger.exception(f"Error executing tool {tool_name}")
179
+ result = {"error": str(e)}
180
+ consecutive_errors += 1
181
+
182
+ # Emit tool result event
183
+ if emit_events:
184
+ await ctx.emit(EventType.TOOL_RESULT, {
185
+ "tool_call_id": tool_call_id,
186
+ "name": tool_name,
187
+ "result": result,
188
+ })
189
+
190
+ # Add tool result to messages
191
+ result_str = json.dumps(result) if not isinstance(result, str) else result
192
+ messages.append({
193
+ "role": "tool",
194
+ "tool_call_id": tool_call_id,
195
+ "content": result_str,
196
+ })
197
+
198
+ # Check for too many consecutive errors
199
+ if consecutive_errors >= max_consecutive_errors:
200
+ error_msg = result.get('error', 'Unknown error') if isinstance(result, dict) else str(result)
201
+ logger.warning(f"Aborting agentic loop after {consecutive_errors} consecutive tool errors: {error_msg}")
202
+
203
+ # Emit error event for run history
204
+ if emit_events:
205
+ await ctx.emit(EventType.ERROR, {
206
+ "error": f"Tool loop aborted after {consecutive_errors} consecutive errors",
207
+ "last_error": error_msg,
208
+ "tool_name": tool_name,
209
+ "iterations": iteration,
210
+ })
211
+
212
+ # Add error to messages for conversation history
213
+ final_content = f"I encountered repeated errors while trying to complete this task. The last error was: {error_msg}"
214
+ messages.append({
215
+ "role": "assistant",
216
+ "content": final_content,
217
+ })
218
+
219
+ if emit_events:
220
+ await ctx.emit(EventType.ASSISTANT_MESSAGE, {
221
+ "content": final_content,
222
+ "role": "assistant",
223
+ })
224
+
225
+ return AgenticLoopResult(
226
+ final_content=final_content,
227
+ messages=messages,
228
+ iterations=iteration,
229
+ usage=last_response.usage if last_response else {},
230
+ )
231
+
232
+ # Continue the loop to get next response
233
+ continue
234
+
235
+ # No tool calls - we have the final response
236
+ final_content = response.message.get("content", "")
237
+ messages.append(response.message)
238
+
239
+ # Emit assistant message event for the final response
240
+ if emit_events and final_content:
241
+ await ctx.emit(EventType.ASSISTANT_MESSAGE, {
242
+ "content": final_content,
243
+ "role": "assistant",
244
+ })
245
+
246
+ break
247
+
248
+ return AgenticLoopResult(
249
+ final_content=final_content,
250
+ messages=messages,
251
+ iterations=iteration,
252
+ usage=last_response.usage if last_response else {},
253
+ )
254
+
@@ -59,7 +59,35 @@ class RuntimeConfig:
59
59
  max_retries: int = 3
60
60
  retry_backoff_base: int = 2
61
61
  retry_backoff_max: int = 300
62
-
62
+
63
+ # Conversation history settings
64
+ # When True, agents automatically receive message history from previous runs
65
+ # in the same conversation. This enables multi-turn conversations by default.
66
+ include_conversation_history: bool = True
67
+
68
+ # Maximum number of history messages to include (None = no limit)
69
+ # Useful for limiting context window usage with long conversations
70
+ max_history_messages: Optional[int] = None
71
+
72
+ # Whether to automatically persist messages after each run
73
+ # When True, final_messages are saved to the conversation store
74
+ auto_persist_messages: bool = True
75
+
76
+ # Vector store settings
77
+ vector_store_backend: str = "none" # none, sqlite_vec, pgvector, vertex
78
+ vector_store_path: Optional[str] = None # For sqlite_vec file path
79
+
80
+ # Embedding settings
81
+ embedding_provider: str = "openai" # openai, vertex
82
+ embedding_model: str = "text-embedding-3-small"
83
+
84
+ # Vertex AI settings (for vertex vector store and embeddings)
85
+ vertex_project_id: Optional[str] = None
86
+ vertex_location: str = "us-central1"
87
+ vertex_index_endpoint_id: Optional[str] = None
88
+ vertex_deployed_index_id: Optional[str] = None
89
+ vertex_index_id: Optional[str] = None
90
+
63
91
  def get_openai_api_key(self) -> Optional[str]:
64
92
  """Get OpenAI API key from config or environment."""
65
93
  return self.openai_api_key or os.environ.get("OPENAI_API_KEY")
@@ -150,8 +178,19 @@ def _apply_env_vars(config: RuntimeConfig) -> None:
150
178
  "AGENT_RUNTIME_LANGFUSE_PUBLIC_KEY": "langfuse_public_key",
151
179
  "AGENT_RUNTIME_LANGFUSE_SECRET_KEY": "langfuse_secret_key",
152
180
  "AGENT_RUNTIME_LANGFUSE_HOST": "langfuse_host",
181
+ # Vector store settings
182
+ "AGENT_RUNTIME_VECTOR_STORE_BACKEND": "vector_store_backend",
183
+ "AGENT_RUNTIME_VECTOR_STORE_PATH": "vector_store_path",
184
+ "AGENT_RUNTIME_EMBEDDING_PROVIDER": "embedding_provider",
185
+ "AGENT_RUNTIME_EMBEDDING_MODEL": "embedding_model",
186
+ # Vertex AI settings
187
+ "AGENT_RUNTIME_VERTEX_PROJECT_ID": "vertex_project_id",
188
+ "AGENT_RUNTIME_VERTEX_LOCATION": "vertex_location",
189
+ "AGENT_RUNTIME_VERTEX_INDEX_ENDPOINT_ID": "vertex_index_endpoint_id",
190
+ "AGENT_RUNTIME_VERTEX_DEPLOYED_INDEX_ID": "vertex_deployed_index_id",
191
+ "AGENT_RUNTIME_VERTEX_INDEX_ID": "vertex_index_id",
153
192
  }
154
-
193
+
155
194
  int_fields = {
156
195
  "AGENT_RUNTIME_RUN_TIMEOUT_SECONDS": "run_timeout_seconds",
157
196
  "AGENT_RUNTIME_HEARTBEAT_INTERVAL_SECONDS": "heartbeat_interval_seconds",
@@ -159,14 +198,25 @@ def _apply_env_vars(config: RuntimeConfig) -> None:
159
198
  "AGENT_RUNTIME_MAX_RETRIES": "max_retries",
160
199
  "AGENT_RUNTIME_RETRY_BACKOFF_BASE": "retry_backoff_base",
161
200
  "AGENT_RUNTIME_RETRY_BACKOFF_MAX": "retry_backoff_max",
201
+ "AGENT_RUNTIME_MAX_HISTORY_MESSAGES": "max_history_messages",
162
202
  }
163
-
203
+
204
+ bool_fields = {
205
+ "AGENT_RUNTIME_INCLUDE_CONVERSATION_HISTORY": "include_conversation_history",
206
+ "AGENT_RUNTIME_AUTO_PERSIST_MESSAGES": "auto_persist_messages",
207
+ }
208
+
164
209
  for env_var, attr in env_mapping.items():
165
210
  value = os.environ.get(env_var)
166
211
  if value is not None:
167
212
  setattr(config, attr, value)
168
-
213
+
169
214
  for env_var, attr in int_fields.items():
170
215
  value = os.environ.get(env_var)
171
216
  if value is not None:
172
217
  setattr(config, attr, int(value))
218
+
219
+ for env_var, attr in bool_fields.items():
220
+ value = os.environ.get(env_var)
221
+ if value is not None:
222
+ setattr(config, attr, value.lower() in ("true", "1", "yes", "on"))