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.
Files changed (37) hide show
  1. xpander_sdk/__init__.py +6 -0
  2. xpander_sdk/consts/api_routes.py +9 -0
  3. xpander_sdk/models/activity.py +65 -0
  4. xpander_sdk/models/compactization.py +112 -0
  5. xpander_sdk/models/deep_planning.py +18 -0
  6. xpander_sdk/models/events.py +6 -0
  7. xpander_sdk/models/frameworks.py +2 -2
  8. xpander_sdk/models/generic.py +27 -0
  9. xpander_sdk/models/notifications.py +98 -0
  10. xpander_sdk/models/orchestrations.py +271 -0
  11. xpander_sdk/modules/agents/models/agent.py +11 -5
  12. xpander_sdk/modules/agents/sub_modules/agent.py +25 -10
  13. xpander_sdk/modules/backend/__init__.py +8 -0
  14. xpander_sdk/modules/backend/backend_module.py +47 -2
  15. xpander_sdk/modules/backend/decorators/__init__.py +7 -0
  16. xpander_sdk/modules/backend/decorators/on_auth_event.py +131 -0
  17. xpander_sdk/modules/backend/events_registry.py +172 -0
  18. xpander_sdk/modules/backend/frameworks/agno.py +377 -15
  19. xpander_sdk/modules/backend/frameworks/dispatch.py +3 -1
  20. xpander_sdk/modules/backend/utils/mcp_oauth.py +37 -25
  21. xpander_sdk/modules/events/decorators/__init__.py +3 -0
  22. xpander_sdk/modules/events/decorators/on_tool.py +384 -0
  23. xpander_sdk/modules/events/events_module.py +28 -1
  24. xpander_sdk/modules/tasks/models/task.py +3 -14
  25. xpander_sdk/modules/tasks/sub_modules/task.py +276 -84
  26. xpander_sdk/modules/tools_repository/models/mcp.py +1 -0
  27. xpander_sdk/modules/tools_repository/sub_modules/tool.py +46 -15
  28. xpander_sdk/modules/tools_repository/tools_repository_module.py +6 -2
  29. xpander_sdk/modules/tools_repository/utils/generic.py +3 -0
  30. xpander_sdk/utils/agents/__init__.py +0 -0
  31. xpander_sdk/utils/agents/compactization_agent.py +257 -0
  32. xpander_sdk/utils/generic.py +5 -0
  33. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/METADATA +224 -14
  34. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/RECORD +37 -24
  35. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/WHEEL +0 -0
  36. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/licenses/LICENSE +0 -0
  37. {xpander_sdk-2.0.144.dist-info → xpander_sdk-2.0.192.dist-info}/top_level.txt +0 -0
@@ -41,6 +41,7 @@ from xpander_sdk.modules.tools_repository.utils.schemas import (
41
41
  schema_enforcement_block_and_descriptions,
42
42
  )
43
43
  from xpander_sdk.utils.event_loop import run_sync
44
+ from xpander_sdk.modules.events.decorators.on_tool import ToolHooksRegistry
44
45
 
45
46
 
46
47
  class Tool(XPanderSharedModel):
@@ -376,6 +377,15 @@ class Tool(XPanderSharedModel):
376
377
  task_id=task_id,
377
378
  )
378
379
 
380
+ # Execute before hooks
381
+ await ToolHooksRegistry.execute_before_hooks(
382
+ tool=self,
383
+ payload=payload,
384
+ payload_extension=payload_extension,
385
+ tool_call_id=tool_call_id,
386
+ agent_version=agent_version
387
+ )
388
+
379
389
  try:
380
390
  if self.schema and payload:
381
391
  try:
@@ -386,32 +396,43 @@ class Tool(XPanderSharedModel):
386
396
  ) from validation_error
387
397
 
388
398
  if self.is_local:
389
- await self.agraph_preflight_check(
390
- agent_id=agent_id,
391
- agent_version=agent_version,
392
- configuration=configuration,
393
- task_id=task_id,
394
- )
395
-
396
399
  if self.fn is None:
397
400
  raise RuntimeError(
398
401
  f"No local function provided for this tool ({self.id})."
399
402
  )
400
403
 
401
404
  result = await invoke_local_fn(fn=self.fn, payload=payload)
405
+
406
+ await self.agraph_preflight_check(
407
+ agent_id=agent_id,
408
+ agent_version=agent_version,
409
+ configuration=configuration,
410
+ task_id=task_id,
411
+ payload={"input": payload, "output": result.model_dump() if isinstance(result, BaseModel) else result}
412
+ )
413
+
402
414
  tool_invocation_result.result = result
403
415
  tool_invocation_result.is_success = True
404
- return tool_invocation_result
405
-
406
- tool_invocation_result.result = await self.acall_remote_tool(
407
- agent_id=agent_id,
408
- agent_version=agent_version,
416
+ else:
417
+ tool_invocation_result.result = await self.acall_remote_tool(
418
+ agent_id=agent_id,
419
+ agent_version=agent_version,
420
+ payload=payload,
421
+ payload_extension=payload_extension,
422
+ configuration=configuration,
423
+ task_id=task_id,
424
+ )
425
+ tool_invocation_result.is_success = True
426
+
427
+ # Execute after hooks on success
428
+ await ToolHooksRegistry.execute_after_hooks(
429
+ tool=self,
409
430
  payload=payload,
410
431
  payload_extension=payload_extension,
411
- configuration=configuration,
412
- task_id=task_id,
432
+ tool_call_id=tool_call_id,
433
+ agent_version=agent_version,
434
+ result=tool_invocation_result.result
413
435
  )
414
- tool_invocation_result.is_success = True
415
436
 
416
437
  except Exception as e:
417
438
  tool_invocation_result.is_error = True
@@ -421,6 +442,16 @@ class Tool(XPanderSharedModel):
421
442
  else:
422
443
  tool_invocation_result.status_code = 500
423
444
  tool_invocation_result.result = str(e)
445
+
446
+ # Execute error hooks on failure
447
+ await ToolHooksRegistry.execute_error_hooks(
448
+ tool=self,
449
+ payload=payload,
450
+ payload_extension=payload_extension,
451
+ tool_call_id=tool_call_id,
452
+ agent_version=agent_version,
453
+ error=e
454
+ )
424
455
 
425
456
  return tool_invocation_result
426
457
 
@@ -16,7 +16,7 @@ from xpander_sdk.models.configuration import Configuration
16
16
  from xpander_sdk.models.shared import XPanderSharedModel
17
17
  from xpander_sdk.modules.tools_repository.sub_modules.tool import Tool
18
18
  from xpander_sdk.utils.event_loop import run_sync
19
-
19
+ import json
20
20
 
21
21
  class ToolsRepository(XPanderSharedModel):
22
22
  """
@@ -51,7 +51,7 @@ class ToolsRepository(XPanderSharedModel):
51
51
  tools: List[Tool] = []
52
52
 
53
53
  agent_graph: Optional[Any] = None
54
- is_async: Optional[bool] = False
54
+ is_async: Optional[bool] = True
55
55
 
56
56
  # Immutable registry for tools defined via decorator
57
57
  _local_tools: ClassVar[List[Tool]] = []
@@ -158,6 +158,10 @@ class ToolsRepository(XPanderSharedModel):
158
158
  fn_list = []
159
159
 
160
160
  for tool in self.list:
161
+
162
+ # add json schema to the model doc
163
+ tool.schema.__doc__ = "Pay attention to the schema, dont miss. " + json.dumps(tool.schema.model_json_schema(mode="serialization"))
164
+
161
165
  schema_cls: Type[BaseModel] = tool.schema
162
166
 
163
167
  # Create closure to capture tool and schema_cls
@@ -38,6 +38,9 @@ def json_type_to_python(json_type: str, prop_schema: dict = None):
38
38
  item_type = json_type_to_python(items.get("type"), items)
39
39
  return List[item_type]
40
40
 
41
+ if isinstance(json_type, list) and len(json_type) > 1:
42
+ json_type = next((t for t in json_type if t != "null"), None)
43
+
41
44
  return {
42
45
  "string": str,
43
46
  "integer": int,
File without changes
@@ -0,0 +1,257 @@
1
+ from typing import TYPE_CHECKING, List, Union
2
+ from agno.agent import Agent as AgnoAgent
3
+ from loguru import logger
4
+ import json
5
+ from xpander_sdk.models.compactization import TaskCompactizationEvent, TaskCompactizationOutput, TaskCompactizationInput, TaskCompactizationRetryEvent
6
+ from xpander_sdk.models.deep_planning import DeepPlanningItem
7
+ from xpander_sdk.models.events import TaskUpdateEventType
8
+ from xpander_sdk.models.frameworks import Framework
9
+ from xpander_sdk.models.shared import Tokens
10
+ from xpander_sdk.modules.agents.agents_module import Agents
11
+ from xpander_sdk.modules.backend.backend_module import Backend
12
+ from xpander_sdk.modules.backend.utils.mcp_oauth import push_event
13
+ from xpander_sdk.modules.tasks.sub_modules.task import TaskUpdateEvent
14
+ from xpander_sdk.utils.event_loop import run_sync
15
+
16
+ if TYPE_CHECKING:
17
+ from xpander_sdk.modules.tasks.sub_modules.task import Task
18
+
19
+ def run_task_compactization(message: str, task: "Task", uncompleted_tasks: List[DeepPlanningItem]) -> Union[str, TaskCompactizationOutput]:
20
+ try:
21
+
22
+ # report retry event
23
+ try:
24
+ run_sync(
25
+ push_event(
26
+ task=task,
27
+ event=TaskCompactizationEvent(type="retry", data=TaskCompactizationRetryEvent(is_retry=True)),
28
+ event_type=TaskUpdateEventType.TaskCompactization
29
+ )
30
+ )
31
+ except Exception as e:
32
+ pass
33
+
34
+ # get agent to identify framework
35
+ agent = Agents(configuration=task.configuration).get(agent_id=task.agent_id,version=task.agent_version)
36
+
37
+ # non agno, let the same agent handle it
38
+ if agent.framework != Framework.Agno:
39
+ return "\n".join([
40
+ "Task not finished, uncompleted tasks detected:",
41
+ f"Uncompleted tasks: {[task.model_dump_json() for task in uncompleted_tasks]}",
42
+ "You must complete tasks if fulfilled",
43
+ f"User's original request: \"{message}\""
44
+ ])
45
+
46
+ # load backend args for consistency of model provider and settings
47
+ agno_args = Backend(configuration=agent.configuration).get_args(agent_id=agent.id, agent_version=agent.version, task=task)
48
+
49
+ if agent.model_provider == "openai":
50
+ agno_args["model"].id = "gpt-5-mini"
51
+
52
+ # create compacitzation agent
53
+ compactization_agent = AgnoAgent(
54
+ output_schema=TaskCompactizationOutput,
55
+ name="Task Compactization Agent",
56
+ model=agno_args.get("model"),
57
+ description="""
58
+ You are a system component that handles early/unapproved agent task exits.
59
+
60
+ When an agent stops execution with uncompleted tasks, you analyze the state and generate a continuation prompt
61
+ that will be sent DIRECTLY to that agent to resume its work.
62
+
63
+ Your output becomes the agent's next input - it must guide the agent to:
64
+ 1. Continue from where it stopped (seamlessly, no restart)
65
+ 2. Complete ALL remaining uncompleted tasks
66
+ 3. Use correct tools (especially xpask_for_information for questions)
67
+ 4. NOT expose internal orchestration (xp* tools, task IDs) to the user
68
+
69
+ You are a bridge between system orchestration and agent execution.
70
+ """,
71
+ role="""
72
+ You are the **Compactization Agent** - a system component for handling early agent exits.
73
+
74
+ **The Flow:**
75
+ 1. Agent A (e.g., researcher) starts task and creates plan
76
+ 2. Agent A executes but stops/exits early with uncompleted tasks
77
+ 3. System detects uncompleted tasks and triggers YOU
78
+ 4. You analyze Agent A's execution state and generate continuation guidance
79
+ 5. Your output goes DIRECTLY to Agent A as its next prompt
80
+ 6. Agent A resumes and completes remaining work
81
+
82
+ **Your Mission:**
83
+ Generate a prompt that Agent A will receive to continue its work. This prompt must:
84
+ - Guide Agent A on what's been done and what remains
85
+ - Correct any protocol violations (e.g., asking questions without tool)
86
+ - Instruct Agent A to NOT expose internal xp* tools to the user
87
+ - Make Agent A's continuation feel seamless to the user
88
+
89
+ You are NOT talking to the user. You are talking to Agent A.
90
+ """,
91
+ instructions="""
92
+ ## Your Role: Agent-to-Agent Handoff
93
+
94
+ You are writing a message that will be sent TO the agent (Agent A) to continue its task.
95
+
96
+ **Key Understanding:**
97
+ - Your `new_task_prompt` = The message Agent A will receive
98
+ - Agent A will see this message and continue working
99
+ - The user will see Agent A's response, not your message directly
100
+ - You must guide Agent A to work seamlessly without exposing internals
101
+
102
+ ## Core Principles
103
+
104
+ * **You are guiding Agent A**, not the user
105
+ * **Agent A must complete ALL uncompleted tasks** - tell it what remains
106
+ * **Stay factual** - Use "Unknown" for missing info
107
+ * **Preserve exact state** - Keep IDs, names, paths, outputs unchanged in context
108
+ * **Correct violations** - If Agent A asked questions without tool, tell it to use the tool
109
+ * **Hide orchestration** - Instruct Agent A to NOT mention xp* tools, task IDs, or internals to user
110
+
111
+ ## What You Must Capture
112
+
113
+ * **Tool call history.** For each tool: name/id, purpose, inputs (high-level), outputs (high-level), errors. Include plan tools (xpcreate_agent_plan, xpcomplete_agent_plan_item, etc.).
114
+ * **Artifacts and state.** All created files, IDs, URLs, variables, decisions, and configurations.
115
+ * **What worked / what didn't.** Successes, failures, blockers, missing info, wrong assumptions.
116
+ * **Task completion status.** For each uncompleted task: exact ID, title, and specific reason it's incomplete.
117
+ * **PROTOCOL VIOLATIONS.** Check if agent asked questions in response text AFTER calling xpstart_execution_plan (violates protocol).
118
+
119
+ ## Detecting Protocol Violations
120
+
121
+ If the agent's last message contains questions to the user AFTER the plan was started (after xpstart_execution_plan was called):
122
+ - This is a PROTOCOL VIOLATION
123
+ - The agent should have used xpask_for_information tool instead
124
+
125
+ When violation is detected, you MUST make it SUPER CLEAR in BOTH output fields:
126
+
127
+ **In `new_task_prompt`:**
128
+ - START with: "CRITICAL PROTOCOL VIOLATION DETECTED: You asked questions directly after starting the plan."
129
+ - Explicitly state: "You MUST use xpask_for_information tool to ask questions once plan is running."
130
+ - Include: "NEVER write questions in your response text after calling xpstart_execution_plan."
131
+ - Then provide the continuation instructions
132
+
133
+ **In `task_context`:**
134
+ - Add a dedicated section at the TOP before section 1: "**PROTOCOL VIOLATION DETECTED**"
135
+ - State clearly: "Agent asked questions in response text after calling xpstart_execution_plan. This violates execution protocol."
136
+ - Remind: "Rule: After xpstart_execution_plan is called, questions MUST be asked using xpask_for_information tool, NOT written in response text."
137
+
138
+ Signs of protocol violation in messages:
139
+ - Phrases like "Before I proceed", "I need clarification", "Please choose", "Which option"
140
+ - Questions written in response text after xpstart_execution_plan was called
141
+
142
+ ## Continuation Prompt: Your Message TO Agent A
143
+
144
+ Your `new_task_prompt` is what Agent A will read. Structure it as guidance TO the agent:
145
+
146
+ 1. **IF protocol violation:** "You asked questions without using xpask_for_information tool. Use the tool for questions."
147
+ 2. **Orient Agent A:** "You were working on [task]. You've completed [X, Y, Z]."
148
+ 3. **State what remains:** "You still need to complete: [list remaining work in plain language]."
149
+ 4. **Guide next steps:** "Continue by: [step-by-step what to do next]."
150
+ 5. **Hide internals instruction:** "Important: Do NOT mention xp* tools, task IDs, or internal workflow to the user. Present your work naturally."
151
+ 6. **Enforce protocol:** "Mark each task complete immediately after finishing using xpcomplete_agent_plan_item."
152
+ 7. **Natural tone:** "Continue your response to the user seamlessly, as if you never stopped."
153
+
154
+ You are INSTRUCTING Agent A on how to continue, not writing the user-facing response yourself.
155
+
156
+ ## Task Context Requirements
157
+
158
+ Your `task_context` must:
159
+ 1. **IF protocol violation detected:** Add "PROTOCOL VIOLATION DETECTED" section at the TOP before section 1
160
+ 2. Follow the exact 9-heading structure specified in the output schema (or 10 if violation detected)
161
+ 3. Be deterministic and parseable by downstream systems
162
+ 4. Focus on actionable continuation steps in section 9 (NEXT ACTIONS)
163
+ 5. Include task IDs for all uncompleted tasks
164
+ 6. Emphasize that ALL tasks must be completed and marked complete
165
+
166
+ ## Critical Rules
167
+
168
+ ❌ **NEVER:**
169
+ - Suggest restarting or creating a new plan
170
+ - Mark tasks as complete that aren't actually done
171
+ - Invent details or speculate
172
+ - Write a "fresh start" prompt
173
+
174
+ ✅ **ALWAYS:**
175
+ - Write as instructions TO Agent A (you're guiding it, not doing its work)
176
+ - Tell Agent A what it's done and what remains
177
+ - Instruct Agent A to complete remaining tasks
178
+ - Tell Agent A to use correct tools (xpask_for_information for questions)
179
+ - Instruct Agent A to hide xp* tools and internals from user
180
+ - Include technical details (task IDs, tool names) in task_context for Agent A's reference
181
+ - Correct any protocol violations Agent A made
182
+ """,
183
+ expected_output="""
184
+ Return a JSON object with exactly these two fields:
185
+
186
+ - new_task_prompt (string): Instructions TO Agent A for continuing its task.
187
+
188
+ This message will be sent directly to Agent A. Write it as guidance:
189
+
190
+ IF PROTOCOL VIOLATION:
191
+ "You asked questions without using xpask_for_information tool. Use the tool for questions after plan starts."
192
+
193
+ Then:
194
+ "You were working on [task name]. You've completed [work done in plain language].
195
+
196
+ You still need to complete: [list remaining uncompleted work naturally].
197
+
198
+ Continue by: [specific steps for Agent A to take].
199
+
200
+ IMPORTANT: When responding to the user, do NOT mention xp* tools, task IDs, or internal workflow.
201
+ Present your work naturally as if execution never stopped. The user should not see any interruption.
202
+
203
+ Mark each task complete immediately after finishing using xpcomplete_agent_plan_item."
204
+
205
+ - task_context (string): Comprehensive context for continuation.
206
+ IF PROTOCOL VIOLATION DETECTED:
207
+ - Add section at TOP: "**PROTOCOL VIOLATION DETECTED**\nAgent asked questions in response text after calling xpstart_execution_plan. This violates execution protocol. Rule: After xpstart_execution_plan is called, questions MUST be asked using xpask_for_information tool, NOT written in response text.\n\n"
208
+ Then follow the EXACT 9-section structure defined in TaskCompactizationOutput.task_context.
209
+ Must be deterministic, actionable, and focused on completing ALL remaining tasks and marking them as completed.
210
+
211
+ Important:
212
+ - Do NOT invent details; write 'Unknown' when information is missing.
213
+ - Preserve exact IDs, names, file paths, numbers, and user phrasing.
214
+ - Final actions must include marking each remaining task as completed with the plan tools.
215
+ - IF protocol violation: Make it SUPER CLEAR in both fields that tool must be used for questions.
216
+ """
217
+ )
218
+
219
+ session = agent.get_session(session_id=task.id)
220
+
221
+ # run compactization
222
+ run_result = compactization_agent.run(
223
+ input=TaskCompactizationInput(
224
+ user_input=task.input,
225
+ agent_instructions=agent.instructions,
226
+ task_context_and_messages=json.dumps({"messages": [message.model_dump() for message in session.get_messages() if message.role != "system"]}),
227
+ uncompleted_tasks=uncompleted_tasks
228
+ )
229
+ )
230
+
231
+ # reset old session
232
+ agent.delete_session(session_id=task.id)
233
+
234
+ # report LLM Metrics
235
+ task.tokens = Tokens(
236
+ completion_tokens=run_result.metrics.output_tokens,
237
+ prompt_tokens=run_result.metrics.input_tokens
238
+ )
239
+
240
+ task.report_metrics(configuration=task.configuration)
241
+
242
+ # report compactization event
243
+ try:
244
+ run_sync(
245
+ push_event(
246
+ task=task,
247
+ event=TaskCompactizationEvent(type="summarization", data=run_result.content),
248
+ event_type=TaskUpdateEventType.TaskCompactization
249
+ )
250
+ )
251
+ except Exception as e:
252
+ pass
253
+
254
+ return run_result.content
255
+ except Exception as e:
256
+ logger.warning(f"Failed to run task compactization - {str(e)}")
257
+ return message
@@ -0,0 +1,5 @@
1
+ from datetime import datetime, timezone
2
+
3
+
4
+ def get_current_timestamp() -> str:
5
+ return datetime.now(timezone.utc).isoformat()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xpander-sdk
3
- Version: 2.0.144
3
+ Version: 2.0.192
4
4
  Summary: xpander.ai Backend-as-a-service for AI Agents - SDK
5
5
  Home-page: https://www.xpander.ai
6
6
  Author: xpanderAI
@@ -11,15 +11,16 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: python-dotenv
15
- Requires-Dist: packaging
16
- Requires-Dist: pydantic
17
- Requires-Dist: loguru
18
- Requires-Dist: httpx
19
- Requires-Dist: httpx_sse
20
- Requires-Dist: nest-asyncio
21
- Requires-Dist: strands-agents
22
- Requires-Dist: openai-agents
14
+ Requires-Dist: python-dotenv>=1.2.1
15
+ Requires-Dist: packaging>=25.0
16
+ Requires-Dist: pydantic>=2.12.5
17
+ Requires-Dist: loguru>=0.7.3
18
+ Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: httpx_sse>=0.4.3
20
+ Requires-Dist: nest-asyncio>=1.6.0
21
+ Requires-Dist: strands-agents>=1.20.0
22
+ Requires-Dist: openai-agents>=0.6.4
23
+ Requires-Dist: python-toon>=0.1.3
23
24
  Provides-Extra: agno
24
25
  Requires-Dist: agno; extra == "agno"
25
26
  Requires-Dist: sqlalchemy; extra == "agno"
@@ -47,7 +48,7 @@ Dynamic: requires-dist
47
48
  Dynamic: requires-python
48
49
  Dynamic: summary
49
50
 
50
- # xpander.ai SDK
51
+ # xpander.ai SDK
51
52
 
52
53
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Documentation](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://docs.xpander.ai) [![PyPI Version](https://img.shields.io/pypi/v/xpander-sdk?label=PyPI)](https://pypi.org/project/xpander-sdk/) [![Downloads](https://pepy.tech/badge/xpander-sdk)](https://pepy.tech/project/xpander-sdk)
53
54
 
@@ -131,6 +132,11 @@ tasks = Tasks(configuration=config)
131
132
  task = await tasks.aget("task-id")
132
133
  await task.aset_status(AgentExecutionStatus.Running)
133
134
  await task.asave()
135
+
136
+ # Retrieve task activity log
137
+ activity_log = await task.aget_activity_log()
138
+ for message in activity_log.messages:
139
+ print(f"{message.role}: {message.content.text}")
134
140
  ```
135
141
 
136
142
  ### 4. Tools Integration
@@ -287,6 +293,123 @@ async for event in task.aevents():
287
293
  print(f"Event Data: {event.data}")
288
294
  ```
289
295
 
296
+ ### Authentication Events Callback
297
+
298
+ Handle authentication events in real-time. This callback is triggered only for authentication flows (e.g., MCP OAuth requiring user login).
299
+
300
+ **You can use both approaches simultaneously** - decorated handlers will always be invoked, and you can also pass an explicit callback for additional handling.
301
+
302
+ You can provide the callback in two ways:
303
+
304
+ #### Option 1: Direct Function
305
+
306
+ ```python
307
+ from xpander_sdk import Backend
308
+ from xpander_sdk.modules.agents.sub_modules.agent import Agent
309
+ from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
310
+ from agno.agent import Agent as AgnoAgent
311
+
312
+ # Define event callback (async or sync)
313
+ async def my_event_callback(agent: Agent, task: Task, event: TaskUpdateEvent):
314
+ """Called for authentication events only"""
315
+ # event.type will always be "auth_event"
316
+ print(f"Authentication required: {event.data}")
317
+ # Display login URL or handle OAuth flow
318
+
319
+ # Get args with callback
320
+ backend = Backend(configuration=config)
321
+ args = await backend.aget_args(
322
+ agent_id="agent-123",
323
+ task=my_task,
324
+ auth_events_callback=my_event_callback
325
+ )
326
+ ```
327
+
328
+ #### Option 2: Decorator (Auto-registered)
329
+
330
+ ```python
331
+ from xpander_sdk import Backend, on_auth_event
332
+ from xpander_sdk.modules.agents.sub_modules.agent import Agent
333
+ from xpander_sdk.modules.tasks.sub_modules.task import Task, TaskUpdateEvent
334
+ from agno.agent import Agent as AgnoAgent
335
+
336
+ # Use decorator - auto-registers globally
337
+ @on_auth_event
338
+ async def handle_auth(agent: Agent, task: Task, event: TaskUpdateEvent):
339
+ # event.type will always be "auth_event"
340
+ print(f"Authentication required for {agent.name}")
341
+ print(f"Auth data: {event.data}")
342
+
343
+ # Decorated handler is automatically invoked - no need to pass it
344
+ backend = Backend(configuration=config)
345
+ args = await backend.aget_args(
346
+ agent_id="agent-123",
347
+ task=my_task
348
+ )
349
+ ```
350
+
351
+ #### Option 3: Combine Both
352
+
353
+ ```python
354
+ from xpander_sdk import Backend, on_auth_event
355
+
356
+ # Global handler for all auth events
357
+ @on_auth_event
358
+ async def log_auth(agent, task, event):
359
+ print(f"[GLOBAL] Auth event for {agent.name}")
360
+
361
+ # Additional one-time handler
362
+ async def custom_handler(agent, task, event):
363
+ print(f"[CUSTOM] Specific handling for this call")
364
+
365
+ # Both handlers will be invoked
366
+ args = await backend.aget_args(
367
+ agent_id="agent-123",
368
+ auth_events_callback=custom_handler # Optional additional callback
369
+ )
370
+
371
+ # Use with Agno
372
+ agno_agent = AgnoAgent(**args)
373
+ result = await agno_agent.arun(
374
+ input="Process this data",
375
+ stream=True
376
+ )
377
+ ```
378
+
379
+ ### Task Activity Monitoring
380
+
381
+ ```python
382
+ from xpander_sdk import Task
383
+ from xpander_sdk.models.activity import (
384
+ AgentActivityThreadMessage,
385
+ AgentActivityThreadToolCall,
386
+ AgentActivityThreadReasoning
387
+ )
388
+
389
+ # Load a completed task
390
+ task = await Task.aload("task-id")
391
+
392
+ # Get detailed activity log
393
+ activity_log = await task.aget_activity_log()
394
+
395
+ # Analyze messages between user and agent
396
+ for message in activity_log.messages:
397
+ if isinstance(message, AgentActivityThreadMessage):
398
+ print(f"{message.role}: {message.content.text}")
399
+ elif isinstance(message, AgentActivityThreadToolCall):
400
+ # Tool call
401
+ print(f"Tool: {message.tool_name}")
402
+ print(f"Payload: {message.payload}")
403
+ print(f"Result: {message.result}")
404
+ elif isinstance(message, AgentActivityThreadReasoning):
405
+ # Reasoning step
406
+ print(f"Reasoning ({message.type}): {message.thought}")
407
+
408
+ # Synchronous version
409
+ task = Task.load("task-id")
410
+ activity_log = task.get_activity_log()
411
+ ```
412
+
290
413
  ### Local Task Testing
291
414
 
292
415
  ```python
@@ -378,6 +501,93 @@ load_dotenv()
378
501
  config = Configuration()
379
502
  ```
380
503
 
504
+ ## 🏢 Self-Hosted Deployment
505
+
506
+ If you're using a self-hosted xpander.ai deployment, configure the SDK to point to your Agent Controller endpoint.
507
+
508
+ **Important**: Use the **Agent Controller API key** generated during Helm installation, not your xpander.ai cloud API key.
509
+
510
+ ### Configuration
511
+
512
+ ```bash
513
+ # Set environment variables
514
+ export XPANDER_API_KEY="your-agent-controller-api-key" # From Helm installation
515
+ export XPANDER_ORGANIZATION_ID="your-org-id"
516
+ export XPANDER_BASE_URL="https://agent-controller.my-company.com"
517
+ ```
518
+
519
+ Or configure explicitly:
520
+
521
+ ```python
522
+ from xpander_sdk import Configuration
523
+
524
+ config = Configuration(
525
+ api_key="your-agent-controller-api-key", # From Helm installation
526
+ organization_id="your-org-id",
527
+ base_url="https://agent-controller.my-company.com"
528
+ )
529
+ ```
530
+
531
+ ### Using with Agno Framework
532
+
533
+ ```python
534
+ from xpander_sdk import Backend, Configuration
535
+ from agno.agent import Agent
536
+
537
+ # Configure for self-hosted
538
+ config = Configuration(
539
+ api_key="your-agent-controller-api-key", # From Helm installation
540
+ organization_id="your-org-id",
541
+ base_url="https://agent-controller.my-company.com"
542
+ )
543
+
544
+ # Initialize Backend with self-hosted config
545
+ backend = Backend(configuration=config)
546
+
547
+ # Create agent - it will use your self-hosted infrastructure
548
+ agno_agent = Agent(**backend.get_args(agent_id="agent-123"))
549
+
550
+ # Run agent
551
+ result = await agno_agent.arun(
552
+ input="What can you help me with?",
553
+ stream=True
554
+ )
555
+ ```
556
+
557
+ ### Complete Self-Hosted Example
558
+
559
+ ```python
560
+ import asyncio
561
+ from xpander_sdk import Configuration, Agent
562
+
563
+ async def main():
564
+ # Configure for self-hosted deployment
565
+ config = Configuration(
566
+ api_key="your-agent-controller-api-key", # From Helm installation
567
+ organization_id="your-org-id",
568
+ base_url="https://agent-controller.my-company.com"
569
+ )
570
+
571
+ # Load agent from self-hosted deployment
572
+ agent = await Agent.aload("agent-123", configuration=config)
573
+ print(f"Agent: {agent.name}")
574
+
575
+ # Create and execute task
576
+ task = await agent.acreate_task(
577
+ prompt="Analyze Q4 sales data",
578
+ file_urls=["https://example.com/sales-q4.csv"]
579
+ )
580
+ print(f"Task created: {task.id}")
581
+ print(f"Status: {task.status}")
582
+
583
+ if __name__ == "__main__":
584
+ asyncio.run(main())
585
+ ```
586
+
587
+ **Important**: Make sure your `base_url` points to the Agent Controller endpoint (e.g., `https://agent-controller.{your-domain}`), not the root domain.
588
+
589
+ 📖 **Full Guide**: [Self-Hosted Configuration Documentation](https://docs.xpander.ai/api-reference/configuration/self-hosted)
590
+
381
591
  ## 🔄 Error Handling
382
592
 
383
593
  ```python
@@ -392,9 +602,9 @@ except ModuleException as e:
392
602
  ## 🤝 Contributing
393
603
 
394
604
  1. Fork the repository
395
- 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
396
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
397
- 4. Push to the branch (`git push origin feature/amazing-feature`)
605
+ 2. Create a feature branch (`git checkout -b feature/{base_branch}/amazing-feature`)
606
+ 3. Commit your changes (`git commit -m 'feat/chore/fix: Add amazing feature'`)
607
+ 4. Push to the branch (`git push origin feature/{base_branch}/amazing-feature`)
398
608
  5. Open a Pull Request
399
609
 
400
610
  ## 📄 License