basion-agent 0.4.0__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.
- basion_agent/__init__.py +62 -0
- basion_agent/agent.py +360 -0
- basion_agent/agent_state_client.py +149 -0
- basion_agent/app.py +502 -0
- basion_agent/artifact.py +58 -0
- basion_agent/attachment_client.py +153 -0
- basion_agent/checkpoint_client.py +169 -0
- basion_agent/checkpointer.py +16 -0
- basion_agent/cli.py +139 -0
- basion_agent/conversation.py +103 -0
- basion_agent/conversation_client.py +86 -0
- basion_agent/conversation_message.py +48 -0
- basion_agent/exceptions.py +36 -0
- basion_agent/extensions/__init__.py +1 -0
- basion_agent/extensions/langgraph.py +526 -0
- basion_agent/extensions/pydantic_ai.py +180 -0
- basion_agent/gateway_client.py +531 -0
- basion_agent/gateway_pb2.py +73 -0
- basion_agent/gateway_pb2_grpc.py +101 -0
- basion_agent/heartbeat.py +84 -0
- basion_agent/loki_handler.py +355 -0
- basion_agent/memory.py +73 -0
- basion_agent/memory_client.py +155 -0
- basion_agent/message.py +333 -0
- basion_agent/py.typed +0 -0
- basion_agent/streamer.py +184 -0
- basion_agent/structural/__init__.py +6 -0
- basion_agent/structural/artifact.py +94 -0
- basion_agent/structural/base.py +71 -0
- basion_agent/structural/stepper.py +125 -0
- basion_agent/structural/surface.py +90 -0
- basion_agent/structural/text_block.py +96 -0
- basion_agent/tools/__init__.py +19 -0
- basion_agent/tools/container.py +46 -0
- basion_agent/tools/knowledge_graph.py +306 -0
- basion_agent-0.4.0.dist-info/METADATA +880 -0
- basion_agent-0.4.0.dist-info/RECORD +41 -0
- basion_agent-0.4.0.dist-info/WHEEL +5 -0
- basion_agent-0.4.0.dist-info/entry_points.txt +2 -0
- basion_agent-0.4.0.dist-info/licenses/LICENSE +21 -0
- basion_agent-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: basion-agent
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Python SDK for Basion AI Agent framework - handles agent registration, message consumption, and streaming responses
|
|
5
|
+
Author-email: Basion AI <support@basion.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/basion-us/basion-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/basion-us/basion-agent
|
|
9
|
+
Project-URL: Documentation, https://github.com/basion-us/basion-agent#readme
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: grpcio>=1.70.0
|
|
22
|
+
Requires-Dist: grpcio-tools>=1.70.0
|
|
23
|
+
Requires-Dist: protobuf>=5.29.0
|
|
24
|
+
Requires-Dist: requests>=2.31.0
|
|
25
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-mock>=3.11.0; extra == "dev"
|
|
31
|
+
Requires-Dist: respx>=0.20.0; extra == "dev"
|
|
32
|
+
Provides-Extra: langgraph
|
|
33
|
+
Requires-Dist: langgraph>=1.0.0; extra == "langgraph"
|
|
34
|
+
Requires-Dist: langchain-core>=0.3.0; extra == "langgraph"
|
|
35
|
+
Provides-Extra: pydantic
|
|
36
|
+
Requires-Dist: pydantic-ai>=1.0.0; extra == "pydantic"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# Basion Agent SDK
|
|
40
|
+
|
|
41
|
+
Python SDK for building AI agents in the Basion AI platform. Provides agent registration, message handling via Kafka (through Agent Gateway), streaming responses, and integrations with LangGraph and Pydantic AI.
|
|
42
|
+
|
|
43
|
+
## Overview
|
|
44
|
+
|
|
45
|
+
The Basion Agent SDK (`basion-agent`) enables developers to build AI agents that integrate with the Basion AI Management platform. Agents register themselves, receive messages through Kafka topics, and stream responses back to users.
|
|
46
|
+
|
|
47
|
+
### Key Features
|
|
48
|
+
|
|
49
|
+
- **Agent Registration**: Automatic registration with AI Inventory
|
|
50
|
+
- **Message Handling**: Decorator-based handlers with sender filtering
|
|
51
|
+
- **Response Streaming**: Chunked responses via Kafka/Centrifugo
|
|
52
|
+
- **Structural Streaming**: Rich UI components (Artifacts, Surfaces, TextBlocks, Steppers)
|
|
53
|
+
- **Conversation History**: Access to message history via Conversation Store
|
|
54
|
+
- **Memory**: Semantic search over long-term user and conversation memory
|
|
55
|
+
- **Attachments**: Download and process file attachments (images, PDFs, etc.)
|
|
56
|
+
- **Knowledge Graph**: Query biomedical knowledge graphs (diseases, proteins, phenotypes)
|
|
57
|
+
- **Remote Logging**: Send logs to Loki via the gateway for centralized monitoring
|
|
58
|
+
- **LangGraph Integration**: HTTP-based checkpoint saver for LangGraph graphs
|
|
59
|
+
- **Pydantic AI Integration**: Persistent message history for Pydantic AI agents
|
|
60
|
+
- **CLI**: Run agents with `basion-agent run main:app`
|
|
61
|
+
- **Error Handling**: Automatic error responses to users on handler failures
|
|
62
|
+
|
|
63
|
+
## Architecture
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
67
|
+
│ Your Agent Application │
|
|
68
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
69
|
+
│ │
|
|
70
|
+
│ BasionAgentApp │
|
|
71
|
+
│ ├── register_me() → Agent │
|
|
72
|
+
│ │ ├── @on_message decorator │
|
|
73
|
+
│ │ ├── streamer() → Streamer │
|
|
74
|
+
│ │ │ └── stream_by() → Structural (Artifact, Surface, TextBlock, etc) │
|
|
75
|
+
│ │ └── tools → Tools │
|
|
76
|
+
│ │ └── knowledge_graph → KnowledgeGraphTool │
|
|
77
|
+
│ └── run() → Start consume loop │
|
|
78
|
+
│ │
|
|
79
|
+
│ Message Context: │
|
|
80
|
+
│ ├── message.conversation → Conversation (history, metadata) │
|
|
81
|
+
│ ├── message.memory → Memory (semantic search) │
|
|
82
|
+
│ └── message.attachments → List[AttachmentInfo] (file downloads) │
|
|
83
|
+
│ │
|
|
84
|
+
│ Extensions: │
|
|
85
|
+
│ ├── HTTPCheckpointSaver (LangGraph) │
|
|
86
|
+
│ └── PydanticAIMessageStore (Pydantic AI) │
|
|
87
|
+
│ │
|
|
88
|
+
└──────────────────────────────────┬──────────────────────────────────────────┘
|
|
89
|
+
│ gRPC (Kafka) + HTTP (APIs)
|
|
90
|
+
▼
|
|
91
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
92
|
+
│ Agent Gateway │
|
|
93
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
94
|
+
│ gRPC: AgentStream (bidirectional) HTTP: /s/{service}/* proxy │
|
|
95
|
+
│ - Auth - /s/ai-inventory/* │
|
|
96
|
+
│ - Subscribe/Unsubscribe - /s/conversation-store/* │
|
|
97
|
+
│ - Produce/Consume messages - /s/ai-memory/* │
|
|
98
|
+
│ - /s/attachment/* │
|
|
99
|
+
│ - /s/knowledge-graph/* │
|
|
100
|
+
│ - /loki/api/v1/push (logging) │
|
|
101
|
+
└─────────────────┬───────────────────────────────────┬───────────────────────┘
|
|
102
|
+
│ │
|
|
103
|
+
▼ ▼
|
|
104
|
+
┌──────────────┐ ┌────────────────────┐
|
|
105
|
+
│ Kafka │ │ AI Inventory / │
|
|
106
|
+
│ {agent}.inbox │ │ Conversation Store │
|
|
107
|
+
└──────────────┘ │ AI Memory / KG │
|
|
108
|
+
└────────────────────┘
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Message Flow
|
|
112
|
+
|
|
113
|
+
```mermaid
|
|
114
|
+
sequenceDiagram
|
|
115
|
+
participant User
|
|
116
|
+
participant Provider
|
|
117
|
+
participant Router
|
|
118
|
+
participant Gateway as Agent Gateway
|
|
119
|
+
participant Agent as Your Agent
|
|
120
|
+
participant ConvStore as Conversation Store
|
|
121
|
+
|
|
122
|
+
User->>Provider: Send message
|
|
123
|
+
Provider->>Router: Kafka: router.inbox
|
|
124
|
+
Router->>Gateway: Kafka: {agent}.inbox
|
|
125
|
+
Gateway->>Agent: gRPC stream: message
|
|
126
|
+
|
|
127
|
+
Agent->>ConvStore: Get conversation history
|
|
128
|
+
ConvStore-->>Agent: Message history
|
|
129
|
+
|
|
130
|
+
loop Streaming Response
|
|
131
|
+
Agent->>Gateway: gRPC stream: content chunk
|
|
132
|
+
Gateway->>Router: Kafka: router.inbox
|
|
133
|
+
Router->>Provider: Kafka: user.inbox
|
|
134
|
+
Provider->>User: WebSocket: chunk
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
Agent->>Gateway: gRPC stream: done=true
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Installation
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Basic installation
|
|
144
|
+
pip install basion-agent
|
|
145
|
+
|
|
146
|
+
# With LangGraph support
|
|
147
|
+
pip install basion-agent[langgraph]
|
|
148
|
+
|
|
149
|
+
# With Pydantic AI support
|
|
150
|
+
pip install basion-agent[pydantic]
|
|
151
|
+
|
|
152
|
+
# Development installation
|
|
153
|
+
pip install -e ".[dev]"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Quick Start
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from basion_agent import BasionAgentApp
|
|
160
|
+
|
|
161
|
+
# Initialize the app
|
|
162
|
+
app = BasionAgentApp(
|
|
163
|
+
gateway_url="agent-gateway:8080",
|
|
164
|
+
api_key="your-api-key"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Register an agent
|
|
168
|
+
agent = app.register_me(
|
|
169
|
+
name="my-assistant",
|
|
170
|
+
about="A helpful AI assistant",
|
|
171
|
+
document="Answers general questions and provides helpful information.",
|
|
172
|
+
representation_name="My Assistant"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Handle messages
|
|
176
|
+
@agent.on_message
|
|
177
|
+
async def handle_message(message, sender):
|
|
178
|
+
# Access conversation history
|
|
179
|
+
history = await message.conversation.get_history(limit=10)
|
|
180
|
+
|
|
181
|
+
# Stream response
|
|
182
|
+
async with agent.streamer(message) as s:
|
|
183
|
+
s.stream("Hello! ")
|
|
184
|
+
s.stream("How can I help you today?")
|
|
185
|
+
|
|
186
|
+
# Run the agent
|
|
187
|
+
app.run()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## CLI
|
|
191
|
+
|
|
192
|
+
Run agents using uvicorn-style import strings:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Run 'app' from main.py
|
|
196
|
+
basion-agent run main:app
|
|
197
|
+
|
|
198
|
+
# Run 'app' from main.py (defaults to :app)
|
|
199
|
+
basion-agent run main
|
|
200
|
+
|
|
201
|
+
# Run 'application' from myagent.py
|
|
202
|
+
basion-agent run myagent:application
|
|
203
|
+
|
|
204
|
+
# Show version
|
|
205
|
+
basion-agent version
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Your agent file should define a `BasionAgentApp` instance:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# main.py
|
|
212
|
+
app = BasionAgentApp(gateway_url="...", api_key="...")
|
|
213
|
+
agent = app.register_me(name="my-agent", ...)
|
|
214
|
+
|
|
215
|
+
@agent.on_message
|
|
216
|
+
async def handle(message, sender):
|
|
217
|
+
...
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Configuration
|
|
221
|
+
|
|
222
|
+
### Environment Variables
|
|
223
|
+
|
|
224
|
+
| Variable | Description | Default |
|
|
225
|
+
|----------|-------------|---------|
|
|
226
|
+
| `GATEWAY_URL` | Agent Gateway endpoint | Required |
|
|
227
|
+
| `GATEWAY_API_KEY` | API key for authentication | Required |
|
|
228
|
+
|
|
229
|
+
### BasionAgentApp Options
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
app = BasionAgentApp(
|
|
233
|
+
gateway_url="agent-gateway:8080", # Gateway endpoint
|
|
234
|
+
api_key="key", # Authentication key
|
|
235
|
+
heartbeat_interval=60, # Heartbeat frequency (seconds)
|
|
236
|
+
max_concurrent_tasks=100, # Max concurrent message handlers
|
|
237
|
+
error_message_template="...", # Error message sent to users
|
|
238
|
+
secure=False, # Use TLS for gRPC and HTTPS for HTTP
|
|
239
|
+
enable_remote_logging=False, # Send logs to Loki via gateway
|
|
240
|
+
remote_log_level=logging.INFO, # Min log level for remote logging
|
|
241
|
+
remote_log_batch_size=100, # Logs per batch
|
|
242
|
+
remote_log_flush_interval=5.0, # Seconds between flushes
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## API Reference
|
|
247
|
+
|
|
248
|
+
### BasionAgentApp
|
|
249
|
+
|
|
250
|
+
Main application class for initializing and running agents.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
app = BasionAgentApp(gateway_url, api_key)
|
|
254
|
+
|
|
255
|
+
# Register an agent
|
|
256
|
+
agent = app.register_me(
|
|
257
|
+
name="agent-name", # Unique identifier (used for routing)
|
|
258
|
+
about="Short description", # Brief description for agent selection
|
|
259
|
+
document="Full docs...", # Detailed documentation
|
|
260
|
+
representation_name="Name", # Display name (optional)
|
|
261
|
+
metadata={"key": "value"}, # Additional metadata (optional)
|
|
262
|
+
category_name="my-category", # Category in kebab-case (optional, auto-created)
|
|
263
|
+
tag_names=["tag-1", "tag-2"],# Tags in kebab-case (optional, auto-created)
|
|
264
|
+
example_prompts=["Ask me anything"], # Example prompts for users (optional)
|
|
265
|
+
is_experimental=False, # Mark as experimental (optional)
|
|
266
|
+
force_update=False, # Bypass content hash check (optional)
|
|
267
|
+
base_url="http://...", # Base URL for agent's frontend service (optional)
|
|
268
|
+
related_pages=[ # Related pages (optional)
|
|
269
|
+
{"name": "Docs", "endpoint": "/docs"}
|
|
270
|
+
],
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Start consuming messages
|
|
274
|
+
app.run() # Blocks until shutdown
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Agent
|
|
278
|
+
|
|
279
|
+
Handles message registration and response streaming.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
# Register message handler (all senders)
|
|
283
|
+
@agent.on_message
|
|
284
|
+
async def handle(message, sender):
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
# Filter by sender
|
|
288
|
+
@agent.on_message(senders=["user"])
|
|
289
|
+
async def handle_user(message, sender):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
# Exclude sender
|
|
293
|
+
@agent.on_message(senders=["~other-agent"])
|
|
294
|
+
async def handle_not_other(message, sender):
|
|
295
|
+
pass
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Message
|
|
299
|
+
|
|
300
|
+
Represents an incoming message with conversation context, memory, and attachments.
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
@agent.on_message
|
|
304
|
+
async def handle(message, sender):
|
|
305
|
+
message.content # Message content
|
|
306
|
+
message.conversation_id # Conversation ID
|
|
307
|
+
message.user_id # User ID
|
|
308
|
+
message.metadata # Optional message metadata (dict)
|
|
309
|
+
message.schema # Optional message schema (dict)
|
|
310
|
+
|
|
311
|
+
# Conversation history
|
|
312
|
+
history = await message.conversation.get_history(limit=10)
|
|
313
|
+
|
|
314
|
+
# Memory (semantic search)
|
|
315
|
+
results = await message.memory.query_about_user("diagnosis", limit=5)
|
|
316
|
+
|
|
317
|
+
# Attachments
|
|
318
|
+
if message.has_attachments():
|
|
319
|
+
count = message.get_attachment_count()
|
|
320
|
+
attachments = message.get_attachments()
|
|
321
|
+
|
|
322
|
+
# Download first attachment
|
|
323
|
+
data = await message.get_attachment_bytes()
|
|
324
|
+
base64_str = await message.get_attachment_base64()
|
|
325
|
+
buffer = await message.get_attachment_buffer()
|
|
326
|
+
|
|
327
|
+
# Download specific attachment by index
|
|
328
|
+
data = await message.get_attachment_bytes_at(1)
|
|
329
|
+
|
|
330
|
+
# Download all attachments at once
|
|
331
|
+
all_bytes = await message.get_all_attachment_bytes()
|
|
332
|
+
all_base64 = await message.get_all_attachment_base64()
|
|
333
|
+
|
|
334
|
+
# Inspect attachment metadata
|
|
335
|
+
for att in attachments:
|
|
336
|
+
att.filename # "document.pdf"
|
|
337
|
+
att.content_type # "application/pdf"
|
|
338
|
+
att.size # bytes
|
|
339
|
+
att.url # download URL
|
|
340
|
+
att.file_extension # "pdf"
|
|
341
|
+
att.file_type # "pdf"
|
|
342
|
+
att.is_image() # True/False
|
|
343
|
+
att.is_pdf() # True/False
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Conversation
|
|
347
|
+
|
|
348
|
+
Access conversation history and metadata.
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
@agent.on_message
|
|
352
|
+
async def handle(message, sender):
|
|
353
|
+
conv = message.conversation
|
|
354
|
+
|
|
355
|
+
# Get message history
|
|
356
|
+
history = await conv.get_history(limit=10)
|
|
357
|
+
history = await conv.get_history(role="user", limit=20, offset=0)
|
|
358
|
+
|
|
359
|
+
# Get conversation metadata
|
|
360
|
+
metadata = await conv.get_metadata()
|
|
361
|
+
|
|
362
|
+
# Get messages where this agent was sender or recipient
|
|
363
|
+
agent_history = await conv.get_agent_history(limit=50)
|
|
364
|
+
agent_history = await conv.get_agent_history(agent_name="other-agent")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Memory
|
|
368
|
+
|
|
369
|
+
Semantic search over long-term user and conversation memory. Accessed via `message.memory`.
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
@agent.on_message
|
|
373
|
+
async def handle(message, sender):
|
|
374
|
+
mem = message.memory
|
|
375
|
+
|
|
376
|
+
# Search user's long-term memory
|
|
377
|
+
results = await mem.query_about_user(
|
|
378
|
+
query="previous diagnosis",
|
|
379
|
+
limit=10, # Max results (1-100)
|
|
380
|
+
threshold=70, # Similarity threshold 0-100
|
|
381
|
+
context_messages=2, # Surrounding messages to include (0-20)
|
|
382
|
+
)
|
|
383
|
+
for r in results:
|
|
384
|
+
r.message.content # Matched message content
|
|
385
|
+
r.score # Similarity score
|
|
386
|
+
r.context # List of surrounding MemoryMessage objects
|
|
387
|
+
|
|
388
|
+
# Search conversation memory
|
|
389
|
+
results = await mem.query_about_conversation(
|
|
390
|
+
query="what was discussed",
|
|
391
|
+
limit=5,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Get user summary (aggregated across all conversations)
|
|
395
|
+
summary = await mem.get_user_summary()
|
|
396
|
+
if summary:
|
|
397
|
+
summary.text # Summary text
|
|
398
|
+
summary.message_count # Total messages
|
|
399
|
+
summary.last_updated # Timestamp
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Streamer
|
|
403
|
+
|
|
404
|
+
Streams response chunks back to the user (or another agent).
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
@agent.on_message
|
|
408
|
+
async def handle(message, sender):
|
|
409
|
+
# Basic streaming (auto-finishes on exit)
|
|
410
|
+
async with agent.streamer(message) as s:
|
|
411
|
+
s.stream("Chunk 1...")
|
|
412
|
+
s.stream("Chunk 2...")
|
|
413
|
+
|
|
414
|
+
# Streaming with options
|
|
415
|
+
async with agent.streamer(
|
|
416
|
+
message,
|
|
417
|
+
send_to="user", # or another agent name
|
|
418
|
+
awaiting=True, # Set awaiting_route to this agent
|
|
419
|
+
) as s:
|
|
420
|
+
# Non-persisted content (not saved to DB)
|
|
421
|
+
s.stream("Thinking...", persist=False, event_type="thinking")
|
|
422
|
+
|
|
423
|
+
# Persisted content
|
|
424
|
+
s.stream("Here's my response...")
|
|
425
|
+
|
|
426
|
+
# write() is an alias for stream()
|
|
427
|
+
s.write("More content...")
|
|
428
|
+
|
|
429
|
+
# Set metadata on the message
|
|
430
|
+
s.set_message_metadata({"source": "search"})
|
|
431
|
+
|
|
432
|
+
# Set response schema for forms
|
|
433
|
+
s.set_response_schema({
|
|
434
|
+
"type": "object",
|
|
435
|
+
"properties": {
|
|
436
|
+
"name": {"type": "string"}
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
# Manual streaming (without context manager)
|
|
441
|
+
s = agent.streamer(message)
|
|
442
|
+
s.stream("Hello...")
|
|
443
|
+
await s.finish()
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Structural Streaming
|
|
447
|
+
|
|
448
|
+
Rich UI components streamed alongside text content. Use `s.stream_by()` to bind a structural component to the streamer.
|
|
449
|
+
|
|
450
|
+
#### Artifact
|
|
451
|
+
|
|
452
|
+
Artifacts represent files, images, or embeds that are generated and displayed. Artifact data is persisted to the database.
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
from basion_agent import Artifact
|
|
456
|
+
|
|
457
|
+
@agent.on_message
|
|
458
|
+
async def handle(message, sender):
|
|
459
|
+
async with agent.streamer(message) as s:
|
|
460
|
+
artifact = Artifact()
|
|
461
|
+
|
|
462
|
+
# Show progress
|
|
463
|
+
s.stream_by(artifact).generating("Creating chart...", progress=0.5)
|
|
464
|
+
|
|
465
|
+
# Complete with result
|
|
466
|
+
s.stream_by(artifact).done(
|
|
467
|
+
url="https://example.com/chart.png",
|
|
468
|
+
type="image", # image, iframe, document, video, audio, code, link, file
|
|
469
|
+
title="Sales Chart",
|
|
470
|
+
description="Q4 sales data",
|
|
471
|
+
metadata={"width": 800, "height": 600}
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Or signal an error
|
|
475
|
+
# s.stream_by(artifact).error("Failed to generate chart")
|
|
476
|
+
|
|
477
|
+
s.stream("Here's your chart!")
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Surface
|
|
481
|
+
|
|
482
|
+
Surfaces are interactive embedded components (iframes, widgets). Similar API to Artifact.
|
|
483
|
+
|
|
484
|
+
```python
|
|
485
|
+
from basion_agent import Surface
|
|
486
|
+
|
|
487
|
+
@agent.on_message
|
|
488
|
+
async def handle(message, sender):
|
|
489
|
+
async with agent.streamer(message) as s:
|
|
490
|
+
surface = Surface()
|
|
491
|
+
s.stream_by(surface).generating("Loading widget...")
|
|
492
|
+
s.stream_by(surface).done(
|
|
493
|
+
url="https://example.com/calendar",
|
|
494
|
+
type="iframe",
|
|
495
|
+
title="Calendar Widget",
|
|
496
|
+
)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
#### TextBlock
|
|
500
|
+
|
|
501
|
+
Collapsible text blocks with streaming title/body and visual variants. TextBlock events are not persisted.
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
from basion_agent import TextBlock
|
|
505
|
+
|
|
506
|
+
@agent.on_message
|
|
507
|
+
async def handle(message, sender):
|
|
508
|
+
async with agent.streamer(message) as s:
|
|
509
|
+
block = TextBlock()
|
|
510
|
+
|
|
511
|
+
# Set visual variant: thinking, note, warning, error, success
|
|
512
|
+
s.stream_by(block).set_variant("thinking")
|
|
513
|
+
|
|
514
|
+
# Stream title (appends)
|
|
515
|
+
s.stream_by(block).stream_title("Deep ")
|
|
516
|
+
s.stream_by(block).stream_title("Analysis...")
|
|
517
|
+
|
|
518
|
+
# Stream body (appends)
|
|
519
|
+
s.stream_by(block).stream_body("Step 1: Checking patterns\n")
|
|
520
|
+
s.stream_by(block).stream_body("Step 2: Validating\n")
|
|
521
|
+
|
|
522
|
+
# Replace title/body entirely
|
|
523
|
+
s.stream_by(block).update_title("Analysis Complete")
|
|
524
|
+
s.stream_by(block).update_body("All checks passed.")
|
|
525
|
+
|
|
526
|
+
# Mark as done
|
|
527
|
+
s.stream_by(block).done()
|
|
528
|
+
|
|
529
|
+
s.stream("Analysis finished!")
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Stepper
|
|
533
|
+
|
|
534
|
+
Multi-step progress indicators. Stepper events are not persisted.
|
|
535
|
+
|
|
536
|
+
```python
|
|
537
|
+
from basion_agent import Stepper
|
|
538
|
+
|
|
539
|
+
@agent.on_message
|
|
540
|
+
async def handle(message, sender):
|
|
541
|
+
async with agent.streamer(message) as s:
|
|
542
|
+
stepper = Stepper(steps=["Fetch", "Process", "Report"])
|
|
543
|
+
|
|
544
|
+
s.stream_by(stepper).start_step(0)
|
|
545
|
+
# ... do work ...
|
|
546
|
+
s.stream_by(stepper).complete_step(0)
|
|
547
|
+
|
|
548
|
+
s.stream_by(stepper).start_step(1)
|
|
549
|
+
# ... do work ...
|
|
550
|
+
s.stream_by(stepper).complete_step(1)
|
|
551
|
+
|
|
552
|
+
# Add a step dynamically
|
|
553
|
+
s.stream_by(stepper).add_step("Verify")
|
|
554
|
+
|
|
555
|
+
s.stream_by(stepper).start_step(2)
|
|
556
|
+
s.stream_by(stepper).complete_step(2)
|
|
557
|
+
|
|
558
|
+
s.stream_by(stepper).start_step(3)
|
|
559
|
+
# Update label mid-step
|
|
560
|
+
s.stream_by(stepper).update_step_label(3, "Verify (Final)")
|
|
561
|
+
s.stream_by(stepper).complete_step(3)
|
|
562
|
+
|
|
563
|
+
# Or signal failure
|
|
564
|
+
# s.stream_by(stepper).fail_step(1, error="Timeout")
|
|
565
|
+
|
|
566
|
+
s.stream_by(stepper).done()
|
|
567
|
+
s.stream("All steps complete!")
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Knowledge Graph (Tools)
|
|
571
|
+
|
|
572
|
+
Query biomedical knowledge graphs for diseases, proteins, phenotypes, drugs, and pathways. Accessed via `agent.tools.knowledge_graph`.
|
|
573
|
+
|
|
574
|
+
```python
|
|
575
|
+
@agent.on_message
|
|
576
|
+
async def handle(message, sender):
|
|
577
|
+
kg = agent.tools.knowledge_graph
|
|
578
|
+
|
|
579
|
+
# Search diseases
|
|
580
|
+
diseases = await kg.search_diseases(name="Huntington", limit=5)
|
|
581
|
+
disease = await kg.get_disease(disease_id=123)
|
|
582
|
+
|
|
583
|
+
# Search proteins/genes
|
|
584
|
+
proteins = await kg.search_proteins(symbol="BRCA1", limit=10)
|
|
585
|
+
|
|
586
|
+
# Search phenotypes (HPO terms)
|
|
587
|
+
phenotypes = await kg.search_phenotypes(name="seizure", hpo_id="HP:0001250")
|
|
588
|
+
|
|
589
|
+
# Search drugs
|
|
590
|
+
drugs = await kg.search_drugs(name="aspirin")
|
|
591
|
+
|
|
592
|
+
# Search pathways
|
|
593
|
+
pathways = await kg.search_pathways(name="apoptosis")
|
|
594
|
+
|
|
595
|
+
# Find similar diseases (by shared phenotypes)
|
|
596
|
+
similar = await kg.find_similar_diseases("Huntington Disease", limit=10)
|
|
597
|
+
for s in similar:
|
|
598
|
+
s.disease_name # Disease name
|
|
599
|
+
s.similarity_score # 0.0 - 1.0
|
|
600
|
+
s.shared_count # Number of shared phenotypes
|
|
601
|
+
|
|
602
|
+
# Find similar diseases (by shared genes)
|
|
603
|
+
similar = await kg.find_similar_diseases_by_genes("Huntington Disease")
|
|
604
|
+
|
|
605
|
+
# Get entity connections
|
|
606
|
+
edges = await kg.get_entity_network("BRCA1", "protein")
|
|
607
|
+
for e in edges:
|
|
608
|
+
e.source_id, e.source_type
|
|
609
|
+
e.target_id, e.target_type
|
|
610
|
+
e.relation_type
|
|
611
|
+
|
|
612
|
+
# k-hop graph traversal
|
|
613
|
+
subgraph = await kg.k_hop_traversal("BRCA1", "protein", k=2, limit_edges=100)
|
|
614
|
+
|
|
615
|
+
# Shortest path between entities
|
|
616
|
+
path = await kg.find_shortest_path(
|
|
617
|
+
start_name="BRCA1", start_type="protein",
|
|
618
|
+
end_name="Breast Cancer", end_type="disease",
|
|
619
|
+
max_hops=5
|
|
620
|
+
)
|
|
621
|
+
for step in path:
|
|
622
|
+
step.node_id, step.node_name, step.node_type, step.relation
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Remote Logging (Loki)
|
|
626
|
+
|
|
627
|
+
Send agent logs to Loki via the gateway for centralized monitoring. Logs are batched and sent in the background.
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
import logging
|
|
631
|
+
|
|
632
|
+
app = BasionAgentApp(
|
|
633
|
+
gateway_url="agent-gateway:8080",
|
|
634
|
+
api_key="key",
|
|
635
|
+
enable_remote_logging=True, # Enable Loki logging
|
|
636
|
+
remote_log_level=logging.INFO, # Min level (default: INFO)
|
|
637
|
+
remote_log_batch_size=100, # Logs per batch (default: 100)
|
|
638
|
+
remote_log_flush_interval=5.0, # Flush every N seconds (default: 5.0)
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Then use standard Python logging - it will be sent to Loki automatically
|
|
642
|
+
logger = logging.getLogger(__name__)
|
|
643
|
+
logger.info("Agent started", extra={"custom_field": "value"})
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
## Extensions
|
|
647
|
+
|
|
648
|
+
### LangGraph Integration
|
|
649
|
+
|
|
650
|
+
Use `HTTPCheckpointSaver` to persist LangGraph state via the Conversation Store checkpoint API.
|
|
651
|
+
|
|
652
|
+
```python
|
|
653
|
+
from basion_agent import BasionAgentApp
|
|
654
|
+
from basion_agent.extensions.langgraph import HTTPCheckpointSaver
|
|
655
|
+
from langgraph.graph import StateGraph
|
|
656
|
+
|
|
657
|
+
app = BasionAgentApp(gateway_url="...", api_key="...")
|
|
658
|
+
checkpointer = HTTPCheckpointSaver(app=app)
|
|
659
|
+
|
|
660
|
+
# Define your LangGraph
|
|
661
|
+
graph = StateGraph(MyState)
|
|
662
|
+
# ... add nodes and edges ...
|
|
663
|
+
compiled = graph.compile(checkpointer=checkpointer)
|
|
664
|
+
|
|
665
|
+
agent = app.register_me(name="langgraph-agent", ...)
|
|
666
|
+
|
|
667
|
+
@agent.on_message
|
|
668
|
+
async def handle(message, sender):
|
|
669
|
+
config = {"configurable": {"thread_id": message.conversation_id}}
|
|
670
|
+
|
|
671
|
+
async with agent.streamer(message) as s:
|
|
672
|
+
# Graph state persists across messages via checkpointer
|
|
673
|
+
result = await compiled.ainvoke(
|
|
674
|
+
{"messages": [message.content]},
|
|
675
|
+
config
|
|
676
|
+
)
|
|
677
|
+
s.stream(result["messages"][-1])
|
|
678
|
+
|
|
679
|
+
app.run()
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Pydantic AI Integration
|
|
683
|
+
|
|
684
|
+
Use `PydanticAIMessageStore` to persist Pydantic AI message history.
|
|
685
|
+
|
|
686
|
+
```python
|
|
687
|
+
from basion_agent import BasionAgentApp
|
|
688
|
+
from basion_agent.extensions.pydantic_ai import PydanticAIMessageStore
|
|
689
|
+
from pydantic_ai import Agent as PydanticAgent
|
|
690
|
+
|
|
691
|
+
app = BasionAgentApp(gateway_url="...", api_key="...")
|
|
692
|
+
store = PydanticAIMessageStore(app=app)
|
|
693
|
+
|
|
694
|
+
my_llm = PydanticAgent('openai:gpt-4o', system_prompt="You are helpful.")
|
|
695
|
+
|
|
696
|
+
agent = app.register_me(name="pydantic-agent", ...)
|
|
697
|
+
|
|
698
|
+
@agent.on_message
|
|
699
|
+
async def handle(message, sender):
|
|
700
|
+
# Load previous messages
|
|
701
|
+
history = await store.load(message.conversation_id)
|
|
702
|
+
|
|
703
|
+
async with agent.streamer(message) as s:
|
|
704
|
+
async with my_llm.run_stream(
|
|
705
|
+
message.content,
|
|
706
|
+
message_history=history
|
|
707
|
+
) as result:
|
|
708
|
+
async for chunk in result.stream_text():
|
|
709
|
+
s.stream(chunk)
|
|
710
|
+
|
|
711
|
+
# Save updated history
|
|
712
|
+
await store.save(message.conversation_id, result.all_messages())
|
|
713
|
+
|
|
714
|
+
app.run()
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
## Advanced Usage
|
|
718
|
+
|
|
719
|
+
### Inter-Agent Communication
|
|
720
|
+
|
|
721
|
+
Agents can send messages to other agents using the `send_to` parameter.
|
|
722
|
+
|
|
723
|
+
```python
|
|
724
|
+
@agent.on_message(senders=["user"])
|
|
725
|
+
async def handle_user(message, sender):
|
|
726
|
+
# Forward to specialist agent
|
|
727
|
+
async with agent.streamer(message, send_to="specialist-agent") as s:
|
|
728
|
+
s.stream("Forwarding your question to the specialist...")
|
|
729
|
+
|
|
730
|
+
@agent.on_message(senders=["specialist-agent"])
|
|
731
|
+
async def handle_specialist(message, sender):
|
|
732
|
+
# Respond to user with specialist's answer
|
|
733
|
+
async with agent.streamer(message, send_to="user") as s:
|
|
734
|
+
s.stream(f"The specialist says: {message.content}")
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### Dynamic Forms with Response Schema
|
|
738
|
+
|
|
739
|
+
Request structured input from users using JSON Schema forms.
|
|
740
|
+
|
|
741
|
+
```python
|
|
742
|
+
@agent.on_message
|
|
743
|
+
async def handle(message, sender):
|
|
744
|
+
async with agent.streamer(message, awaiting=True) as s:
|
|
745
|
+
s.stream("Please fill out this form:")
|
|
746
|
+
s.set_response_schema({
|
|
747
|
+
"type": "object",
|
|
748
|
+
"title": "Contact Information",
|
|
749
|
+
"properties": {
|
|
750
|
+
"name": {"type": "string", "title": "Full Name"},
|
|
751
|
+
"email": {"type": "string", "format": "email"},
|
|
752
|
+
"message": {"type": "string", "title": "Message"}
|
|
753
|
+
},
|
|
754
|
+
"required": ["name", "email"]
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
# When user submits form, message.content will be JSON
|
|
758
|
+
@agent.on_message
|
|
759
|
+
async def handle_form(message, sender):
|
|
760
|
+
import json
|
|
761
|
+
data = json.loads(message.content)
|
|
762
|
+
name = data.get("name")
|
|
763
|
+
# Process form data...
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### Error Handling
|
|
767
|
+
|
|
768
|
+
Customize error handling behavior:
|
|
769
|
+
|
|
770
|
+
```python
|
|
771
|
+
app = BasionAgentApp(
|
|
772
|
+
gateway_url="...",
|
|
773
|
+
api_key="...",
|
|
774
|
+
error_message_template="Sorry, something went wrong. Please try again."
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
agent = app.register_me(...)
|
|
778
|
+
|
|
779
|
+
# Disable automatic error responses
|
|
780
|
+
agent.send_error_responses = False
|
|
781
|
+
|
|
782
|
+
@agent.on_message
|
|
783
|
+
async def handle(message, sender):
|
|
784
|
+
try:
|
|
785
|
+
# Your logic
|
|
786
|
+
pass
|
|
787
|
+
except Exception as e:
|
|
788
|
+
# Custom error handling
|
|
789
|
+
async with agent.streamer(message) as s:
|
|
790
|
+
s.stream(f"I encountered an issue: {str(e)}")
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
## Project Structure
|
|
794
|
+
|
|
795
|
+
```
|
|
796
|
+
ai-framework/
|
|
797
|
+
├── src/
|
|
798
|
+
│ └── basion_agent/
|
|
799
|
+
│ ├── __init__.py # Package exports
|
|
800
|
+
│ ├── app.py # BasionAgentApp
|
|
801
|
+
│ ├── agent.py # Agent class
|
|
802
|
+
│ ├── message.py # Message class (attachments, memory)
|
|
803
|
+
│ ├── streamer.py # Streamer class (stream_by, structural)
|
|
804
|
+
│ ├── conversation.py # Conversation helper (history, metadata)
|
|
805
|
+
│ ├── conversation_client.py # HTTP client for Conversation Store
|
|
806
|
+
│ ├── conversation_message.py # ConversationMessage dataclass
|
|
807
|
+
│ ├── memory.py # Memory context (query_about_user, etc.)
|
|
808
|
+
│ ├── memory_client.py # HTTP client for AI Memory
|
|
809
|
+
│ ├── attachment_client.py # HTTP client for attachments (download)
|
|
810
|
+
│ ├── checkpoint_client.py # HTTP client for checkpoints
|
|
811
|
+
│ ├── agent_state_client.py # HTTP client for agent state
|
|
812
|
+
│ ├── gateway_client.py # gRPC client for Agent Gateway
|
|
813
|
+
│ ├── gateway_pb2.py # Generated protobuf
|
|
814
|
+
│ ├── gateway_pb2_grpc.py # Generated gRPC stubs
|
|
815
|
+
│ ├── heartbeat.py # Heartbeat manager
|
|
816
|
+
│ ├── loki_handler.py # Loki remote log handler
|
|
817
|
+
│ ├── cli.py # CLI (basion-agent run)
|
|
818
|
+
│ ├── exceptions.py # Custom exceptions
|
|
819
|
+
│ ├── structural/
|
|
820
|
+
│ │ ├── __init__.py
|
|
821
|
+
│ │ ├── base.py # StructuralStreamer base class
|
|
822
|
+
│ │ ├── artifact.py # Artifact (image, file, iframe)
|
|
823
|
+
│ │ ├── surface.py # Surface (interactive embeds)
|
|
824
|
+
│ │ ├── text_block.py # TextBlock (collapsible text)
|
|
825
|
+
│ │ └── stepper.py # Stepper (multi-step progress)
|
|
826
|
+
│ ├── tools/
|
|
827
|
+
│ │ ├── __init__.py
|
|
828
|
+
│ │ ├── container.py # Tools container (lazy init)
|
|
829
|
+
│ │ └── knowledge_graph.py # Knowledge Graph client
|
|
830
|
+
│ └── extensions/
|
|
831
|
+
│ ├── __init__.py
|
|
832
|
+
│ ├── langgraph.py # LangGraph HTTPCheckpointSaver
|
|
833
|
+
│ └── pydantic_ai.py # Pydantic AI MessageStore
|
|
834
|
+
├── pyproject.toml
|
|
835
|
+
└── README.md
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
## Development
|
|
839
|
+
|
|
840
|
+
### Running Tests
|
|
841
|
+
|
|
842
|
+
```bash
|
|
843
|
+
# Install dev dependencies
|
|
844
|
+
pip install -e ".[dev]"
|
|
845
|
+
|
|
846
|
+
# Run tests with coverage
|
|
847
|
+
pytest
|
|
848
|
+
|
|
849
|
+
# Run specific test file
|
|
850
|
+
pytest tests/test_agent.py -v
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Regenerating Protobuf
|
|
854
|
+
|
|
855
|
+
If the gateway.proto file changes:
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
python -m grpc_tools.protoc \
|
|
859
|
+
-I../../agent-gateway/proto \
|
|
860
|
+
--python_out=src/basion_agent \
|
|
861
|
+
--grpc_python_out=src/basion_agent \
|
|
862
|
+
../../agent-gateway/proto/gateway.proto
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
## Dependencies
|
|
866
|
+
|
|
867
|
+
| Package | Purpose |
|
|
868
|
+
|---------|---------|
|
|
869
|
+
| grpcio | gRPC communication with Agent Gateway |
|
|
870
|
+
| grpcio-tools | Protobuf compilation |
|
|
871
|
+
| protobuf | Message serialization |
|
|
872
|
+
| requests | Sync HTTP for registration |
|
|
873
|
+
| aiohttp | Async HTTP for runtime operations |
|
|
874
|
+
|
|
875
|
+
### Optional Dependencies
|
|
876
|
+
|
|
877
|
+
| Package | Install Command | Purpose |
|
|
878
|
+
|---------|-----------------|---------|
|
|
879
|
+
| langgraph | `pip install basion-agent[langgraph]` | LangGraph checkpoint integration |
|
|
880
|
+
| pydantic-ai | `pip install basion-agent[pydantic]` | Pydantic AI message history |
|