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.
Files changed (41) hide show
  1. basion_agent/__init__.py +62 -0
  2. basion_agent/agent.py +360 -0
  3. basion_agent/agent_state_client.py +149 -0
  4. basion_agent/app.py +502 -0
  5. basion_agent/artifact.py +58 -0
  6. basion_agent/attachment_client.py +153 -0
  7. basion_agent/checkpoint_client.py +169 -0
  8. basion_agent/checkpointer.py +16 -0
  9. basion_agent/cli.py +139 -0
  10. basion_agent/conversation.py +103 -0
  11. basion_agent/conversation_client.py +86 -0
  12. basion_agent/conversation_message.py +48 -0
  13. basion_agent/exceptions.py +36 -0
  14. basion_agent/extensions/__init__.py +1 -0
  15. basion_agent/extensions/langgraph.py +526 -0
  16. basion_agent/extensions/pydantic_ai.py +180 -0
  17. basion_agent/gateway_client.py +531 -0
  18. basion_agent/gateway_pb2.py +73 -0
  19. basion_agent/gateway_pb2_grpc.py +101 -0
  20. basion_agent/heartbeat.py +84 -0
  21. basion_agent/loki_handler.py +355 -0
  22. basion_agent/memory.py +73 -0
  23. basion_agent/memory_client.py +155 -0
  24. basion_agent/message.py +333 -0
  25. basion_agent/py.typed +0 -0
  26. basion_agent/streamer.py +184 -0
  27. basion_agent/structural/__init__.py +6 -0
  28. basion_agent/structural/artifact.py +94 -0
  29. basion_agent/structural/base.py +71 -0
  30. basion_agent/structural/stepper.py +125 -0
  31. basion_agent/structural/surface.py +90 -0
  32. basion_agent/structural/text_block.py +96 -0
  33. basion_agent/tools/__init__.py +19 -0
  34. basion_agent/tools/container.py +46 -0
  35. basion_agent/tools/knowledge_graph.py +306 -0
  36. basion_agent-0.4.0.dist-info/METADATA +880 -0
  37. basion_agent-0.4.0.dist-info/RECORD +41 -0
  38. basion_agent-0.4.0.dist-info/WHEEL +5 -0
  39. basion_agent-0.4.0.dist-info/entry_points.txt +2 -0
  40. basion_agent-0.4.0.dist-info/licenses/LICENSE +21 -0
  41. 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 |