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.
@@ -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](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](../LICENSE)
29
+ [![PyPI version](https://img.shields.io/pypi/v/bat-adk)](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](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](../LICENSE)
4
+ [![PyPI version](https://img.shields.io/pypi/v/bat-adk)](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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,4 @@
1
+ from .application import AgentApplication
2
+ from .config import AgentConfig
3
+ from .graph import AgentGraph
4
+ from .state import AgentState, AgentTaskResult
@@ -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
+ )