xpander-sdk 2.0.144__py3-none-any.whl → 2.0.192__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.
- xpander_sdk/__init__.py +6 -0
- xpander_sdk/consts/api_routes.py +9 -0
- xpander_sdk/models/activity.py +65 -0
- xpander_sdk/models/compactization.py +112 -0
- xpander_sdk/models/deep_planning.py +18 -0
- xpander_sdk/models/events.py +6 -0
- xpander_sdk/models/frameworks.py +2 -2
- xpander_sdk/models/generic.py +27 -0
- xpander_sdk/models/notifications.py +98 -0
- xpander_sdk/models/orchestrations.py +271 -0
- xpander_sdk/modules/agents/models/agent.py +11 -5
- xpander_sdk/modules/agents/sub_modules/agent.py +25 -10
- xpander_sdk/modules/backend/__init__.py +8 -0
- xpander_sdk/modules/backend/backend_module.py +47 -2
- xpander_sdk/modules/backend/decorators/__init__.py +7 -0
- xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
- xpander_sdk/modules/backend/events_registry.py +172 -0
- xpander_sdk/modules/backend/frameworks/agno.py +377 -15
- xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
- xpander_sdk/modules/backend/utils/mcp_oauth.py +37 -25
- xpander_sdk/modules/events/decorators/__init__.py +3 -0
- xpander_sdk/modules/events/decorators/on_tool.py +384 -0
- xpander_sdk/modules/events/events_module.py +28 -1
- xpander_sdk/modules/tasks/models/task.py +3 -14
- xpander_sdk/modules/tasks/sub_modules/task.py +276 -84
- xpander_sdk/modules/tools_repository/models/mcp.py +1 -0
- xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
- xpander_sdk/modules/tools_repository/tools_repository_module.py +6 -2
- xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
- xpander_sdk/utils/agents/__init__.py +0 -0
- xpander_sdk/utils/agents/compactization_agent.py +257 -0
- xpander_sdk/utils/generic.py +5 -0
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +224 -14
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +37 -24
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
- {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/top_level.txt +0 -0
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import json
|
|
2
3
|
import shlex
|
|
3
4
|
from os import getenv, environ
|
|
4
5
|
from typing import Any, Callable, Dict, List, Optional
|
|
5
6
|
|
|
6
7
|
from loguru import logger
|
|
7
|
-
|
|
8
|
+
from toon import encode as toon_encode
|
|
8
9
|
from xpander_sdk import Configuration
|
|
9
10
|
from xpander_sdk.models.shared import OutputFormat, ThinkMode
|
|
10
11
|
from xpander_sdk.modules.agents.agents_module import Agents
|
|
11
|
-
from xpander_sdk.modules.agents.models.agent import AgentGraphItemType
|
|
12
|
+
from xpander_sdk.modules.agents.models.agent import AgentGraphItemType, LLMReasoningEffort
|
|
12
13
|
from xpander_sdk.modules.agents.sub_modules.agent import Agent
|
|
13
14
|
from xpander_sdk.modules.backend.utils.mcp_oauth import authenticate_mcp_server
|
|
14
15
|
from xpander_sdk.modules.tasks.sub_modules.task import Task
|
|
15
16
|
from xpander_sdk.modules.tools_repository.models.mcp import (
|
|
17
|
+
MCPOAuthGetTokenGenericResponse,
|
|
16
18
|
MCPOAuthGetTokenResponse,
|
|
17
19
|
MCPOAuthResponseType,
|
|
18
20
|
MCPServerAuthType,
|
|
19
21
|
MCPServerTransport,
|
|
20
22
|
MCPServerType,
|
|
21
23
|
)
|
|
24
|
+
from xpander_sdk.modules.tools_repository.models.tool_invocation_result import ToolInvocationResult
|
|
22
25
|
from xpander_sdk.modules.tools_repository.sub_modules.tool import Tool
|
|
23
26
|
from xpander_sdk.modules.tools_repository.utils.schemas import build_model_from_schema
|
|
24
27
|
from agno.agent import Agent as AgnoAgent
|
|
@@ -34,13 +37,14 @@ async def build_agent_args(
|
|
|
34
37
|
override: Optional[Dict[str, Any]] = None,
|
|
35
38
|
tools: Optional[List[Callable]] = None,
|
|
36
39
|
is_async: Optional[bool] = True,
|
|
40
|
+
auth_events_callback: Optional[Callable] = None,
|
|
37
41
|
) -> Dict[str, Any]:
|
|
38
42
|
model = _load_llm_model(agent=xpander_agent, override=override)
|
|
39
43
|
args: Dict[str, Any] = {
|
|
40
44
|
"id": xpander_agent.id,
|
|
41
45
|
"store_events": True
|
|
42
46
|
}
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
_configure_output(args=args, agent=xpander_agent, task=task)
|
|
45
49
|
_configure_session_storage(args=args, agent=xpander_agent, task=task)
|
|
46
50
|
_configure_agentic_memory(args=args, agent=xpander_agent, task=task)
|
|
@@ -53,7 +57,7 @@ async def build_agent_args(
|
|
|
53
57
|
# Configure pre-hooks (guardrails, etc.)
|
|
54
58
|
_configure_pre_hooks(args=args, agent=xpander_agent, model=model)
|
|
55
59
|
|
|
56
|
-
args["tools"] = await _resolve_agent_tools(agent=xpander_agent, task=task)
|
|
60
|
+
args["tools"] = await _resolve_agent_tools(agent=xpander_agent, task=task, auth_events_callback=auth_events_callback)
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
if tools and len(tools) != 0:
|
|
@@ -100,7 +104,7 @@ async def build_agent_args(
|
|
|
100
104
|
# convert to members
|
|
101
105
|
members = await asyncio.gather(
|
|
102
106
|
*[
|
|
103
|
-
build_agent_args(xpander_agent=sub_agent, override=override, task=task)
|
|
107
|
+
build_agent_args(xpander_agent=sub_agent, override=override, task=task, is_async=is_async, auth_events_callback=auth_events_callback)
|
|
104
108
|
for sub_agent in sub_agents
|
|
105
109
|
]
|
|
106
110
|
)
|
|
@@ -108,7 +112,7 @@ async def build_agent_args(
|
|
|
108
112
|
args.update(
|
|
109
113
|
{
|
|
110
114
|
"members": [
|
|
111
|
-
AgnoAgent(**member) if "
|
|
115
|
+
AgnoAgent(**member) if "members" not in member else AgnoTeam(**member)
|
|
112
116
|
for member in members
|
|
113
117
|
],
|
|
114
118
|
"add_member_tools_to_context": True,
|
|
@@ -199,6 +203,19 @@ async def build_agent_args(
|
|
|
199
203
|
except Exception:
|
|
200
204
|
pass
|
|
201
205
|
|
|
206
|
+
# try toon optimization if compression enabled
|
|
207
|
+
if xpander_agent and xpander_agent and xpander_agent.agno_settings and xpander_agent.agno_settings.tool_calls_compression and xpander_agent.agno_settings.tool_calls_compression.enabled and not function_name.startswith("xp"):
|
|
208
|
+
try:
|
|
209
|
+
if isinstance(result, ToolInvocationResult):
|
|
210
|
+
json_result = json.loads(result.result) if isinstance(result.result, str) else result.result
|
|
211
|
+
result.result = toon_encode(json_result)
|
|
212
|
+
elif hasattr(result, "content"):
|
|
213
|
+
json_result = json.loads(result.content)
|
|
214
|
+
result.content = toon_encode(json_result)
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
202
219
|
# Return the result
|
|
203
220
|
return result
|
|
204
221
|
|
|
@@ -213,10 +230,349 @@ async def build_agent_args(
|
|
|
213
230
|
if args["model"] and args["model"].id and args["model"].id.startswith("gpt-5"):
|
|
214
231
|
del args["model"].temperature
|
|
215
232
|
|
|
233
|
+
# configure deep planning guidance
|
|
234
|
+
_configure_deep_planning_guidance(args=args, agent=xpander_agent, task=task)
|
|
216
235
|
return args
|
|
217
236
|
|
|
237
|
+
def _configure_deep_planning_guidance(args: Dict[str, Any], agent: Agent, task: Optional[Task]) -> None:
|
|
238
|
+
if args and agent and task and agent.deep_planning and task.deep_planning.enabled == True:
|
|
239
|
+
task.reload() # reload the task - get latest version
|
|
240
|
+
# add instructions guidance
|
|
241
|
+
if not "instructions" in args:
|
|
242
|
+
args["instructions"] = ""
|
|
243
|
+
|
|
244
|
+
args["instructions"] += """\n
|
|
245
|
+
<important_planning_instructions>
|
|
246
|
+
## **Deep Planning Tools - Essential for Multi-Step Tasks**
|
|
247
|
+
|
|
248
|
+
**ABSOLUTE RULE - READ THIS FIRST**:
|
|
249
|
+
|
|
250
|
+
IF YOU ARE ABOUT TO WRITE A QUESTION TO THE USER, STOP AND CHECK:
|
|
251
|
+
- Did I call xpstart_execution_plan?
|
|
252
|
+
- YES → Use xpask_for_information tool (DO NOT write the question in your response)
|
|
253
|
+
- NO → You can ask directly
|
|
254
|
+
|
|
255
|
+
SIMPLE RULE:
|
|
256
|
+
- BEFORE calling xpstart_execution_plan: You CAN ask questions directly in your response
|
|
257
|
+
- AFTER calling xpstart_execution_plan: You are FORBIDDEN from writing questions - use xpask_for_information tool
|
|
258
|
+
|
|
259
|
+
Once execution has started, writing things like "Before I proceed, I need to know..." or "I need clarification on..."
|
|
260
|
+
or "Please choose one of the following..." in your response text is STRICTLY PROHIBITED and will cause execution failures.
|
|
261
|
+
|
|
262
|
+
MANDATORY CHECK BEFORE ASKING: Have I started the plan? YES = use tool | NO = can ask directly
|
|
263
|
+
|
|
264
|
+
When handling complex tasks with multiple steps, use these planning tools to track progress systematically.
|
|
265
|
+
|
|
266
|
+
### **Core Workflow**
|
|
267
|
+
1. **CREATE** plan at the start (`xpcreate_agent_plan`)
|
|
268
|
+
2. **START** plan execution (`xpstart_execution_plan`) - MANDATORY to enable enforcement
|
|
269
|
+
3. **CHECK** plan before each action (`xpget_agent_plan`) - note the FULL UUID of the task you'll work on
|
|
270
|
+
4. **DO THE WORK** for one task
|
|
271
|
+
5. **COMPLETE** task IMMEDIATELY - call `xpcomplete_agent_plan_items` with the FULL UUID RIGHT AFTER finishing work (DO NOT DELAY!)
|
|
272
|
+
6. **ASK** user for info if needed (`xpask_for_information`) - MANDATORY if you need input or want to pause
|
|
273
|
+
7. Repeat steps 3-6 until all tasks are done
|
|
274
|
+
|
|
275
|
+
**UUID REQUIREMENT**: All task IDs are full UUIDs (e.g., '35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92'). You MUST use the exact complete UUID string from the plan when marking tasks complete.
|
|
276
|
+
|
|
277
|
+
**CRITICAL RULES - ABSOLUTE REQUIREMENTS**:
|
|
278
|
+
- Mark each task complete THE MOMENT you finish it - not later, not at the end, RIGHT AWAY!
|
|
279
|
+
- **CHECK BEFORE ASKING**: Did you call xpstart_execution_plan?
|
|
280
|
+
- If YES: Use xpask_for_information tool ONLY (never write questions in response)
|
|
281
|
+
- If NO: Can ask directly
|
|
282
|
+
- **AFTER plan starts: Writing questions in your response is FORBIDDEN** - violates execution protocol
|
|
283
|
+
- If you called xpstart_execution_plan and then write "I need clarification" or "Before I proceed" or "Please choose", you are VIOLATING THE PROTOCOL
|
|
284
|
+
- AFTER xpstart_execution_plan: The ONLY way to ask users questions is through `xpask_for_information` tool - ZERO EXCEPTIONS!
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### **Tool Reference**
|
|
289
|
+
|
|
290
|
+
#### **1. xpcreate_agent_plan** - Create Initial Plan
|
|
291
|
+
**When to use**: At the very start of any multi-step task, ONLY if no plan exists yet
|
|
292
|
+
**Note**: After creating a plan, you MUST call `xpstart_execution_plan` to begin enforcement
|
|
293
|
+
|
|
294
|
+
**How to use**:
|
|
295
|
+
- Pass an array of task objects (NOT strings)
|
|
296
|
+
- Each task must have a `title` field with short, explanatory description
|
|
297
|
+
- Example format:
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"body_params": {
|
|
301
|
+
"tasks": [
|
|
302
|
+
{"title": "Research API requirements"},
|
|
303
|
+
{"title": "Design data schema"},
|
|
304
|
+
{"title": "Implement endpoints"},
|
|
305
|
+
{"title": "Write tests"},
|
|
306
|
+
{"title": "Deploy to staging"}
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
**Important**:
|
|
312
|
+
- Include ALL steps needed from start to finish
|
|
313
|
+
- Each title should be clear and actionable (3-6 words)
|
|
314
|
+
- Do NOT pass plain strings like `["Task 1", "Task 2"]` - must be objects!
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
#### **2. xpget_agent_plan** - View Current Plan
|
|
319
|
+
**When to use**: Before deciding what to do next; to check progress
|
|
320
|
+
|
|
321
|
+
**Returns**:
|
|
322
|
+
- All tasks with their IDs, titles, and completion status
|
|
323
|
+
- Use this to know what's done and what remains
|
|
324
|
+
|
|
325
|
+
**No parameters needed** - just call the tool
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
#### **3. xpcomplete_agent_plan_items** - Mark Task(s) Complete
|
|
330
|
+
**When to use**: **IMMEDIATELY** after finishing one or more tasks (NOT before, NOT later, RIGHT NOW!)
|
|
331
|
+
|
|
332
|
+
**How to use**:
|
|
333
|
+
```json
|
|
334
|
+
// Single task
|
|
335
|
+
{
|
|
336
|
+
"body_params": {
|
|
337
|
+
"ids": ["35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92"]
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Multiple tasks (when finishing related tasks together)
|
|
342
|
+
{
|
|
343
|
+
"body_params": {
|
|
344
|
+
"ids": ["35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92", "8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c", "f2b9d1c7-3e8a-4b6f-9d2c-5a7e1f4b8c3d"]
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
**🚨 CRITICAL - NON-NEGOTIABLE RULES**:
|
|
349
|
+
- IDs must be the FULL UUID strings (e.g., '35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92') from the plan's 'id' field
|
|
350
|
+
- Get these exact UUID strings from xpget_agent_plan before calling this tool
|
|
351
|
+
- Call THIS TOOL the INSTANT you finish task(s)
|
|
352
|
+
- Can mark single or multiple tasks complete in one call
|
|
353
|
+
- Use multiple FULL UUIDs when finishing related tasks at the same time
|
|
354
|
+
- DO NOT use shortened IDs, abbreviations, or partial UUIDs - must be complete UUID strings
|
|
355
|
+
- DO NOT postpone marking completion
|
|
356
|
+
- DO NOT be lazy - mark it complete RIGHT AFTER the work is done
|
|
357
|
+
- This is MANDATORY for progress tracking and continuation
|
|
358
|
+
- If you finish a task and don't mark it complete immediately, you are doing it WRONG
|
|
359
|
+
|
|
360
|
+
**Pattern**: Finish work → Get task UUID from plan → IMMEDIATELY call xpcomplete_agent_plan_items with FULL UUID → Move to next task
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
#### **4. xpadd_new_agent_plan_item** - Add Discovered Task
|
|
365
|
+
**When to use**: When you discover additional work needed during execution
|
|
366
|
+
|
|
367
|
+
**How to use**:
|
|
368
|
+
```json
|
|
369
|
+
{
|
|
370
|
+
"body_params": {
|
|
371
|
+
"title": "Validate input schemas",
|
|
372
|
+
"completed": false
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
#### **5. xpupdate_agent_plan_item** - Modify Existing Task
|
|
379
|
+
**When to use**: When task details change or need clarification
|
|
380
|
+
|
|
381
|
+
**How to use**:
|
|
382
|
+
```json
|
|
383
|
+
{
|
|
384
|
+
"body_params": {
|
|
385
|
+
"id": "task-uuid",
|
|
386
|
+
"title": "Updated description",
|
|
387
|
+
"completed": true
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
(All fields optional except `id`)
|
|
392
|
+
|
|
393
|
+
---
|
|
218
394
|
|
|
219
|
-
|
|
395
|
+
#### **6. xpdelete_agent_plan_item** - Remove Task
|
|
396
|
+
**When to use**: When a task becomes unnecessary or redundant
|
|
397
|
+
|
|
398
|
+
**How to use**:
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"body_params": {
|
|
402
|
+
"id": "task-uuid"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
#### **7. xpstart_execution_plan** - Start Plan Execution
|
|
409
|
+
**When to use**: Immediately after creating a plan with `xpcreate_agent_plan`
|
|
410
|
+
**CRITICAL**: Must be called to enable enforcement mode before executing tasks
|
|
411
|
+
|
|
412
|
+
**How to use**:
|
|
413
|
+
```json
|
|
414
|
+
{
|
|
415
|
+
"body_params": {}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
**No parameters needed** - just call after plan creation
|
|
419
|
+
|
|
420
|
+
**What it does**:
|
|
421
|
+
- Marks plan as "started"
|
|
422
|
+
- Enables enforcement mode if `enforce` flag is true
|
|
423
|
+
- Allows plan execution to proceed
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
#### **8. xpask_for_information** - Ask User a Question
|
|
428
|
+
**When to use**: **MANDATORY AFTER PLAN STARTS** when you need ANY of the following:
|
|
429
|
+
- Information from the user
|
|
430
|
+
- Clarification on requirements
|
|
431
|
+
- Approval before proceeding
|
|
432
|
+
- To pause execution for user input
|
|
433
|
+
|
|
434
|
+
**PREREQUISITE**: Plan must be started (running) first
|
|
435
|
+
|
|
436
|
+
**CRITICAL RULE**: Once the plan has started, if you need user input or want to pause, you MUST use this tool.
|
|
437
|
+
DO NOT just respond with questions in your regular output after plan starts - that breaks execution flow!
|
|
438
|
+
|
|
439
|
+
**NOTE**: BEFORE starting the plan (before xpstart_execution_plan), you CAN ask questions directly if needed.
|
|
440
|
+
|
|
441
|
+
**How to use**:
|
|
442
|
+
```json
|
|
443
|
+
{
|
|
444
|
+
"body_params": {
|
|
445
|
+
"question": "What is the customer email address?"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**What it does**:
|
|
451
|
+
- Properly pauses plan enforcement and sets `question_raised` flag
|
|
452
|
+
- Delivers question to the user through the correct channel
|
|
453
|
+
- Manages execution state for proper continuation
|
|
454
|
+
- Allows resuming execution after user responds
|
|
455
|
+
|
|
456
|
+
**Why this matters**: Using this tool ensures execution can be properly continued with full context.
|
|
457
|
+
Just responding with a question will NOT pause execution correctly!
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
### **Best Practices**
|
|
462
|
+
|
|
463
|
+
✅ **DO:**
|
|
464
|
+
- Create comprehensive plans with ALL necessary steps
|
|
465
|
+
- **START** the plan with `xpstart_execution_plan` after creating it
|
|
466
|
+
- Use descriptive, actionable task titles
|
|
467
|
+
- Check plan before each action to stay oriented
|
|
468
|
+
- **Always use FULL UUID strings when marking tasks complete** (e.g., '35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92')
|
|
469
|
+
- **Get the exact UUID from xpget_agent_plan** - copy the full 'id' field value
|
|
470
|
+
- **Mark tasks complete THE INSTANT you finish them - NO DELAYS, NO EXCEPTIONS**
|
|
471
|
+
- **Can mark multiple tasks at once if finished together** (e.g., related tasks done simultaneously)
|
|
472
|
+
- **ALWAYS use `xpask_for_information` when you need user input or want to pause**
|
|
473
|
+
- Call plan tools **sequentially** (one at a time, never in parallel)
|
|
474
|
+
- Follow the pattern: DO WORK → GET UUID → MARK COMPLETE WITH FULL UUID → NEXT TASK
|
|
475
|
+
|
|
476
|
+
❌ **DON'T - THESE ARE FORBIDDEN:**
|
|
477
|
+
- Mark tasks complete before they're actually done
|
|
478
|
+
- **Use shortened, partial, or abbreviated task IDs** - MUST use complete UUID strings!
|
|
479
|
+
- **Use made-up or guessed UUIDs** - MUST get exact UUID from xpget_agent_plan!
|
|
480
|
+
- **Be lazy and wait to mark tasks complete later** (FORBIDDEN!)
|
|
481
|
+
- **Postpone marking completion to batch at the end** (WRONG! Mark immediately when done!)
|
|
482
|
+
- **AFTER plan starts: NEVER write questions in your response text** ("Before I proceed...", "I need clarification...", etc.)
|
|
483
|
+
- **AFTER plan starts: NEVER respond with questions - ONLY use xpask_for_information tool** (ABSOLUTE RULE!)
|
|
484
|
+
- Pass plain string arrays - must be objects with `title` field
|
|
485
|
+
- Call plan tools in parallel with each other
|
|
486
|
+
- Skip checking the plan between major steps
|
|
487
|
+
- Postpone marking completion "until later" - there is no later, only NOW
|
|
488
|
+
|
|
489
|
+
**REMEMBER**: Once execution started, any text like "I need to know", "Before I proceed", "I need clarification" means you MUST call xpask_for_information instead!
|
|
490
|
+
**EXCEPTION**: Before starting the plan, you CAN ask questions directly if needed for planning.
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
### **Example Complete Workflow**
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
1. User: "Build a REST API for user management"
|
|
498
|
+
|
|
499
|
+
2. Call: xpcreate_agent_plan
|
|
500
|
+
tasks: [
|
|
501
|
+
{"title": "Design user schema"},
|
|
502
|
+
{"title": "Create database migration"},
|
|
503
|
+
{"title": "Implement CRUD endpoints"},
|
|
504
|
+
{"title": "Add authentication"},
|
|
505
|
+
{"title": "Write integration tests"}
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
3. Call: xpstart_execution_plan
|
|
509
|
+
→ Plan now started, enforcement enabled (if enforce=true)
|
|
510
|
+
|
|
511
|
+
4. Call: xpget_agent_plan
|
|
512
|
+
→ See: Task 1 (ID: 35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92) - Design user schema - Not complete
|
|
513
|
+
|
|
514
|
+
5. [Realize need user input] Call: xpask_for_information
|
|
515
|
+
question: "Which database should we use - PostgreSQL or MySQL?"
|
|
516
|
+
→ question_raised=true, waiting for response
|
|
517
|
+
|
|
518
|
+
6. [After user responds, DO THE WORK: Design schema]
|
|
519
|
+
|
|
520
|
+
7. ⚠️ IMMEDIATELY Call: xpcomplete_agent_plan_items
|
|
521
|
+
ids: ["35f56b8e-1427-4a5e-a1c0-57a4f7ec8e92"]
|
|
522
|
+
→ MARKED COMPLETE RIGHT AFTER FINISHING - NOT DELAYED! Used FULL UUID from plan!
|
|
523
|
+
|
|
524
|
+
8. Call: xpget_agent_plan
|
|
525
|
+
→ See: Task 1 ✓ complete, Task 2 (ID: 8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c) - Create database migration - Not complete
|
|
526
|
+
|
|
527
|
+
9. [DO THE WORK: Create migration file]
|
|
528
|
+
|
|
529
|
+
10. ⚠️ IMMEDIATELY Call: xpcomplete_agent_plan_items
|
|
530
|
+
ids: ["8a3c4f12-9b7e-4d2a-b5c8-1f6e9a0d3b4c"]
|
|
531
|
+
→ MARKED COMPLETE RIGHT AWAY! Used FULL UUID from plan!
|
|
532
|
+
|
|
533
|
+
// Alternative: If tasks 3 and 4 are done together, can batch complete:
|
|
534
|
+
11a. [DO THE WORK: Implement endpoints AND add auth together]
|
|
535
|
+
11b. ⚠️ IMMEDIATELY Call: xpcomplete_agent_plan_items
|
|
536
|
+
ids: ["f2b9d1c7-3e8a-4b6f-9d2c-5a7e1f4b8c3d", "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"]
|
|
537
|
+
→ Both marked complete at once! Used FULL UUIDs from plan!
|
|
538
|
+
|
|
539
|
+
12. [Continue this pattern: GET PLAN → DO WORK → MARK COMPLETE IMMEDIATELY → REPEAT]
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### **WRONG WAY - ANTI-PATTERN (DO NOT DO THIS)**
|
|
543
|
+
|
|
544
|
+
```
|
|
545
|
+
❌ WRONG:
|
|
546
|
+
1. Call: xpcreate_agent_plan
|
|
547
|
+
2. Call: xpstart_execution_plan ← PLAN IS NOW STARTED!
|
|
548
|
+
3. Respond: "Before I proceed, I need clarification on..." ← WRONG! FORBIDDEN!
|
|
549
|
+
|
|
550
|
+
✅ CORRECT:
|
|
551
|
+
1. Call: xpcreate_agent_plan
|
|
552
|
+
2. Call: xpstart_execution_plan ← PLAN IS NOW STARTED!
|
|
553
|
+
3. Call: xpask_for_information with question ← CORRECT! Use the tool!
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**KEY POINT**: Once you call `xpstart_execution_plan`, the execution mode changes.
|
|
557
|
+
You CANNOT write questions in your response anymore. You MUST use the tool.
|
|
558
|
+
|
|
559
|
+
**Golden Rule**: FINISH TASK → MARK COMPLETE INSTANTLY → MOVE TO NEXT TASK. No delays, no batching, no "I'll do it later"!
|
|
560
|
+
</important_planning_instructions>
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
# add the expected output guidance
|
|
564
|
+
if not "expected_output" in args:
|
|
565
|
+
args["expected_output"] = ""
|
|
566
|
+
args["expected_output"] += "\nAll planned tasks completed and marked as done."
|
|
567
|
+
|
|
568
|
+
# add the plan to additional_context
|
|
569
|
+
if not "additional_context" in args:
|
|
570
|
+
args["additional_context"] = ""
|
|
571
|
+
|
|
572
|
+
plan_str = task.deep_planning.model_dump_json() if task.deep_planning and task.deep_planning.enabled and len(task.deep_planning.tasks) != 0 else "No execution plan, please generate"
|
|
573
|
+
args["additional_context"] += f" \n Current execution plan: {plan_str}"
|
|
574
|
+
|
|
575
|
+
def _load_llm_model(agent: Agent, override: Optional[Dict[str, Any]] = {}) -> Any:
|
|
220
576
|
"""
|
|
221
577
|
Load and configure the appropriate LLM model based on the agent's provider configuration.
|
|
222
578
|
|
|
@@ -278,6 +634,10 @@ def _load_llm_model(agent: Agent, override: Optional[Dict[str, Any]]) -> Any:
|
|
|
278
634
|
return env_llm_key or agent.llm_credentials.value
|
|
279
635
|
|
|
280
636
|
llm_args = {}
|
|
637
|
+
|
|
638
|
+
if agent.llm_reasoning_effort and agent.llm_reasoning_effort != LLMReasoningEffort.Medium and agent.model_name and "gpt-5" in agent.model_name.lower():
|
|
639
|
+
llm_args = { "reasoning_effort": agent.llm_reasoning_effort.value }
|
|
640
|
+
|
|
281
641
|
if agent.llm_api_base and len(agent.llm_api_base) != 0:
|
|
282
642
|
llm_args["base_url"] = agent.llm_api_base
|
|
283
643
|
|
|
@@ -427,7 +787,7 @@ def _configure_tool_calls_compression(
|
|
|
427
787
|
args["compression_manager"] = CompressionManager(
|
|
428
788
|
compress_tool_results=True,
|
|
429
789
|
compress_tool_results_limit=agent.agno_settings.tool_calls_compression.threshold,
|
|
430
|
-
compress_tool_call_instructions=agent.agno_settings.tool_calls_compression.instructions,
|
|
790
|
+
compress_tool_call_instructions="never compress ids, keep them full. "+(agent.agno_settings.tool_calls_compression.instructions if agent.agno_settings.tool_calls_compression.instructions else ""),
|
|
431
791
|
)
|
|
432
792
|
|
|
433
793
|
def _configure_agentic_memory(
|
|
@@ -442,7 +802,7 @@ def _configure_agentic_memory(
|
|
|
442
802
|
args["memory_manager"] = MemoryManager(delete_memories=True,clear_memories=True)
|
|
443
803
|
args["enable_agentic_memory"] = agent.agno_settings.agentic_memory
|
|
444
804
|
|
|
445
|
-
if agent_memories_enabled:
|
|
805
|
+
if agent_memories_enabled and not agent.is_a_team:
|
|
446
806
|
args["add_culture_to_context"] = True
|
|
447
807
|
|
|
448
808
|
if agent.agno_settings.agentic_culture:
|
|
@@ -533,7 +893,7 @@ def _configure_pre_hooks(args: Dict[str, Any], agent: Agent, model: Any) -> None
|
|
|
533
893
|
args["pre_hooks"].append(openai_moderation_guardrail)
|
|
534
894
|
|
|
535
895
|
|
|
536
|
-
async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> List[Any]:
|
|
896
|
+
async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None, auth_events_callback: Optional[Callable] = None) -> List[Any]:
|
|
537
897
|
mcp_servers = agent.mcp_servers
|
|
538
898
|
|
|
539
899
|
# combine task mcps and agent mcps
|
|
@@ -579,6 +939,7 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
|
|
|
579
939
|
),
|
|
580
940
|
include_tools=mcp.allowed_tools or None,
|
|
581
941
|
timeout_seconds=120,
|
|
942
|
+
tool_name_prefix="mcp_tool"
|
|
582
943
|
)
|
|
583
944
|
)
|
|
584
945
|
elif mcp.url:
|
|
@@ -596,7 +957,9 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
|
|
|
596
957
|
if not task.input.user or not task.input.user.id:
|
|
597
958
|
raise ValueError("MCP server with OAuth authentication detected but user id not set on the task (task.input.user.id)")
|
|
598
959
|
|
|
599
|
-
auth_result: MCPOAuthGetTokenResponse = await authenticate_mcp_server(mcp_server=mcp,task=task,user_id=task.input.user.id)
|
|
960
|
+
auth_result: MCPOAuthGetTokenResponse = await authenticate_mcp_server(mcp_server=mcp,task=task,user_id=task.input.user.id, auth_events_callback=auth_events_callback)
|
|
961
|
+
if auth_result and auth_result.data and isinstance(auth_result.data, MCPOAuthGetTokenGenericResponse) and auth_result.data.message:
|
|
962
|
+
raise ValueError(f"MCP authentication failed: {auth_result.data.message}")
|
|
600
963
|
if not auth_result:
|
|
601
964
|
raise ValueError("MCP Server authentication failed")
|
|
602
965
|
if auth_result.type != MCPOAuthResponseType.TOKEN_READY:
|
|
@@ -612,10 +975,9 @@ async def _resolve_agent_tools(agent: Agent, task: Optional[Task] = None) -> Lis
|
|
|
612
975
|
transport=transport,
|
|
613
976
|
server_params=params_cls(url=mcp.url, headers=mcp.headers),
|
|
614
977
|
include_tools=mcp.allowed_tools or None,
|
|
615
|
-
timeout_seconds=120
|
|
978
|
+
timeout_seconds=120,
|
|
979
|
+
tool_name_prefix="mcp_tool"
|
|
616
980
|
)
|
|
617
981
|
)
|
|
618
982
|
|
|
619
|
-
return agent.tools.functions +
|
|
620
|
-
*[mcp.__aenter__() for mcp in mcp_tools]
|
|
621
|
-
)
|
|
983
|
+
return agent.tools.functions + mcp_tools
|
|
@@ -10,6 +10,7 @@ async def dispatch_get_args(
|
|
|
10
10
|
override: Optional[Dict[str, Any]] = None,
|
|
11
11
|
tools: Optional[List[Callable]] = None,
|
|
12
12
|
is_async: Optional[bool] = True,
|
|
13
|
+
auth_events_callback: Optional[Callable] = None,
|
|
13
14
|
) -> Dict[str, Any]:
|
|
14
15
|
"""
|
|
15
16
|
Dispatch to the correct framework-specific argument resolver.
|
|
@@ -20,6 +21,7 @@ async def dispatch_get_args(
|
|
|
20
21
|
override (Optional[Dict[str, Any]]): Dict of override values.
|
|
21
22
|
tools (Optional[List[Callable]]): Optional additional tools to be added to the agent arguments.
|
|
22
23
|
is_async (Optional[bool]): Is in Async Context?.
|
|
24
|
+
auth_events_callback (Optional[Callable]): Optional callback function (async or sync) that receives (agent, task, event) for authentication events only.
|
|
23
25
|
|
|
24
26
|
Returns:
|
|
25
27
|
Dict[str, Any]: Arguments for instantiating the framework agent.
|
|
@@ -28,7 +30,7 @@ async def dispatch_get_args(
|
|
|
28
30
|
match agent.framework:
|
|
29
31
|
case Framework.Agno:
|
|
30
32
|
from .agno import build_agent_args
|
|
31
|
-
return await build_agent_args(xpander_agent=agent, task=task, override=override, tools=tools, is_async=is_async)
|
|
33
|
+
return await build_agent_args(xpander_agent=agent, task=task, override=override, tools=tools, is_async=is_async, auth_events_callback=auth_events_callback)
|
|
32
34
|
# case Framework.Langchain: # PLACEHOLDER
|
|
33
35
|
# from .langchain import build_agent_args
|
|
34
36
|
# return await build_agent_args(xpander_agent=agent, task=task, override=override)
|
|
@@ -1,57 +1,69 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
|
+
from typing import Callable, Optional
|
|
3
4
|
from loguru import logger
|
|
4
5
|
from xpander_sdk.consts.api_routes import APIRoute
|
|
5
6
|
from xpander_sdk.core.xpander_api_client import APIClient
|
|
6
7
|
from xpander_sdk.models.events import TaskUpdateEventType
|
|
8
|
+
from xpander_sdk.modules.agents.sub_modules.agent import Agent
|
|
7
9
|
from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
|
|
8
10
|
from xpander_sdk.modules.tools_repository.models.mcp import MCPOAuthGetTokenGenericResponse, MCPOAuthGetTokenResponse, MCPOAuthResponseType, MCPServerDetails
|
|
11
|
+
from xpander_sdk.modules.backend.events_registry import EventsRegistry
|
|
9
12
|
|
|
10
13
|
POLLING_INTERVAL = 1 # every 1s
|
|
11
|
-
MAX_WAIT_FOR_LOGIN =
|
|
14
|
+
MAX_WAIT_FOR_LOGIN = 600 # 10 mintutes
|
|
12
15
|
|
|
13
|
-
async def push_event(task: Task, event: TaskUpdateEvent):
|
|
16
|
+
async def push_event(task: Task, event: TaskUpdateEvent, event_type: TaskUpdateEventType, auth_events_callback: Optional[Callable] = None):
|
|
14
17
|
client = APIClient(configuration=task.configuration)
|
|
18
|
+
|
|
19
|
+
evt = TaskUpdateEvent(
|
|
20
|
+
task_id=task.id,
|
|
21
|
+
organization_id=task.organization_id,
|
|
22
|
+
time=datetime.now(timezone.utc).isoformat(),
|
|
23
|
+
type=event_type,
|
|
24
|
+
data=event
|
|
25
|
+
)
|
|
26
|
+
|
|
15
27
|
await client.make_request(
|
|
16
28
|
path=APIRoute.PushExecutionEventToQueue.format(task_id=task.id),
|
|
17
29
|
method="POST",
|
|
18
|
-
payload=[
|
|
19
|
-
TaskUpdateEvent(
|
|
20
|
-
task_id=task.id,
|
|
21
|
-
organization_id=task.organization_id,
|
|
22
|
-
time=datetime.now(timezone.utc).isoformat(),
|
|
23
|
-
type=TaskUpdateEventType.AuthEvent,
|
|
24
|
-
data=event
|
|
25
|
-
).model_dump_safe()
|
|
26
|
-
]
|
|
30
|
+
payload=[evt.model_dump_safe()]
|
|
27
31
|
)
|
|
32
|
+
|
|
33
|
+
# Invoke both explicit callback and registered handlers
|
|
34
|
+
# 1. Call explicit callback if provided
|
|
35
|
+
if auth_events_callback:
|
|
36
|
+
if asyncio.iscoroutinefunction(auth_events_callback):
|
|
37
|
+
await auth_events_callback(task.configuration.state.agent, task, evt)
|
|
38
|
+
else:
|
|
39
|
+
auth_events_callback(task.configuration.state.agent, task, evt)
|
|
40
|
+
|
|
41
|
+
# 2. Always invoke registered handlers from EventsRegistry
|
|
42
|
+
registry = EventsRegistry()
|
|
43
|
+
if registry.has_auth_handlers():
|
|
44
|
+
await registry.invoke_auth_handlers(task.configuration.state.agent, task, evt)
|
|
28
45
|
|
|
29
|
-
async def get_token(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
|
|
46
|
+
async def get_token(mcp_server: MCPServerDetails, task: Task, user_id: str, return_result: Optional[bool] = False, generate_login_url: Optional[bool] = True) -> MCPOAuthGetTokenResponse:
|
|
30
47
|
client = APIClient(configuration=task.configuration)
|
|
31
48
|
result: MCPOAuthGetTokenResponse = await client.make_request(
|
|
32
49
|
path=APIRoute.GetUserMCPAuthToken.format(agent_id=task.agent_id, user_id=user_id),
|
|
33
50
|
method="POST",
|
|
51
|
+
query={"generate_login_url": generate_login_url},
|
|
34
52
|
payload=mcp_server.model_dump(),
|
|
35
53
|
model=MCPOAuthGetTokenResponse
|
|
36
54
|
)
|
|
37
|
-
if result.type == MCPOAuthResponseType.TOKEN_READY:
|
|
55
|
+
if result.type == MCPOAuthResponseType.TOKEN_READY or return_result:
|
|
38
56
|
return result
|
|
39
57
|
|
|
40
58
|
return None
|
|
41
59
|
|
|
42
|
-
async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user_id: str) -> MCPOAuthGetTokenResponse:
|
|
60
|
+
async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user_id: str, auth_events_callback: Optional[Callable] = None) -> MCPOAuthGetTokenResponse:
|
|
43
61
|
try:
|
|
44
62
|
logger.info(f"Authenticating MCP Server {mcp_server.url}")
|
|
45
63
|
|
|
46
64
|
user_identifier = user_id if mcp_server.share_user_token_across_other_agents else f"{task.agent_id}_{user_id}"
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
result: MCPOAuthGetTokenResponse = await client.make_request(
|
|
50
|
-
path=APIRoute.GetUserMCPAuthToken.format(agent_id=task.agent_id, user_id=user_identifier),
|
|
51
|
-
method="POST",
|
|
52
|
-
payload=mcp_server.model_dump(),
|
|
53
|
-
model=MCPOAuthGetTokenResponse
|
|
54
|
-
)
|
|
66
|
+
result: MCPOAuthGetTokenResponse = await get_token(mcp_server=mcp_server, task=task, user_id=user_identifier, return_result=True)
|
|
55
67
|
|
|
56
68
|
if not result:
|
|
57
69
|
raise Exception("Invalid response")
|
|
@@ -59,7 +71,7 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
|
|
|
59
71
|
if result.type == MCPOAuthResponseType.LOGIN_REQUIRED:
|
|
60
72
|
logger.info(f"Initiating login for MCP Server {mcp_server.url}")
|
|
61
73
|
# Notify user about login requirement
|
|
62
|
-
await push_event(task=task, event=result)
|
|
74
|
+
await push_event(task=task, event=result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
|
|
63
75
|
|
|
64
76
|
# Poll for token with timeout
|
|
65
77
|
elapsed_time = 0
|
|
@@ -68,12 +80,12 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
|
|
|
68
80
|
elapsed_time += POLLING_INTERVAL
|
|
69
81
|
|
|
70
82
|
# Check for token
|
|
71
|
-
token_result = await get_token(mcp_server=mcp_server, task=task, user_id=user_identifier)
|
|
83
|
+
token_result = await get_token(mcp_server=mcp_server, task=task, user_id=user_identifier, generate_login_url=False)
|
|
72
84
|
if token_result and token_result.type == MCPOAuthResponseType.TOKEN_READY:
|
|
73
85
|
logger.info(f"Successful login for MCP Server {mcp_server.url}")
|
|
74
86
|
redacted_token_result = MCPOAuthGetTokenResponse(**token_result.model_dump_safe())
|
|
75
87
|
redacted_token_result.data.access_token = "REDACTED"
|
|
76
|
-
await push_event(task=task, event=redacted_token_result)
|
|
88
|
+
await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
|
|
77
89
|
return token_result
|
|
78
90
|
|
|
79
91
|
# Timeout reached
|
|
@@ -83,7 +95,7 @@ async def authenticate_mcp_server(mcp_server: MCPServerDetails, task: Task, user
|
|
|
83
95
|
logger.info(f"Token ready for MCP Server {mcp_server.url}")
|
|
84
96
|
redacted_token_result = MCPOAuthGetTokenResponse(**result.model_dump_safe())
|
|
85
97
|
redacted_token_result.data.access_token = "REDACTED"
|
|
86
|
-
await push_event(task=task, event=redacted_token_result)
|
|
98
|
+
await push_event(task=task, event=redacted_token_result, event_type=TaskUpdateEventType.AuthEvent, auth_events_callback=auth_events_callback)
|
|
87
99
|
|
|
88
100
|
return result
|
|
89
101
|
except Exception as e:
|