aury-agent 0.0.4__py3-none-any.whl → 0.0.5__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,309 @@
1
+ """Context and message building helpers for ReactAgent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from ..core.logging import react_logger as logger
10
+ from ..context_providers import AgentContext
11
+ from ..llm import LLMMessage
12
+
13
+ if TYPE_CHECKING:
14
+ from ..core.context import InvocationContext
15
+ from ..core.types import PromptInput
16
+ from ..context_providers import ContextProvider
17
+ from ..middleware import MiddlewareChain
18
+ from ..core.types.tool import BaseTool
19
+
20
+
21
+ async def fetch_agent_context(
22
+ ctx: "InvocationContext",
23
+ input: "PromptInput",
24
+ context_providers: list["ContextProvider"],
25
+ direct_tools: list["BaseTool"],
26
+ delegate_tool_class: type | None,
27
+ middleware_chain: "MiddlewareChain | None",
28
+ ) -> AgentContext:
29
+ """Fetch context from all providers and merge with direct tools.
30
+
31
+ Process:
32
+ 1. Fetch from all providers and merge
33
+ 2. Add direct tools (from create())
34
+ 3. If providers returned subagents, create DelegateTool
35
+
36
+ Args:
37
+ ctx: InvocationContext
38
+ input: User prompt input
39
+ context_providers: List of context providers
40
+ direct_tools: Direct tools from create()
41
+ delegate_tool_class: Custom DelegateTool class
42
+ middleware_chain: Middleware chain for DelegateTool
43
+
44
+ Returns:
45
+ Merged AgentContext with all tools
46
+ """
47
+ from ..tool.builtin import DelegateTool
48
+ from ..backends.subagent import ListSubAgentBackend
49
+
50
+ # Set input on context for providers to access
51
+ ctx.input = input
52
+
53
+ # Fetch from all context_providers
54
+ outputs: list[AgentContext] = []
55
+ for provider in context_providers:
56
+ try:
57
+ output = await provider.fetch(ctx)
58
+ outputs.append(output)
59
+ except Exception as e:
60
+ logger.warning(f"Provider {provider.name} fetch failed: {e}")
61
+
62
+ # Merge all provider outputs
63
+ merged = AgentContext.merge(outputs)
64
+
65
+ # Add direct tools (from create())
66
+ all_tools = list(direct_tools) # Copy direct tools
67
+ seen_names = {t.name for t in all_tools}
68
+
69
+ # Add tools from providers (deduplicate)
70
+ for tool in merged.tools:
71
+ if tool.name not in seen_names:
72
+ seen_names.add(tool.name)
73
+ all_tools.append(tool)
74
+
75
+ # If providers returned subagents, create DelegateTool
76
+ if merged.subagents:
77
+ # Check if we already have a delegate tool
78
+ has_delegate = any(t.name == "delegate" for t in all_tools)
79
+ if not has_delegate:
80
+ backend = ListSubAgentBackend(merged.subagents)
81
+ tool_cls = delegate_tool_class or DelegateTool
82
+ delegate_tool = tool_cls(backend, middleware=middleware_chain)
83
+ all_tools.append(delegate_tool)
84
+
85
+ # Return merged context with combined tools
86
+ return AgentContext(
87
+ system_content=merged.system_content,
88
+ user_content=merged.user_content,
89
+ tools=all_tools,
90
+ messages=merged.messages,
91
+ subagents=merged.subagents,
92
+ skills=merged.skills,
93
+ )
94
+
95
+
96
+ def _parse_content(content):
97
+ """Parse content, handling stringified JSON."""
98
+ if isinstance(content, str):
99
+ # Try to parse if it looks like JSON array
100
+ if content.startswith("["):
101
+ try:
102
+ import json
103
+ return json.loads(content)
104
+ except json.JSONDecodeError:
105
+ pass
106
+ return content
107
+
108
+
109
+ def fix_incomplete_tool_calls(messages: list[dict]) -> list[dict]:
110
+ """Fix incomplete tool_use/tool_result pairs in history.
111
+
112
+ If an assistant message has tool_use blocks without corresponding
113
+ tool_result messages, add placeholder tool_result messages.
114
+
115
+ This handles cases where execution was interrupted between
116
+ saving assistant message and saving tool results.
117
+
118
+ Supports both formats:
119
+ - OpenAI format: assistant message with tool_calls field
120
+ - Claude format: assistant message with content containing tool_use blocks
121
+
122
+ Args:
123
+ messages: List of message dicts from history
124
+
125
+ Returns:
126
+ Fixed message list with placeholder tool_results added
127
+ """
128
+ if not messages:
129
+ return messages
130
+
131
+ result = []
132
+ i = 0
133
+
134
+ while i < len(messages):
135
+ msg = messages[i]
136
+ result.append(msg)
137
+
138
+ # Check if this is an assistant message with tool_use
139
+ if msg.get("role") == "assistant":
140
+ tool_use_ids = []
141
+
142
+ # Format 1: OpenAI format - tool_calls as separate field
143
+ tool_calls = msg.get("tool_calls")
144
+ if tool_calls and isinstance(tool_calls, list):
145
+ for tc in tool_calls:
146
+ if isinstance(tc, dict):
147
+ tool_id = tc.get("id")
148
+ if tool_id:
149
+ tool_use_ids.append(tool_id)
150
+
151
+ # Format 2: Claude format - tool_use in content
152
+ if not tool_use_ids:
153
+ content = _parse_content(msg.get("content"))
154
+ if isinstance(content, list):
155
+ for part in content:
156
+ if isinstance(part, dict) and part.get("type") == "tool_use":
157
+ tool_id = part.get("id")
158
+ if tool_id:
159
+ tool_use_ids.append(tool_id)
160
+
161
+ if tool_use_ids:
162
+ # Collect tool_result ids from following messages
163
+ tool_result_ids = set()
164
+ j = i + 1
165
+ while j < len(messages):
166
+ next_msg = messages[j]
167
+ if next_msg.get("role") == "tool":
168
+ # Check tool_call_id or content for tool_use_id
169
+ tcid = next_msg.get("tool_call_id")
170
+ if tcid:
171
+ tool_result_ids.add(tcid)
172
+ # Also check content if it's a list with tool_result
173
+ nc = _parse_content(next_msg.get("content"))
174
+ if isinstance(nc, list):
175
+ for part in nc:
176
+ if isinstance(part, dict) and part.get("type") == "tool_result":
177
+ tuid = part.get("tool_use_id")
178
+ if tuid:
179
+ tool_result_ids.add(tuid)
180
+ j += 1
181
+ elif next_msg.get("role") in ("user", "assistant"):
182
+ # Stop at next user/assistant message
183
+ break
184
+ else:
185
+ j += 1
186
+
187
+ # Add placeholder for missing tool_results
188
+ for tool_id in tool_use_ids:
189
+ if tool_id not in tool_result_ids:
190
+ logger.warning(
191
+ f"Found incomplete tool_use without tool_result: {tool_id}, adding placeholder"
192
+ )
193
+ result.append({
194
+ "role": "tool",
195
+ "content": "[执行被中断]",
196
+ "tool_call_id": tool_id,
197
+ })
198
+
199
+ i += 1
200
+
201
+ return result
202
+
203
+
204
+ async def build_messages(
205
+ input: "PromptInput",
206
+ agent_context: AgentContext,
207
+ system_prompt: str | None,
208
+ ) -> list[LLMMessage]:
209
+ """Build message history for LLM.
210
+
211
+ Uses AgentContext from providers for system content, messages, etc.
212
+
213
+ Args:
214
+ input: User prompt input
215
+ agent_context: Merged context from providers
216
+ system_prompt: System prompt from config (or default)
217
+
218
+ Returns:
219
+ List of LLMMessage for LLM call
220
+ """
221
+ messages = []
222
+
223
+ # System message: config.system_prompt + agent_context.system_content
224
+ final_system_prompt = system_prompt or default_system_prompt(agent_context.tools)
225
+
226
+ # Format system_prompt with dynamic variables
227
+ now = datetime.now()
228
+
229
+ # Build template variables: datetime + custom vars from input
230
+ template_vars = {
231
+ "current_date": now.strftime("%Y-%m-%d"),
232
+ "current_time": now.strftime("%H:%M:%S"),
233
+ "current_datetime": now.strftime("%Y-%m-%d %H:%M:%S"),
234
+ }
235
+
236
+ # Add custom variables from PromptInput (user_name, tenant, etc.)
237
+ if hasattr(input, 'vars') and input.vars:
238
+ template_vars.update(input.vars)
239
+
240
+ try:
241
+ final_system_prompt = final_system_prompt.format(**template_vars)
242
+ except KeyError as e:
243
+ # Log missing variable but continue
244
+ logger.debug(f"System prompt template variable not found: {e}")
245
+ pass
246
+
247
+ if agent_context.system_content:
248
+ final_system_prompt = final_system_prompt + "\n\n" + agent_context.system_content
249
+ messages.append(LLMMessage(role="system", content=final_system_prompt))
250
+
251
+ # Historical messages from AgentContext (provided by MessageContextProvider)
252
+ # Fix incomplete tool_use/tool_result pairs first
253
+ history_messages = fix_incomplete_tool_calls(agent_context.messages)
254
+ for i, msg in enumerate(history_messages):
255
+ raw_content = msg.get("content", "")
256
+ content = _parse_content(raw_content)
257
+ logger.info(
258
+ f"[build_messages] msg[{i}] role={msg.get('role')}, "
259
+ f"raw_content_type={type(raw_content).__name__}, "
260
+ f"raw_content_preview={str(raw_content)[:200]}, "
261
+ f"parsed_content_type={type(content).__name__}"
262
+ )
263
+ messages.append(LLMMessage(
264
+ role=msg.get("role", "user"),
265
+ content=content,
266
+ tool_call_id=msg.get("tool_call_id"),
267
+ ))
268
+
269
+ # User content prefix (from providers) + current user message
270
+ content = input.text
271
+ if agent_context.user_content:
272
+ content = agent_context.user_content + "\n\n" + content
273
+
274
+ if input.attachments:
275
+ # Build multimodal content
276
+ content_parts = [{"type": "text", "text": content}]
277
+ for attachment in input.attachments:
278
+ content_parts.append(attachment)
279
+ content = content_parts
280
+
281
+ messages.append(LLMMessage(role="user", content=content))
282
+
283
+ return messages
284
+
285
+
286
+ def default_system_prompt(tools: list["BaseTool"]) -> str:
287
+ """Generate default system prompt with tool descriptions.
288
+
289
+ Args:
290
+ tools: List of available tools
291
+
292
+ Returns:
293
+ Default system prompt string
294
+ """
295
+ tool_list = []
296
+ for tool in tools:
297
+ info = tool.get_info()
298
+ tool_list.append(f"- {info.name}: {info.description}")
299
+
300
+ tools_desc = "\n".join(tool_list) if tool_list else "No tools available."
301
+
302
+ return f"""You are a helpful AI assistant with access to tools.
303
+
304
+ Available tools:
305
+ {tools_desc}
306
+
307
+ When you need to use a tool, make a tool call. After receiving the tool result, continue reasoning or provide your final response.
308
+
309
+ Think step by step and use tools when necessary to complete the user's request."""
@@ -0,0 +1,301 @@
1
+ """Factory functions for ReactAgent creation and restoration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..core.base import AgentConfig
8
+ from ..core.context import InvocationContext
9
+ from ..core.types.session import Session, generate_id
10
+
11
+ if TYPE_CHECKING:
12
+ from ..llm import LLMProvider
13
+ from ..tool import ToolSet
14
+ from ..core.types.tool import BaseTool
15
+ from ..backends import Backends
16
+ from ..backends.snapshot import SnapshotBackend
17
+ from ..backends.subagent import AgentConfig as SubAgentConfig
18
+ from ..core.event_bus import Bus
19
+ from ..middleware import MiddlewareChain, Middleware
20
+ from ..memory import MemoryManager
21
+ from ..context_providers import ContextProvider
22
+
23
+
24
+ class SessionNotFoundError(Exception):
25
+ """Raised when session is not found in storage."""
26
+ pass
27
+
28
+
29
+ def create_react_agent(
30
+ llm: "LLMProvider",
31
+ tools: "ToolSet | list[BaseTool] | None" = None,
32
+ config: AgentConfig | None = None,
33
+ *,
34
+ backends: "Backends | None" = None,
35
+ session: "Session | None" = None,
36
+ bus: "Bus | None" = None,
37
+ middlewares: "list[Middleware] | None" = None,
38
+ subagents: "list[SubAgentConfig] | None" = None,
39
+ memory: "MemoryManager | None" = None,
40
+ snapshot: "SnapshotBackend | None" = None,
41
+ # ContextProvider system
42
+ context_providers: "list[ContextProvider] | None" = None,
43
+ enable_history: bool = True,
44
+ history_limit: int = 50,
45
+ # Tool customization
46
+ delegate_tool_class: "type[BaseTool] | None" = None,
47
+ ) -> "ReactAgent":
48
+ """Create ReactAgent with minimal boilerplate.
49
+
50
+ This is the recommended way to create a ReactAgent for simple use cases.
51
+ Session, Storage, and Bus are auto-created if not provided.
52
+
53
+ Args:
54
+ llm: LLM provider (required)
55
+ tools: Tool registry or list of tools (optional)
56
+ config: Agent configuration (optional)
57
+ backends: Backends container (recommended, auto-created if None)
58
+ session: Session object (auto-created if None)
59
+ bus: Event bus (auto-created if None)
60
+ middlewares: List of middlewares (auto-creates chain)
61
+ subagents: List of sub-agent configs (auto-creates SubAgentManager)
62
+ memory: Memory manager (optional)
63
+ snapshot: Snapshot backend (optional)
64
+ context_providers: Additional custom context providers (optional)
65
+ enable_history: Enable message history (default True)
66
+ history_limit: Max conversation turns to keep (default 50)
67
+ delegate_tool_class: Custom DelegateTool class (optional)
68
+
69
+ Returns:
70
+ Configured ReactAgent ready to run
71
+
72
+ Example:
73
+ # Minimal
74
+ agent = create_react_agent(llm=my_llm)
75
+
76
+ # With backends
77
+ agent = create_react_agent(
78
+ llm=my_llm,
79
+ backends=Backends.create_default(),
80
+ )
81
+
82
+ # With tools and middlewares
83
+ agent = create_react_agent(
84
+ llm=my_llm,
85
+ tools=[tool1, tool2],
86
+ middlewares=[MessageContainerMiddleware()],
87
+ )
88
+
89
+ # With sub-agents
90
+ agent = create_react_agent(
91
+ llm=my_llm,
92
+ subagents=[
93
+ AgentConfig(key="researcher", agent=researcher_agent),
94
+ ],
95
+ )
96
+
97
+ # With custom context providers
98
+ agent = create_react_agent(
99
+ llm=my_llm,
100
+ tools=[tool1],
101
+ context_providers=[MyRAGProvider(), MyProjectProvider()],
102
+ )
103
+ """
104
+ from .agent import ReactAgent
105
+ from ..core.event_bus import EventBus
106
+ from ..backends import Backends
107
+ from ..backends.subagent import ListSubAgentBackend
108
+ from ..tool import ToolSet
109
+ from ..tool.builtin import DelegateTool
110
+ from ..middleware import MiddlewareChain, MessageBackendMiddleware
111
+ from ..context_providers import MessageContextProvider
112
+
113
+ # Auto-create backends if not provided
114
+ if backends is None:
115
+ backends = Backends.create_default()
116
+
117
+ # Auto-create missing components
118
+ if session is None:
119
+ session = Session(id=generate_id("sess"))
120
+ if bus is None:
121
+ bus = EventBus()
122
+
123
+ # Create middleware chain (add MessageBackendMiddleware if history enabled)
124
+ middleware_chain: MiddlewareChain | None = None
125
+ if middlewares or enable_history:
126
+ middleware_chain = MiddlewareChain()
127
+ # Add message persistence middleware first (uses backends.message)
128
+ if enable_history and backends.message is not None:
129
+ middleware_chain.use(MessageBackendMiddleware(max_history=history_limit))
130
+ # Add user middlewares
131
+ if middlewares:
132
+ for mw in middlewares:
133
+ middleware_chain.use(mw)
134
+
135
+ # === Build tools list (direct, no provider) ===
136
+ tool_list: list["BaseTool"] = []
137
+ if tools is not None:
138
+ if isinstance(tools, ToolSet):
139
+ tool_list = list(tools.all())
140
+ else:
141
+ tool_list = list(tools)
142
+
143
+ # Handle subagents - create DelegateTool directly
144
+ if subagents:
145
+ backend = ListSubAgentBackend(subagents)
146
+ tool_cls = delegate_tool_class or DelegateTool
147
+ delegate_tool = tool_cls(backend, middleware=middleware_chain)
148
+ tool_list.append(delegate_tool)
149
+
150
+ # === Build providers ===
151
+ default_providers: list["ContextProvider"] = []
152
+
153
+ # MessageContextProvider - for fetching history (uses backends.message)
154
+ if enable_history:
155
+ message_provider = MessageContextProvider(max_messages=history_limit * 2)
156
+ default_providers.append(message_provider)
157
+
158
+ # Combine default + custom context_providers
159
+ all_providers = default_providers + (context_providers or [])
160
+
161
+ # Build context
162
+ ctx = InvocationContext(
163
+ session=session,
164
+ invocation_id=generate_id("inv"),
165
+ agent_id=config.name if config else "react_agent",
166
+ backends=backends,
167
+ bus=bus,
168
+ llm=llm,
169
+ middleware=middleware_chain,
170
+ memory=memory,
171
+ snapshot=snapshot,
172
+ )
173
+
174
+ agent = ReactAgent(ctx, config)
175
+ agent._tools = tool_list # Direct tools (not from context_provider)
176
+ agent._context_providers = all_providers
177
+ agent._delegate_tool_class = delegate_tool_class or DelegateTool
178
+ agent._middleware_chain = middleware_chain
179
+ return agent
180
+
181
+
182
+ async def restore_react_agent(
183
+ session_id: str,
184
+ llm: "LLMProvider",
185
+ *,
186
+ backends: "Backends | None" = None,
187
+ tools: "ToolSet | list[BaseTool] | None" = None,
188
+ config: AgentConfig | None = None,
189
+ bus: "Bus | None" = None,
190
+ middleware: "MiddlewareChain | None" = None,
191
+ memory: "MemoryManager | None" = None,
192
+ snapshot: "SnapshotBackend | None" = None,
193
+ ) -> "ReactAgent":
194
+ """Restore agent from persisted state.
195
+
196
+ Use this to resume an agent after:
197
+ - Page refresh
198
+ - Process restart
199
+ - Cross-process recovery
200
+
201
+ Args:
202
+ session_id: Session ID to restore
203
+ llm: LLM provider
204
+ backends: Backends container (recommended, auto-created if None)
205
+ tools: Tool registry or list of tools
206
+ config: Agent configuration
207
+ bus: Event bus (auto-created if None)
208
+ middleware: Middleware chain
209
+ memory: Memory manager
210
+ snapshot: Snapshot backend
211
+
212
+ Returns:
213
+ Restored ReactAgent ready to continue
214
+
215
+ Raises:
216
+ SessionNotFoundError: If session not found
217
+
218
+ Example:
219
+ agent = await restore_react_agent(
220
+ session_id="sess_xxx",
221
+ backends=my_backends,
222
+ llm=my_llm,
223
+ )
224
+
225
+ # Check if waiting for HITL response
226
+ if agent.is_suspended:
227
+ print(f"Waiting for: {agent.pending_request}")
228
+ else:
229
+ # Continue conversation
230
+ await agent.run("Continue...")
231
+ """
232
+ from .agent import ReactAgent
233
+ from ..core.event_bus import Bus
234
+ from ..core.types.session import Session, Invocation, InvocationState, generate_id
235
+ from ..core.state import State
236
+ from ..tool import ToolSet
237
+ from ..backends import Backends
238
+
239
+ # Auto-create backends if not provided
240
+ if backends is None:
241
+ backends = Backends.create_default()
242
+
243
+ # Validate storage backend is available
244
+ if backends.state is None:
245
+ raise ValueError("Cannot restore: no storage backend available (backends.state is None)")
246
+
247
+ storage = backends.state
248
+
249
+ # 1. Load session
250
+ session_data = await storage.get("sessions", session_id)
251
+ if not session_data:
252
+ raise SessionNotFoundError(f"Session not found: {session_id}")
253
+ session = Session.from_dict(session_data)
254
+
255
+ # 2. Load current invocation
256
+ invocation: Invocation | None = None
257
+ if session_data.get("current_invocation_id"):
258
+ inv_data = await storage.get("invocations", session_data["current_invocation_id"])
259
+ if inv_data:
260
+ invocation = Invocation.from_dict(inv_data)
261
+
262
+ # 3. Load state
263
+ state = State(storage, session_id)
264
+ await state.restore()
265
+
266
+ # 4. Handle tools
267
+ tool_set: ToolSet | None = None
268
+ if tools is not None:
269
+ if isinstance(tools, ToolSet):
270
+ tool_set = tools
271
+ else:
272
+ tool_set = ToolSet()
273
+ for tool in tools:
274
+ tool_set.add(tool)
275
+ else:
276
+ tool_set = ToolSet()
277
+
278
+ # 5. Create bus if needed
279
+ if bus is None:
280
+ bus = Bus()
281
+
282
+ # 6. Build context
283
+ ctx = InvocationContext(
284
+ session=session,
285
+ invocation_id=invocation.id if invocation else generate_id("inv"),
286
+ agent_id=config.name if config else "react_agent",
287
+ backends=backends,
288
+ bus=bus,
289
+ llm=llm,
290
+ tools=tool_set,
291
+ middleware=middleware,
292
+ memory=memory,
293
+ snapshot=snapshot,
294
+ )
295
+
296
+ # 7. Create agent
297
+ agent = ReactAgent(ctx, config)
298
+ agent._restored_invocation = invocation
299
+ agent._state = state
300
+
301
+ return agent