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.
- aury/agents/context_providers/message.py +8 -5
- aury/agents/core/base.py +11 -0
- aury/agents/core/factory.py +8 -0
- aury/agents/core/parallel.py +26 -4
- aury/agents/core/state.py +25 -0
- aury/agents/core/types/tool.py +1 -0
- aury/agents/hitl/ask_user.py +44 -0
- aury/agents/llm/adapter.py +55 -26
- aury/agents/llm/openai.py +5 -1
- aury/agents/memory/manager.py +33 -2
- aury/agents/messages/store.py +27 -1
- aury/agents/middleware/base.py +57 -0
- aury/agents/middleware/chain.py +81 -18
- aury/agents/react/agent.py +161 -1484
- aury/agents/react/context.py +309 -0
- aury/agents/react/factory.py +301 -0
- aury/agents/react/pause.py +241 -0
- aury/agents/react/persistence.py +182 -0
- aury/agents/react/step.py +680 -0
- aury/agents/react/tools.py +318 -0
- aury/agents/tool/builtin/bash.py +11 -0
- aury/agents/tool/builtin/delegate.py +38 -3
- aury/agents/tool/builtin/edit.py +16 -0
- aury/agents/tool/builtin/plan.py +19 -0
- aury/agents/tool/builtin/read.py +13 -0
- aury/agents/tool/builtin/thinking.py +10 -4
- aury/agents/tool/builtin/yield_result.py +9 -6
- aury/agents/tool/set.py +23 -0
- aury/agents/workflow/adapter.py +22 -3
- aury/agents/workflow/executor.py +51 -7
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/METADATA +1 -1
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/RECORD +34 -28
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/WHEEL +0 -0
- {aury_agent-0.0.4.dist-info → aury_agent-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -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
|