Skip to content

Commit 3cf104a

Browse files
committed
refactor: update consumer, admin, exceptions, and __init__ for Moonshot API
1 parent ff475ec commit 3cf104a

5 files changed

Lines changed: 517 additions & 2 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Streamline Agent Memory Example.
2+
3+
Demonstrates the memory MCP tools (remember, recall) for building
4+
agents with persistent, semantically searchable memory. Shows both
5+
single-agent memory and multi-agent shared memory.
6+
7+
Prerequisites:
8+
- Streamline server running with memory features enabled
9+
- pip install streamline-sdk
10+
11+
Run with:
12+
python examples/agent_memory/memory_demo.py
13+
"""
14+
15+
import asyncio
16+
import os
17+
18+
from streamline_sdk import StreamlineClient
19+
20+
21+
async def single_agent_memory(client: StreamlineClient) -> None:
22+
"""Demonstrate remember/recall for a single agent."""
23+
print("=== Single Agent Memory ===")
24+
25+
# Store architectural decisions
26+
await client.memory_remember(
27+
agent_id="demo-agent",
28+
content="We chose PostgreSQL for its JSONB support and mature ecosystem",
29+
kind="fact",
30+
importance=0.8,
31+
tags=["architecture", "database"],
32+
)
33+
34+
await client.memory_remember(
35+
agent_id="demo-agent",
36+
content="Redis is used as a caching layer with a 15-minute TTL",
37+
kind="fact",
38+
importance=0.7,
39+
tags=["architecture", "caching"],
40+
)
41+
42+
await client.memory_remember(
43+
agent_id="demo-agent",
44+
content="User requested dark mode support in the dashboard",
45+
kind="preference",
46+
importance=0.6,
47+
tags=["ui", "user-request"],
48+
)
49+
50+
print("Stored 3 memories\n")
51+
52+
# Recall by semantic similarity
53+
print("--- Recall: 'why did we pick our database?' ---")
54+
results = await client.memory_recall(
55+
agent_id="demo-agent",
56+
query="why did we pick our database?",
57+
k=5,
58+
)
59+
for hit in results:
60+
print(f" [{hit.tier}] score={hit.score:.2f}: {hit.content}")
61+
62+
print("\n--- Recall: 'caching strategy' ---")
63+
results = await client.memory_recall(
64+
agent_id="demo-agent",
65+
query="caching strategy",
66+
k=5,
67+
)
68+
for hit in results:
69+
print(f" [{hit.tier}] score={hit.score:.2f}: {hit.content}")
70+
71+
72+
async def multi_agent_shared_memory(client: StreamlineClient) -> None:
73+
"""Demonstrate shared memory between multiple agents."""
74+
print("\n=== Multi-Agent Shared Memory ===")
75+
76+
# Agent A stores a decision
77+
await client.memory_remember(
78+
agent_id="agent-a",
79+
namespace="team-shared",
80+
content="Deploy target is Kubernetes on AWS EKS",
81+
kind="fact",
82+
importance=0.9,
83+
tags=["infra", "deployment"],
84+
)
85+
print("Agent A stored deployment decision")
86+
87+
# Agent B stores related context
88+
await client.memory_remember(
89+
agent_id="agent-b",
90+
namespace="team-shared",
91+
content="CI/CD pipeline uses GitHub Actions with OIDC auth to AWS",
92+
kind="fact",
93+
importance=0.8,
94+
tags=["infra", "ci-cd"],
95+
)
96+
print("Agent B stored CI/CD context")
97+
98+
# Agent C recalls shared memories from the team namespace
99+
print("\n--- Agent C recalls 'deployment infrastructure' from shared namespace ---")
100+
results = await client.memory_recall(
101+
agent_id="agent-c",
102+
namespace="team-shared",
103+
query="deployment infrastructure",
104+
k=5,
105+
)
106+
for hit in results:
107+
print(f" [{hit.tier}] score={hit.score:.2f}: {hit.content}")
108+
109+
110+
async def main() -> None:
111+
"""Run agent memory demos."""
112+
bootstrap = os.environ.get("STREAMLINE_BOOTSTRAP_SERVERS", "localhost:9092")
113+
http_url = os.environ.get("STREAMLINE_HTTP", "http://localhost:9094")
114+
115+
async with StreamlineClient(
116+
bootstrap_servers=bootstrap, http_endpoint=http_url
117+
) as client:
118+
await single_agent_memory(client)
119+
await multi_agent_shared_memory(client)
120+
121+
print("\nDone!")
122+
123+
124+
if __name__ == "__main__":
125+
asyncio.run(main())

streamline_sdk/__init__.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ async def main():
1919

2020
from .client import StreamlineClient
2121
from .producer import Producer, ProducerRecord, RecordMetadata
22-
from .consumer import Consumer, ConsumerRecord
22+
from .consumer import Consumer, ConsumerRecord, SearchHit
2323
from .admin import Admin, TopicConfig, TopicInfo, PartitionInfo
24-
from .admin import ClusterInfo, BrokerInfo, ConsumerLag, ConsumerGroupLag, InspectedMessage, MetricPoint
24+
from .admin import ClusterInfo, BrokerInfo, ConsumerLag, ConsumerGroupLag, InspectedMessage, MetricPoint, BranchInfo
2525
from .exceptions import (
2626
StreamlineError,
2727
ConnectionError,
@@ -43,6 +43,40 @@ async def main():
4343
)
4444
from .schema_producer import SchemaProducer, SchemaConsumer, DeserializedRecord
4545
from .traced import TracedProducer, TracedConsumer
46+
from .branches_admin import (
47+
BranchAdminClient,
48+
BranchAdminError,
49+
BranchMessage,
50+
BranchView,
51+
)
52+
from .contracts import (
53+
ContractsClient,
54+
ContractsError,
55+
ValidationError,
56+
ValidationResult,
57+
)
58+
from .attestation import (
59+
ATTEST_HEADER,
60+
Attestor,
61+
AttestationError,
62+
SignedAttestation,
63+
)
64+
from .verifier import (
65+
StreamlineVerifier,
66+
VerificationResult as AttestationVerificationResult,
67+
)
68+
from .search import (
69+
SearchClient,
70+
SearchError,
71+
SearchHit,
72+
SearchResult,
73+
)
74+
from .memory import (
75+
MemoryClient,
76+
MemoryError,
77+
RecalledMemory,
78+
WrittenEntry,
79+
)
4680

4781
__version__ = "0.2.0"
4882

@@ -61,6 +95,7 @@ async def main():
6195
"TopicConfig",
6296
"TopicInfo",
6397
"PartitionInfo",
98+
"BranchInfo",
6499
# Exceptions
65100
"StreamlineError",
66101
"ConnectionError",
@@ -98,4 +133,32 @@ async def main():
98133
# Traced wrappers
99134
"TracedProducer",
100135
"TracedConsumer",
136+
# Branches admin (M5 P1)
137+
"BranchAdminClient",
138+
"BranchAdminError",
139+
"BranchMessage",
140+
"BranchView",
141+
# Contracts validate (M2)
142+
"ContractsClient",
143+
"ContractsError",
144+
"ValidationError",
145+
"ValidationResult",
146+
# Attestation (M4)
147+
"ATTEST_HEADER",
148+
"Attestor",
149+
"AttestationError",
150+
"SignedAttestation",
151+
# Local attestation verifier
152+
"StreamlineVerifier",
153+
"AttestationVerificationResult",
154+
# Semantic search (M2)
155+
"SearchClient",
156+
"SearchError",
157+
"SearchHit",
158+
"SearchResult",
159+
# Agent Memory (M1)
160+
"MemoryClient",
161+
"MemoryError",
162+
"RecalledMemory",
163+
"WrittenEntry",
101164
]

streamline_sdk/admin.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,23 @@ class MetricPoint:
208208
timestamp: int = 0
209209

210210

211+
@dataclass
212+
class BranchInfo:
213+
"""Information about a copy-on-write topic branch (M5).
214+
215+
Attributes:
216+
name: Branch name.
217+
base_topic: The base topic this branch forks from.
218+
state: Branch state (active, discarded, merged).
219+
created_at: Creation timestamp in milliseconds since epoch.
220+
"""
221+
222+
name: str
223+
base_topic: str
224+
state: str = "active"
225+
created_at: int = 0
226+
227+
211228
class Admin:
212229
"""Administrative operations for Streamline.
213230
@@ -399,6 +416,51 @@ def _sync_get():
399416
return json.loads(resp.read())
400417
return await asyncio.to_thread(_sync_get)
401418

419+
async def _http_post(self, path: str, body: Any) -> Any:
420+
"""Make an HTTP POST request to the Streamline REST API."""
421+
http_url = getattr(self._client_config, "http_url", "http://localhost:9094")
422+
url = f"{http_url}{path}"
423+
424+
if HAS_AIOHTTP:
425+
async with aiohttp.ClientSession() as session:
426+
async with session.post(url, json=body, timeout=aiohttp.ClientTimeout(total=10)) as resp:
427+
if resp.status == 404:
428+
raise TopicError(f"Not found: {path}")
429+
if resp.status not in (200, 201):
430+
text = await resp.text()
431+
raise TopicError(f"HTTP {resp.status}: {text}")
432+
return await resp.json()
433+
else:
434+
import urllib.request
435+
payload = json.dumps(body).encode("utf-8")
436+
req = urllib.request.Request(url, data=payload, method="POST")
437+
req.add_header("Content-Type", "application/json")
438+
def _sync_post():
439+
with urllib.request.urlopen(req, timeout=10) as resp:
440+
return json.loads(resp.read())
441+
return await asyncio.to_thread(_sync_post)
442+
443+
async def _http_delete(self, path: str) -> None:
444+
"""Make an HTTP DELETE request to the Streamline REST API."""
445+
http_url = getattr(self._client_config, "http_url", "http://localhost:9094")
446+
url = f"{http_url}{path}"
447+
448+
if HAS_AIOHTTP:
449+
async with aiohttp.ClientSession() as session:
450+
async with session.delete(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
451+
if resp.status == 404:
452+
raise TopicError(f"Not found: {path}")
453+
if resp.status >= 300:
454+
text = await resp.text()
455+
raise TopicError(f"HTTP {resp.status}: {text}")
456+
else:
457+
import urllib.request
458+
req = urllib.request.Request(url, method="DELETE")
459+
def _sync_delete():
460+
with urllib.request.urlopen(req, timeout=10) as resp:
461+
pass
462+
await asyncio.to_thread(_sync_delete)
463+
402464
async def list_consumer_groups(self) -> List[str]:
403465
"""List all consumer groups.
404466
@@ -609,6 +671,62 @@ async def metrics_history(self) -> List[MetricPoint]:
609671
for m in data
610672
]
611673

674+
async def create_branch(
675+
self, name: str, base_topic: str, base_offsets: Optional[Dict[int, int]] = None
676+
) -> "BranchInfo":
677+
"""Create a copy-on-write branch of a topic (M5).
678+
679+
Args:
680+
name: Branch name.
681+
base_topic: Topic to branch from.
682+
base_offsets: Per-partition base offsets (partition -> offset).
683+
684+
Returns:
685+
BranchInfo for the newly created branch.
686+
"""
687+
body: Dict[str, Any] = {"name": name, "base_topic": base_topic}
688+
if base_offsets:
689+
body["base_offsets"] = base_offsets
690+
data = await self._http_post("/v1/branches", body)
691+
return BranchInfo(
692+
name=data.get("name", name),
693+
base_topic=data.get("base_topic", base_topic),
694+
state=data.get("state", "active"),
695+
created_at=int(data.get("created_at", 0)),
696+
)
697+
698+
async def list_branches(self, topic: Optional[str] = None) -> List["BranchInfo"]:
699+
"""List copy-on-write topic branches (M5).
700+
701+
Args:
702+
topic: Filter by base topic (optional).
703+
704+
Returns:
705+
List of BranchInfo objects.
706+
"""
707+
path = "/v1/branches"
708+
if topic:
709+
path += f"?topic={topic}"
710+
data = await self._http_get(path)
711+
items = data if isinstance(data, list) else data.get("items", [])
712+
return [
713+
BranchInfo(
714+
name=b.get("name", ""),
715+
base_topic=b.get("base_topic", ""),
716+
state=b.get("state", "active"),
717+
created_at=int(b.get("created_at", 0)),
718+
)
719+
for b in items
720+
]
721+
722+
async def discard_branch(self, branch_id: str) -> None:
723+
"""Discard (delete) a copy-on-write topic branch (M5).
724+
725+
Args:
726+
branch_id: Branch identifier.
727+
"""
728+
await self._http_delete(f"/v1/branches/{branch_id}")
729+
612730
async def __aenter__(self) -> "Admin":
613731
"""Enter async context manager."""
614732
await self.start()

0 commit comments

Comments
 (0)