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,94 @@
1
+ """Artifact structural streamer for file/image/iframe artifacts."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from .base import StructuralStreamer
5
+ import uuid
6
+
7
+
8
+ class Artifact(StructuralStreamer):
9
+ """Manages artifact lifecycle: generating → done.
10
+
11
+ Artifacts are files, images, iframes that are generated and displayed.
12
+ Artifact data is persisted to database on done().
13
+
14
+ Example:
15
+ artifact = Artifact() # ID auto-generated
16
+ s.stream_by(artifact).generating("Creating chart...", progress=0.5)
17
+ s.stream_by(artifact).done(
18
+ url="https://example.com/chart.png",
19
+ type="image",
20
+ title="Sales Chart"
21
+ )
22
+ """
23
+
24
+ def _generate_id(self) -> str:
25
+ return f"artifact-{uuid.uuid4().hex[:8]}"
26
+
27
+ def get_event_type_prefix(self) -> str:
28
+ return "artifact"
29
+
30
+ def generating(
31
+ self,
32
+ message: Optional[str] = None,
33
+ progress: Optional[float] = None
34
+ ) -> 'Artifact':
35
+ """Signal artifact is being generated.
36
+
37
+ Args:
38
+ message: Status message (e.g., "Creating chart...")
39
+ progress: Progress value 0.0-1.0
40
+
41
+ Returns:
42
+ self for method chaining
43
+ """
44
+ kwargs = {}
45
+ if message is not None:
46
+ kwargs["message"] = message
47
+ if progress is not None:
48
+ kwargs["progress"] = progress
49
+
50
+ return self._send_event("generating", **kwargs)
51
+
52
+ def done(
53
+ self,
54
+ url: str,
55
+ type: str,
56
+ title: Optional[str] = None,
57
+ description: Optional[str] = None,
58
+ metadata: Optional[Dict[str, Any]] = None
59
+ ) -> 'Artifact':
60
+ """Signal artifact is complete and persist to database.
61
+
62
+ Args:
63
+ url: URL to artifact
64
+ type: Artifact type (image, iframe, document, video, audio, code, link, file)
65
+ title: Display title
66
+ description: Optional description
67
+ metadata: Additional metadata (size, mimeType, dimensions, etc.)
68
+
69
+ Returns:
70
+ self for method chaining
71
+ """
72
+ kwargs = {
73
+ "url": url,
74
+ "type": type,
75
+ }
76
+ if title:
77
+ kwargs["title"] = title
78
+ if description:
79
+ kwargs["description"] = description
80
+ if metadata:
81
+ kwargs["metadata"] = metadata
82
+
83
+ return self._send_event("done", **kwargs)
84
+
85
+ def error(self, message: str) -> 'Artifact':
86
+ """Signal artifact generation failed.
87
+
88
+ Args:
89
+ message: Error message
90
+
91
+ Returns:
92
+ self for method chaining
93
+ """
94
+ return self._send_event("error", message=message)
@@ -0,0 +1,71 @@
1
+ """Base class for structural streaming objects."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Any, Optional, TYPE_CHECKING
5
+ import uuid
6
+
7
+ if TYPE_CHECKING:
8
+ from ..streamer import Streamer
9
+
10
+
11
+ class StructuralStreamer(ABC):
12
+ """Base class for structural streaming objects.
13
+
14
+ Structural streamers generate events but don't hold streaming state.
15
+ The Streamer binds to them via stream_by() to enable streaming.
16
+ """
17
+
18
+ def __init__(self, id: Optional[str] = None):
19
+ """Initialize with auto-generated or explicit ID."""
20
+ self.id = id or self._generate_id()
21
+ self._streamer: Optional['Streamer'] = None
22
+
23
+ @abstractmethod
24
+ def _generate_id(self) -> str:
25
+ """Generate a unique ID for this streamer type."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def get_event_type_prefix(self) -> str:
30
+ """Get the event type prefix (e.g., 'artifact', 'text_block')."""
31
+ pass
32
+
33
+ def _bind_streamer(self, streamer: 'Streamer') -> 'StructuralStreamer':
34
+ """Bind this structural streamer to a Streamer instance.
35
+
36
+ Called by Streamer.stream_by(). Returns self for chaining.
37
+ """
38
+ self._streamer = streamer
39
+ return self
40
+
41
+ def _send_event(self, action: str, **kwargs) -> 'StructuralStreamer':
42
+ """Send an event through the bound streamer.
43
+
44
+ Creates event payload and sends via streamer._send().
45
+ Returns self for method chaining.
46
+ """
47
+ if not self._streamer:
48
+ raise RuntimeError(
49
+ f"{self.__class__.__name__} not bound to a Streamer. "
50
+ f"Call streamer.stream_by() first."
51
+ )
52
+
53
+ # Build event body
54
+ body = {"id": self.id, **kwargs}
55
+
56
+ # Build event type
57
+ event_type = f"{self.get_event_type_prefix()}:{action}"
58
+
59
+ # Send through streamer using existing _send() method
60
+ # content = stringified JSON body
61
+ # eventType = structural event type
62
+ # persist = False (structural events not persisted as content)
63
+ import json
64
+ self._streamer._send(
65
+ content=json.dumps(body),
66
+ done=False,
67
+ persist=False,
68
+ event_type=event_type
69
+ )
70
+
71
+ return self
@@ -0,0 +1,125 @@
1
+ """Stepper structural streamer for multi-step progress indicators."""
2
+
3
+ from typing import Optional, List
4
+ from .base import StructuralStreamer
5
+ import uuid
6
+
7
+
8
+ class Stepper(StructuralStreamer):
9
+ """Manages multi-step progress indicator.
10
+
11
+ Stepper events are NOT persisted to database.
12
+ IDs are auto-generated - no need to provide them manually.
13
+
14
+ Example:
15
+ stepper = Stepper(steps=["Fetch", "Process", "Report"]) # ID auto-generated
16
+ s.stream_by(stepper).start_step(0)
17
+ s.stream_by(stepper).complete_step(0)
18
+ s.stream_by(stepper).start_step(1)
19
+ s.stream_by(stepper).add_step("Verify") # Dynamic step
20
+ s.stream_by(stepper).complete_step(1)
21
+ s.stream_by(stepper).done()
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ id: Optional[str] = None,
27
+ steps: Optional[List[str]] = None
28
+ ):
29
+ super().__init__(id)
30
+ self.steps = steps or []
31
+ self._initialized = False
32
+
33
+ def _generate_id(self) -> str:
34
+ return f"stepper-{uuid.uuid4().hex[:8]}"
35
+
36
+ def get_event_type_prefix(self) -> str:
37
+ return "stepper"
38
+
39
+ def _ensure_initialized(self) -> 'Stepper':
40
+ """Send initialization event with steps if not already sent.
41
+
42
+ This is automatically called before any other stepper event.
43
+ """
44
+ if not self._initialized and self.steps:
45
+ self._send_event("init", steps=self.steps)
46
+ self._initialized = True
47
+ return self
48
+
49
+ def start_step(self, index: int) -> 'Stepper':
50
+ """Mark step as in progress.
51
+
52
+ Args:
53
+ index: Step index to start
54
+
55
+ Returns:
56
+ self for method chaining
57
+ """
58
+ self._ensure_initialized()
59
+ return self._send_event("start_step", index=index)
60
+
61
+ def complete_step(self, index: int) -> 'Stepper':
62
+ """Mark step as complete.
63
+
64
+ Args:
65
+ index: Step index to complete
66
+
67
+ Returns:
68
+ self for method chaining
69
+ """
70
+ self._ensure_initialized()
71
+ return self._send_event("complete_step", index=index)
72
+
73
+ def fail_step(self, index: int, error: Optional[str] = None) -> 'Stepper':
74
+ """Mark step as failed.
75
+
76
+ Args:
77
+ index: Step index that failed
78
+ error: Optional error message
79
+
80
+ Returns:
81
+ self for method chaining
82
+ """
83
+ self._ensure_initialized()
84
+ kwargs = {"index": index}
85
+ if error:
86
+ kwargs["error"] = error
87
+ return self._send_event("fail_step", **kwargs)
88
+
89
+ def add_step(self, label: str, index: Optional[int] = None) -> 'Stepper':
90
+ """Add a new step dynamically.
91
+
92
+ Args:
93
+ label: Step label
94
+ index: Optional position to insert at
95
+
96
+ Returns:
97
+ self for method chaining
98
+ """
99
+ self._ensure_initialized()
100
+ kwargs = {"label": label}
101
+ if index is not None:
102
+ kwargs["index"] = index
103
+ return self._send_event("add_step", **kwargs)
104
+
105
+ def update_step_label(self, index: int, label: str) -> 'Stepper':
106
+ """Update step label.
107
+
108
+ Args:
109
+ index: Step index to update
110
+ label: New label
111
+
112
+ Returns:
113
+ self for method chaining
114
+ """
115
+ self._ensure_initialized()
116
+ return self._send_event("update_step_label", index=index, label=label)
117
+
118
+ def done(self) -> 'Stepper':
119
+ """Mark stepper as complete (all steps finished).
120
+
121
+ Returns:
122
+ self for method chaining
123
+ """
124
+ self._ensure_initialized()
125
+ return self._send_event("done")
@@ -0,0 +1,90 @@
1
+ """Surface structural streamer for embedded interactive components."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from .base import StructuralStreamer
5
+ import uuid
6
+
7
+
8
+ class Surface(StructuralStreamer):
9
+ """Manages embedded surface lifecycle (identical to Artifact).
10
+
11
+ Surfaces are embedded interactive components (iframes, embeds).
12
+ Surface data is persisted to database as artifacts with surface type.
13
+
14
+ Example:
15
+ surface = Surface()
16
+ s.stream_by(surface).generating("Loading calendar...")
17
+ s.stream_by(surface).done(url="https://cal.com/embed", type="iframe")
18
+ """
19
+
20
+ def _generate_id(self) -> str:
21
+ return f"surface-{uuid.uuid4().hex[:8]}"
22
+
23
+ def get_event_type_prefix(self) -> str:
24
+ return "surface"
25
+
26
+ def generating(
27
+ self,
28
+ message: Optional[str] = None,
29
+ progress: Optional[float] = None
30
+ ) -> 'Surface':
31
+ """Signal surface is loading.
32
+
33
+ Args:
34
+ message: Status message (e.g., "Loading calendar...")
35
+ progress: Progress value 0.0-1.0
36
+
37
+ Returns:
38
+ self for method chaining
39
+ """
40
+ kwargs = {}
41
+ if message is not None:
42
+ kwargs["message"] = message
43
+ if progress is not None:
44
+ kwargs["progress"] = progress
45
+
46
+ return self._send_event("generating", **kwargs)
47
+
48
+ def done(
49
+ self,
50
+ url: str,
51
+ type: str = "iframe",
52
+ title: Optional[str] = None,
53
+ description: Optional[str] = None,
54
+ metadata: Optional[Dict[str, Any]] = None
55
+ ) -> 'Surface':
56
+ """Signal surface is ready and persist to database.
57
+
58
+ Args:
59
+ url: URL to surface
60
+ type: Surface type (default: iframe)
61
+ title: Display title
62
+ description: Optional description
63
+ metadata: Additional metadata
64
+
65
+ Returns:
66
+ self for method chaining
67
+ """
68
+ kwargs = {
69
+ "url": url,
70
+ "type": type,
71
+ }
72
+ if title:
73
+ kwargs["title"] = title
74
+ if description:
75
+ kwargs["description"] = description
76
+ if metadata:
77
+ kwargs["metadata"] = metadata
78
+
79
+ return self._send_event("done", **kwargs)
80
+
81
+ def error(self, message: str) -> 'Surface':
82
+ """Signal surface loading failed.
83
+
84
+ Args:
85
+ message: Error message
86
+
87
+ Returns:
88
+ self for method chaining
89
+ """
90
+ return self._send_event("error", message=message)
@@ -0,0 +1,96 @@
1
+ """TextBlock structural streamer for collapsible text content."""
2
+
3
+ from typing import Optional
4
+ from .base import StructuralStreamer
5
+ import uuid
6
+
7
+
8
+ class TextBlock(StructuralStreamer):
9
+ """Manages collapsible text block with title and body.
10
+
11
+ Both title and body support streaming (append) and update (replace).
12
+ TextBlock events are NOT persisted to database.
13
+
14
+ Example:
15
+ block = TextBlock() # ID auto-generated
16
+ s.stream_by(block).set_variant("thinking")
17
+ s.stream_by(block).stream_title("Deep ")
18
+ s.stream_by(block).stream_title("Analysis...")
19
+ s.stream_by(block).stream_body("Step 1: Check patterns\\n")
20
+ s.stream_by(block).stream_body("Step 2: Validate\\n")
21
+ s.stream_by(block).update_title("Analysis Complete")
22
+ s.stream_by(block).done()
23
+ """
24
+
25
+ def __init__(self, id: Optional[str] = None, title: Optional[str] = None):
26
+ super().__init__(id)
27
+ self.initial_title = title
28
+
29
+ def _generate_id(self) -> str:
30
+ return f"text_block-{uuid.uuid4().hex[:8]}"
31
+
32
+ def get_event_type_prefix(self) -> str:
33
+ return "text_block"
34
+
35
+ def stream_title(self, content: str) -> 'TextBlock':
36
+ """Stream title content (appends to existing).
37
+
38
+ Args:
39
+ content: Content to append to title
40
+
41
+ Returns:
42
+ self for method chaining
43
+ """
44
+ return self._send_event("stream_title", content=content)
45
+
46
+ def stream_body(self, content: str) -> 'TextBlock':
47
+ """Stream body content (appends to existing).
48
+
49
+ Args:
50
+ content: Content to append to body
51
+
52
+ Returns:
53
+ self for method chaining
54
+ """
55
+ return self._send_event("stream_body", content=content)
56
+
57
+ def update_title(self, title: str) -> 'TextBlock':
58
+ """Replace entire title.
59
+
60
+ Args:
61
+ title: New title (replaces existing)
62
+
63
+ Returns:
64
+ self for method chaining
65
+ """
66
+ return self._send_event("update_title", title=title)
67
+
68
+ def update_body(self, body: str) -> 'TextBlock':
69
+ """Replace entire body.
70
+
71
+ Args:
72
+ body: New body (replaces existing)
73
+
74
+ Returns:
75
+ self for method chaining
76
+ """
77
+ return self._send_event("update_body", body=body)
78
+
79
+ def set_variant(self, variant: str) -> 'TextBlock':
80
+ """Set visual variant.
81
+
82
+ Args:
83
+ variant: Visual style (thinking, note, warning, error, success)
84
+
85
+ Returns:
86
+ self for method chaining
87
+ """
88
+ return self._send_event("set_variant", variant=variant)
89
+
90
+ def done(self) -> 'TextBlock':
91
+ """Mark text block as complete.
92
+
93
+ Returns:
94
+ self for method chaining
95
+ """
96
+ return self._send_event("done")
@@ -0,0 +1,19 @@
1
+ """Tools module for Basion Agent SDK."""
2
+
3
+ from .container import Tools
4
+ from .knowledge_graph import (
5
+ Edge,
6
+ Entity,
7
+ KnowledgeGraphTool,
8
+ PathStep,
9
+ SimilarDisease,
10
+ )
11
+
12
+ __all__ = [
13
+ "Tools",
14
+ "KnowledgeGraphTool",
15
+ "Entity",
16
+ "Edge",
17
+ "SimilarDisease",
18
+ "PathStep",
19
+ ]
@@ -0,0 +1,46 @@
1
+ """Tools container - accessed via agent.tools property."""
2
+
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ if TYPE_CHECKING:
6
+ from .knowledge_graph import KnowledgeGraphTool
7
+
8
+
9
+ class Tools:
10
+ """
11
+ Container for all available tools.
12
+
13
+ Accessed via agent.tools property (similar to agent.streamer(message)):
14
+ kg = agent.tools.knowledge_graph
15
+ diseases = await kg.search_diseases(name="Huntington")
16
+
17
+ Tools are lazy-initialized - no overhead if not used.
18
+ """
19
+
20
+ def __init__(self, gateway_client):
21
+ self._gateway_client = gateway_client
22
+ self._knowledge_graph: Optional["KnowledgeGraphTool"] = None
23
+
24
+ @property
25
+ def knowledge_graph(self) -> "KnowledgeGraphTool":
26
+ """
27
+ Knowledge Graph tool for querying biomedical entities and relationships.
28
+
29
+ Example:
30
+ kg = agent.tools.knowledge_graph
31
+ diseases = await kg.search_diseases(name="Huntington")
32
+ similar = await kg.find_similar_diseases("Huntington Disease")
33
+ path = await kg.find_shortest_path("BRCA1", "protein", "Breast Cancer", "disease")
34
+ """
35
+ if self._knowledge_graph is None:
36
+ from .knowledge_graph import KnowledgeGraphTool
37
+
38
+ self._knowledge_graph = KnowledgeGraphTool(
39
+ self._gateway_client.knowledge_graph_url
40
+ )
41
+ return self._knowledge_graph
42
+
43
+ async def close(self):
44
+ """Close all tool sessions."""
45
+ if self._knowledge_graph:
46
+ await self._knowledge_graph.close()