bat-adk 2025.12__tar.gz
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.
- bat_adk-2025.12/PKG-INFO +70 -0
- bat_adk-2025.12/README.md +45 -0
- bat_adk-2025.12/pyproject.toml +40 -0
- bat_adk-2025.12/setup.cfg +4 -0
- bat_adk-2025.12/src/bat/__init__.py +0 -0
- bat_adk-2025.12/src/bat/agent/__init__.py +4 -0
- bat_adk-2025.12/src/bat/agent/_executor.py +122 -0
- bat_adk-2025.12/src/bat/agent/application.py +301 -0
- bat_adk-2025.12/src/bat/agent/config.py +466 -0
- bat_adk-2025.12/src/bat/agent/graph.py +330 -0
- bat_adk-2025.12/src/bat/agent/state.py +210 -0
- bat_adk-2025.12/src/bat/chat_model_client/__init__.py +2 -0
- bat_adk-2025.12/src/bat/chat_model_client/client.py +340 -0
- bat_adk-2025.12/src/bat/chat_model_client/config.py +134 -0
- bat_adk-2025.12/src/bat/logging/__init__.py +1 -0
- bat_adk-2025.12/src/bat/logging/logging.py +74 -0
- bat_adk-2025.12/src/bat/prebuilt/__init__.py +2 -0
- bat_adk-2025.12/src/bat/prebuilt/call_agent_node.py +634 -0
- bat_adk-2025.12/src/bat/prebuilt/prebuilt_workflow.py +152 -0
- bat_adk-2025.12/src/bat/prebuilt/react_loop.py +278 -0
- bat_adk-2025.12/src/bat_adk.egg-info/PKG-INFO +70 -0
- bat_adk-2025.12/src/bat_adk.egg-info/SOURCES.txt +23 -0
- bat_adk-2025.12/src/bat_adk.egg-info/dependency_links.txt +1 -0
- bat_adk-2025.12/src/bat_adk.egg-info/requires.txt +13 -0
- bat_adk-2025.12/src/bat_adk.egg-info/top_level.txt +1 -0
bat_adk-2025.12/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bat-adk
|
|
3
|
+
Version: 2025.12
|
|
4
|
+
Summary: Software Development Kit for building AI Agents in BubbleRAN MX-PDK and MX-AI
|
|
5
|
+
Author-email: Andrea LEONE <andrea.leone@bubbleran.com>
|
|
6
|
+
Project-URL: Homepage, https://bubbleran.com/
|
|
7
|
+
Project-URL: Repository, https://github.com/bubbleran/bat
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: a2a-sdk>=0.3.20
|
|
13
|
+
Requires-Dist: a2a-sdk[http-server]
|
|
14
|
+
Requires-Dist: httpx>=0.28.1
|
|
15
|
+
Requires-Dist: langchain>=0.3.24
|
|
16
|
+
Requires-Dist: langchain-mcp-adapters<0.2.0,>=0.1.13
|
|
17
|
+
Requires-Dist: langchain-nvidia-ai-endpoints>=0.3.9
|
|
18
|
+
Requires-Dist: langchain-openai>=0.3.14
|
|
19
|
+
Requires-Dist: langchain-ollama>=0.3.3
|
|
20
|
+
Requires-Dist: langgraph>=1.0.0
|
|
21
|
+
Requires-Dist: mcp[cli]>=1.17.0
|
|
22
|
+
Requires-Dist: pydantic>=2.10.6
|
|
23
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
24
|
+
Requires-Dist: uvicorn>=0.37.0
|
|
25
|
+
|
|
26
|
+
# BubbleRAN Agentic Toolkit - Agent Development Kit (ADK)
|
|
27
|
+
|
|
28
|
+
[](../LICENSE)
|
|
29
|
+
[](https://pypi.org/project/bat-adk/)
|
|
30
|
+
|
|
31
|
+
The **BAT-ADK** is a Python-based Software Development Kit designed to simplify the development, deployment, and integration of AI Agents within the BubbleRAN architecture.
|
|
32
|
+
This repository includes the ADK framework ([BubbleRAN Software License](https://bubbleran.com/resources/files/BubbleRAN_Licence-Agreement-1.3.pdf)).
|
|
33
|
+
|
|
34
|
+
## Key Features
|
|
35
|
+
- 🛠️ Easy-to-use Python SDK for developing AI Agents
|
|
36
|
+
- 🔗 Integrates the [LangGraph](https://pypi.org/project/langgraph/) library with the [A2A SDK](https://pypi.org/project/a2a-sdk/) and [MCP SDK]() for building AI Agents beyond POCs (ready for production)
|
|
37
|
+
- ☁️ Ready for Cloud-Native deployment with BubbleRAN [MX-AI](https://bubbleran.com/products/mx-ai/)
|
|
38
|
+
- 🧩 Prebuilt Agentic Workflow (e.g. ReAct, A2A Communication)
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
### Prerequisites
|
|
43
|
+
- Python 3.12+
|
|
44
|
+
- `uv` (recommended) or `pip`
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
### Using `uv`
|
|
49
|
+
```bash
|
|
50
|
+
uv add bat-adk
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Using `pip`
|
|
54
|
+
```bash
|
|
55
|
+
pip install bat-adk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
The BAT-ADK uses [`pydoc-markdown`](https://pydoc-markdown.readthedocs.io/) to generate API documentation directly from Python docstrings.
|
|
61
|
+
|
|
62
|
+
### Generating the Documentation
|
|
63
|
+
|
|
64
|
+
To build the documentation locally, run:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv run pydoc-markdown
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The generated documentation will be available at `adk/build/docs/content/bat-adk`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# BubbleRAN Agentic Toolkit - Agent Development Kit (ADK)
|
|
2
|
+
|
|
3
|
+
[](../LICENSE)
|
|
4
|
+
[](https://pypi.org/project/bat-adk/)
|
|
5
|
+
|
|
6
|
+
The **BAT-ADK** is a Python-based Software Development Kit designed to simplify the development, deployment, and integration of AI Agents within the BubbleRAN architecture.
|
|
7
|
+
This repository includes the ADK framework ([BubbleRAN Software License](https://bubbleran.com/resources/files/BubbleRAN_Licence-Agreement-1.3.pdf)).
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
- 🛠️ Easy-to-use Python SDK for developing AI Agents
|
|
11
|
+
- 🔗 Integrates the [LangGraph](https://pypi.org/project/langgraph/) library with the [A2A SDK](https://pypi.org/project/a2a-sdk/) and [MCP SDK]() for building AI Agents beyond POCs (ready for production)
|
|
12
|
+
- ☁️ Ready for Cloud-Native deployment with BubbleRAN [MX-AI](https://bubbleran.com/products/mx-ai/)
|
|
13
|
+
- 🧩 Prebuilt Agentic Workflow (e.g. ReAct, A2A Communication)
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
### Prerequisites
|
|
18
|
+
- Python 3.12+
|
|
19
|
+
- `uv` (recommended) or `pip`
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### Using `uv`
|
|
24
|
+
```bash
|
|
25
|
+
uv add bat-adk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Using `pip`
|
|
29
|
+
```bash
|
|
30
|
+
pip install bat-adk
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
34
|
+
|
|
35
|
+
The BAT-ADK uses [`pydoc-markdown`](https://pydoc-markdown.readthedocs.io/) to generate API documentation directly from Python docstrings.
|
|
36
|
+
|
|
37
|
+
### Generating the Documentation
|
|
38
|
+
|
|
39
|
+
To build the documentation locally, run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run pydoc-markdown
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The generated documentation will be available at `adk/build/docs/content/bat-adk`
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bat-adk"
|
|
7
|
+
version = "2025.12"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Andrea LEONE", email="andrea.leone@bubbleran.com" },
|
|
10
|
+
]
|
|
11
|
+
description = "Software Development Kit for building AI Agents in BubbleRAN MX-PDK and MX-AI"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"a2a-sdk>=0.3.20",
|
|
20
|
+
"a2a-sdk[http-server]",
|
|
21
|
+
"httpx>=0.28.1",
|
|
22
|
+
"langchain>=0.3.24",
|
|
23
|
+
"langchain-mcp-adapters>=0.1.13,<0.2.0",
|
|
24
|
+
"langchain-nvidia-ai-endpoints>=0.3.9",
|
|
25
|
+
"langchain-openai>=0.3.14",
|
|
26
|
+
"langchain-ollama>=0.3.3",
|
|
27
|
+
"langgraph>=1.0.0",
|
|
28
|
+
"mcp[cli]>=1.17.0",
|
|
29
|
+
"pydantic>=2.10.6",
|
|
30
|
+
"pyyaml>=6.0.1",
|
|
31
|
+
"uvicorn>=0.37.0",
|
|
32
|
+
]
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"build",
|
|
36
|
+
"twine",
|
|
37
|
+
]
|
|
38
|
+
[project.urls]
|
|
39
|
+
"Homepage" = "https://bubbleran.com/"
|
|
40
|
+
"Repository" = "https://github.com/bubbleran/bat"
|
|
File without changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from ..logging import create_logger
|
|
3
|
+
from .graph import AgentGraph
|
|
4
|
+
from .state import AgentTaskResult
|
|
5
|
+
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
6
|
+
from a2a.server.events import EventQueue
|
|
7
|
+
from a2a.server.tasks import TaskUpdater
|
|
8
|
+
from a2a.types import (
|
|
9
|
+
InternalError,
|
|
10
|
+
InvalidParamsError,
|
|
11
|
+
Part,
|
|
12
|
+
Task,
|
|
13
|
+
TaskState,
|
|
14
|
+
TextPart,
|
|
15
|
+
UnsupportedOperationError,
|
|
16
|
+
)
|
|
17
|
+
from a2a.utils import (
|
|
18
|
+
new_agent_text_message,
|
|
19
|
+
new_task,
|
|
20
|
+
)
|
|
21
|
+
from a2a.utils.errors import ServerError
|
|
22
|
+
from typing import Dict
|
|
23
|
+
from typing_extensions import override, Any
|
|
24
|
+
|
|
25
|
+
_logger = create_logger(__name__, "debug")
|
|
26
|
+
|
|
27
|
+
class MinimalAgentExecutor(AgentExecutor):
|
|
28
|
+
"""Minimal Agent Executor.
|
|
29
|
+
|
|
30
|
+
Minimal implementation of the AgentExecutor interface used by the `AgentApplication` class to execute agent tasks.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
agent_graph: AgentGraph
|
|
36
|
+
):
|
|
37
|
+
self.agent_graph = agent_graph
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
async def execute(
|
|
41
|
+
self,
|
|
42
|
+
context: RequestContext,
|
|
43
|
+
event_queue: EventQueue,
|
|
44
|
+
) -> None:
|
|
45
|
+
if not self._request_ok(context):
|
|
46
|
+
raise ServerError(error=InvalidParamsError())
|
|
47
|
+
|
|
48
|
+
query = context.get_user_input()
|
|
49
|
+
task = context.current_task
|
|
50
|
+
if not task:
|
|
51
|
+
task = new_task(context.message)
|
|
52
|
+
await event_queue.enqueue_event(task)
|
|
53
|
+
updater = TaskUpdater(event_queue, task.id, task.context_id)
|
|
54
|
+
try:
|
|
55
|
+
config = {"configurable": {"thread_id": task.context_id}}
|
|
56
|
+
ts = time.time()
|
|
57
|
+
keep_streaming = True
|
|
58
|
+
async for item in self.agent_graph.astream(query, config):
|
|
59
|
+
if keep_streaming:
|
|
60
|
+
usage_metadata = self.agent_graph._get_usage_metadata(ts)
|
|
61
|
+
ts = time.time()
|
|
62
|
+
keep_streaming = await self._process_task_result(task, item, updater, {'usage': usage_metadata})
|
|
63
|
+
else:
|
|
64
|
+
# TODO: add chunk status
|
|
65
|
+
_logger.warning("Artifact has been updated: ignoring additional streamed item.")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
_logger.error(f'An error occurred while streaming the response: {e}')
|
|
68
|
+
raise ServerError(error=InternalError()) from e
|
|
69
|
+
|
|
70
|
+
def _request_ok(self, context: RequestContext) -> bool:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
async def _process_task_result(
|
|
74
|
+
self,
|
|
75
|
+
task: Task,
|
|
76
|
+
task_result: AgentTaskResult,
|
|
77
|
+
updater: TaskUpdater,
|
|
78
|
+
metadata: Dict[str, Any]
|
|
79
|
+
) -> bool:
|
|
80
|
+
keep_streaming = True
|
|
81
|
+
match task_result.task_status:
|
|
82
|
+
case "working":
|
|
83
|
+
message = new_agent_text_message(
|
|
84
|
+
task_result.content,
|
|
85
|
+
task.context_id,
|
|
86
|
+
task.id,
|
|
87
|
+
)
|
|
88
|
+
await updater.update_status(
|
|
89
|
+
TaskState.working,
|
|
90
|
+
message,
|
|
91
|
+
metadata=metadata,
|
|
92
|
+
)
|
|
93
|
+
case "input-required":
|
|
94
|
+
message = new_agent_text_message(
|
|
95
|
+
task_result.content,
|
|
96
|
+
task.context_id,
|
|
97
|
+
task.id,
|
|
98
|
+
)
|
|
99
|
+
await updater.update_status(
|
|
100
|
+
TaskState.input_required,
|
|
101
|
+
message,
|
|
102
|
+
metadata=metadata,
|
|
103
|
+
final=True,
|
|
104
|
+
)
|
|
105
|
+
keep_streaming = False
|
|
106
|
+
case "completed":
|
|
107
|
+
await updater.add_artifact(
|
|
108
|
+
[Part(root=TextPart(text=task_result.content))],
|
|
109
|
+
metadata=metadata,
|
|
110
|
+
)
|
|
111
|
+
keep_streaming = False
|
|
112
|
+
case "error":
|
|
113
|
+
raise ServerError(error=InternalError(message=task_result.content))
|
|
114
|
+
case _:
|
|
115
|
+
_logger.warning(f"Unknown task status: {task_result.task_status}")
|
|
116
|
+
return keep_streaming
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
async def cancel(
|
|
120
|
+
self, request: RequestContext, event_queue: EventQueue
|
|
121
|
+
) -> Task | None:
|
|
122
|
+
raise ServerError(error=UnsupportedOperationError())
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import httpx
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
6
|
+
import uvicorn
|
|
7
|
+
from ..logging import create_logger
|
|
8
|
+
from ._executor import MinimalAgentExecutor
|
|
9
|
+
from .config import AgentConfig
|
|
10
|
+
from .graph import AgentGraph
|
|
11
|
+
from .state import AgentState
|
|
12
|
+
from a2a.client import ClientConfig, ClientFactory
|
|
13
|
+
from a2a.server.apps import A2AStarletteApplication
|
|
14
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
15
|
+
from a2a.server.tasks import InMemoryTaskStore
|
|
16
|
+
from a2a.types import AgentCard, Message, TextPart
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
from jsonschema import ValidationError
|
|
19
|
+
from mcp.server import FastMCP
|
|
20
|
+
from starlette.applications import Starlette
|
|
21
|
+
from threading import Thread
|
|
22
|
+
from typing import Optional, Type
|
|
23
|
+
|
|
24
|
+
load_dotenv()
|
|
25
|
+
_logger = create_logger(__name__, "debug")
|
|
26
|
+
|
|
27
|
+
A2A_APPLICATION_DEFAULT_PORT = 9900
|
|
28
|
+
MCP_APPLICATION_DEFAULT_PORT = 9800
|
|
29
|
+
DEFAULT_HTTPX_CLIENT_TIMEOUT = 180
|
|
30
|
+
|
|
31
|
+
class AgentApplication:
|
|
32
|
+
f"""Agent Application based on `Starlette`.
|
|
33
|
+
This class sets up an agent application that can handle A2A and MCP protocols.
|
|
34
|
+
Supported Environment Variables:
|
|
35
|
+
- `URL` (required): The base URL where the agent will be hosted.
|
|
36
|
+
- `PORT`: The port for the A2A application. Defaults to `{A2A_APPLICATION_DEFAULT_PORT}`.
|
|
37
|
+
- `MCP_PORT`: The port for the MCP application. Defaults to `{MCP_APPLICATION_DEFAULT_PORT}`.
|
|
38
|
+
- `CONFIG`: Path to a configuration file for the agent. Defaults to _"config.yaml"_.
|
|
39
|
+
|
|
40
|
+
Attributes
|
|
41
|
+
-------
|
|
42
|
+
agent_card (AgentCard): The agent card containing metadata about the agent.
|
|
43
|
+
agent_graph (AgentGraph): The agent graph that defines the agent's behavior and capabilities.
|
|
44
|
+
|
|
45
|
+
Example
|
|
46
|
+
-------
|
|
47
|
+
```python
|
|
48
|
+
from bat.agent import AgentApplication
|
|
49
|
+
|
|
50
|
+
agent = AgentApplication(
|
|
51
|
+
agent_card_path='./agent.json',
|
|
52
|
+
agent_graph=MyAgentGraph(),
|
|
53
|
+
)
|
|
54
|
+
agent.run()
|
|
55
|
+
```
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
AgentGraphType: Type[AgentGraph],
|
|
61
|
+
AgentStateType: Type[AgentState],
|
|
62
|
+
agent_card_path: str = './agent.json',
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Initialize the AgentApplication with the given agent card path and agent graph.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
agent_graph (AgentGraph): The agent graph implementing the agent's logic.
|
|
69
|
+
agent_card_path (str): The path to the agent card JSON file. Defaults to _"./agent.json"_.
|
|
70
|
+
"""
|
|
71
|
+
self.a2a_port = int(os.getenv("PORT", A2A_APPLICATION_DEFAULT_PORT))
|
|
72
|
+
self.mcp_port = int(os.getenv("MCP_PORT", MCP_APPLICATION_DEFAULT_PORT))
|
|
73
|
+
|
|
74
|
+
self._agent_card = self.load_agent_card(agent_card_path)
|
|
75
|
+
self._config_path = os.getenv("CONFIG", "config.yaml")
|
|
76
|
+
self._config = AgentConfig.load(self._config_path)
|
|
77
|
+
|
|
78
|
+
self._AgentStateType = AgentStateType
|
|
79
|
+
self._AgentGraphType = AgentGraphType
|
|
80
|
+
agent_graph = AgentGraphType(
|
|
81
|
+
config=self._config,
|
|
82
|
+
StateType=AgentStateType,
|
|
83
|
+
)
|
|
84
|
+
self._agent_executor = MinimalAgentExecutor(agent_graph)
|
|
85
|
+
self._request_handler = DefaultRequestHandler(
|
|
86
|
+
agent_executor=self._agent_executor,
|
|
87
|
+
task_store=InMemoryTaskStore(),
|
|
88
|
+
)
|
|
89
|
+
self._a2a_server = A2AStarletteApplication(
|
|
90
|
+
agent_card=self._agent_card,
|
|
91
|
+
http_handler=self._request_handler
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def load_agent_card(
|
|
95
|
+
self,
|
|
96
|
+
agent_card_path: str,
|
|
97
|
+
) -> AgentCard:
|
|
98
|
+
"""Load the Agent Card from a JSON file.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
agent_card_path (str): The path to the Agent Card JSON file.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
AgentCard: The loaded Agent Card.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
Exception: For general errors during loading.
|
|
108
|
+
EnvironmentError: If the URL environment variable is not set.
|
|
109
|
+
FileNotFoundError: If the agent card file does not exist.
|
|
110
|
+
ValidationError: If the agent card JSON is invalid.
|
|
111
|
+
"""
|
|
112
|
+
url = os.getenv("URL")
|
|
113
|
+
if url is None:
|
|
114
|
+
_logger.error("URL environment variable is not set.")
|
|
115
|
+
raise EnvironmentError("URL environment variable is not set.")
|
|
116
|
+
if not url.startswith("http://") and not url.startswith("https://"):
|
|
117
|
+
url = "http://" + url
|
|
118
|
+
url = url.rstrip("/")
|
|
119
|
+
port = int(os.getenv("PORT", A2A_APPLICATION_DEFAULT_PORT))
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
with open(agent_card_path, 'r') as file:
|
|
123
|
+
agent_data = json.load(file)
|
|
124
|
+
agent_data.setdefault('url', f'{url}:{port}')
|
|
125
|
+
agent_card = AgentCard.model_validate(agent_data)
|
|
126
|
+
_logger.debug('Agent Card loaded.')
|
|
127
|
+
except FileNotFoundError as e:
|
|
128
|
+
raise FileNotFoundError(f'Agent card file not found.') from e
|
|
129
|
+
except ValidationError as e:
|
|
130
|
+
raise ValidationError(f'Invalid agent card format.') from e
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise Exception(f'Error loading agent card: {e}') from e
|
|
133
|
+
|
|
134
|
+
return agent_card
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def agent_graph(self) -> AgentGraph:
|
|
138
|
+
"""Get the agent graph."""
|
|
139
|
+
return self._agent_executor.agent_graph
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def agent_card(self) -> AgentCard:
|
|
143
|
+
"""Get the agent card."""
|
|
144
|
+
return self._agent_card
|
|
145
|
+
|
|
146
|
+
def _build_a2a_application(self) -> Starlette:
|
|
147
|
+
"""Build the A2A Starlette application.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Starlette: The built Starlette application.
|
|
151
|
+
"""
|
|
152
|
+
return self._a2a_server.build()
|
|
153
|
+
|
|
154
|
+
def _build_mcp_application(self) -> FastMCP:
|
|
155
|
+
mcp = FastMCP(
|
|
156
|
+
name=self.agent_card.name,
|
|
157
|
+
host="0.0.0.0",
|
|
158
|
+
port=self.mcp_port,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@mcp.tool(
|
|
162
|
+
name=f"get_{self.agent_card.name.lower().replace(' ', '_')}_card",
|
|
163
|
+
)
|
|
164
|
+
def get_agent_card() -> str:
|
|
165
|
+
"""
|
|
166
|
+
Get the Agent Card as a JSON string, i.e. a description of the Agent and its capabilities.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
str: The Agent Card in JSON format.
|
|
170
|
+
"""
|
|
171
|
+
return self.agent_card.model_dump_json()
|
|
172
|
+
|
|
173
|
+
@mcp.tool(
|
|
174
|
+
name=f"call_{self.agent_card.name.lower().replace(' ', '_')}",
|
|
175
|
+
)
|
|
176
|
+
def call_agent(
|
|
177
|
+
query: str,
|
|
178
|
+
context_id: Optional[str] = None,
|
|
179
|
+
message_id: str = "1",
|
|
180
|
+
) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Call the Agent with a query and return the response.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
query (str): The input query for the Agent.
|
|
186
|
+
context_id (Optional[str]): The context ID for the conversation. Defaults to None.
|
|
187
|
+
If None, a random context ID will be generated calling `uuid.uuid4()`.
|
|
188
|
+
message_id (str): The message ID in the conversation. Defaults to "1".
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
str: The Agent's response.
|
|
192
|
+
"""
|
|
193
|
+
async def get_response_from_stream() -> str:
|
|
194
|
+
client_factory = ClientFactory(
|
|
195
|
+
config=ClientConfig(
|
|
196
|
+
streaming=False,
|
|
197
|
+
httpx_client=httpx.AsyncClient(timeout=DEFAULT_HTTPX_CLIENT_TIMEOUT),
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
client = client_factory.create(card=self.agent_card)
|
|
201
|
+
message = Message(
|
|
202
|
+
context_id=context_id or str(uuid.uuid4()),
|
|
203
|
+
message_id=message_id,
|
|
204
|
+
role="user",
|
|
205
|
+
parts=[TextPart(text=query)]
|
|
206
|
+
)
|
|
207
|
+
stream = client.send_message(message)
|
|
208
|
+
item = await anext(stream)
|
|
209
|
+
|
|
210
|
+
response = None
|
|
211
|
+
if isinstance(item, Message):
|
|
212
|
+
if item.parts and item.parts[0].root.kind == "text":
|
|
213
|
+
response = item.parts[0].root.text
|
|
214
|
+
else:
|
|
215
|
+
_logger.warning("Received Message with non-text part; ignoring.")
|
|
216
|
+
else:
|
|
217
|
+
task = item[0]
|
|
218
|
+
if task.artifacts:
|
|
219
|
+
artifact = task.artifacts[0]
|
|
220
|
+
if artifact.parts and artifact.parts[0].root.kind == "text":
|
|
221
|
+
response = artifact.parts[0].root.text
|
|
222
|
+
else:
|
|
223
|
+
_logger.warning("Received Artifact with non-text part; ignoring.")
|
|
224
|
+
|
|
225
|
+
if response is None:
|
|
226
|
+
response = "No valid response received."
|
|
227
|
+
_logger.warning("No valid response was obtained from the agent stream.")
|
|
228
|
+
return response
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
result = {}
|
|
232
|
+
def runner(coro):
|
|
233
|
+
try:
|
|
234
|
+
loop = asyncio.new_event_loop()
|
|
235
|
+
asyncio.set_event_loop(loop)
|
|
236
|
+
result["value"] = loop.run_until_complete(coro)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
result["error"] = e
|
|
239
|
+
finally:
|
|
240
|
+
pending = asyncio.all_tasks(loop)
|
|
241
|
+
for task in pending:
|
|
242
|
+
task.cancel()
|
|
243
|
+
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
244
|
+
loop.close()
|
|
245
|
+
|
|
246
|
+
t = Thread(
|
|
247
|
+
target=runner,
|
|
248
|
+
args=(get_response_from_stream(),),
|
|
249
|
+
)
|
|
250
|
+
t.start()
|
|
251
|
+
t.join()
|
|
252
|
+
|
|
253
|
+
if "error" in result:
|
|
254
|
+
raise result["error"]
|
|
255
|
+
response = result["value"]
|
|
256
|
+
except Exception as e:
|
|
257
|
+
_logger.error(f"Error while getting response from Agent: {e}")
|
|
258
|
+
response = f"An error occurred while processing your request: {e}"
|
|
259
|
+
return response
|
|
260
|
+
|
|
261
|
+
return mcp
|
|
262
|
+
|
|
263
|
+
def run(
|
|
264
|
+
self,
|
|
265
|
+
expose_mcp: bool = False,
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Run the agent application.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
expose_mcp (bool, optional): Whether to expose the MCP protocol. Defaults to False.
|
|
271
|
+
**This parameter isn't fully supported yet and may lead to unexpected behavior
|
|
272
|
+
when set to True.**
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
if expose_mcp:
|
|
276
|
+
a2a_app = self._build_a2a_application()
|
|
277
|
+
mcp_app = self._build_mcp_application()
|
|
278
|
+
|
|
279
|
+
a2a_server_config = uvicorn.Config(
|
|
280
|
+
app=a2a_app,
|
|
281
|
+
host="0.0.0.0",
|
|
282
|
+
port=self.a2a_port,
|
|
283
|
+
reload=False,
|
|
284
|
+
)
|
|
285
|
+
a2a_server = uvicorn.Server(config=a2a_server_config)
|
|
286
|
+
|
|
287
|
+
t_a2a = Thread(target=lambda: a2a_server.run())
|
|
288
|
+
t_mcp = Thread(target=lambda: asyncio.run(mcp_app.run_streamable_http_async()))
|
|
289
|
+
|
|
290
|
+
t_a2a.start()
|
|
291
|
+
t_mcp.start()
|
|
292
|
+
t_mcp.join()
|
|
293
|
+
t_a2a.join()
|
|
294
|
+
else:
|
|
295
|
+
a2a_app = self._build_a2a_application()
|
|
296
|
+
uvicorn.run(
|
|
297
|
+
app=a2a_app,
|
|
298
|
+
host="0.0.0.0",
|
|
299
|
+
port=self.a2a_port,
|
|
300
|
+
reload=False,
|
|
301
|
+
)
|