axeon-atrest 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: axeon-atrest
3
+ Version: 0.1.0
4
+ Summary: Agent Task Runtime & State Transport — middleware for AI agent communication, memory, and task lifecycle
5
+ License-Expression: MIT
6
+ Keywords: ai,agents,multi-agent,orchestration,middleware,llm
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: redis>=5.0
15
+ Requires-Dist: neo4j>=5.0
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: python-dotenv>=1.0
19
+ Requires-Dist: pydantic>=2.0
20
+ Provides-Extra: api
21
+ Requires-Dist: fastapi>=0.100; extra == "api"
22
+ Requires-Dist: uvicorn>=0.22; extra == "api"
23
+ Provides-Extra: all
24
+ Requires-Dist: fastapi>=0.100; extra == "all"
25
+ Requires-Dist: uvicorn>=0.22; extra == "all"
26
+
27
+ # AT Rest — Agent Task Runtime & State Transport
28
+
29
+ Middleware untuk agent communication, memory, dan task lifecycle.
30
+
31
+ 4 agent built-in (planner, researcher, reviewer, mentor) via Redis Streams bus.
32
+ Neo4j task persistence. FastAPI REST API server. Qdrant + Neo4j knowledge retrieval.
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ pip install atrest
38
+ ```
39
+
40
+ ```python
41
+ from atrest import Agent, generate, search
42
+
43
+ class MyAgent(Agent):
44
+ channel = "researcher"
45
+
46
+ def handle_request(self, msg):
47
+ goal = msg["payload"]["goal"]
48
+ results = search(goal, limit=3)
49
+ summary = generate(f"Summarize: {goal}\n\nContext: {results}")
50
+ return {"summary": summary, "sources": []}
51
+
52
+ MyAgent().run()
53
+ ```
54
+
55
+ ## Available Imports
56
+
57
+ ```python
58
+ from atrest import Agent, ATRestClient, AgentBus, TaskStore, generate, embed, search
59
+ ```
60
+
61
+ - `Agent` — base class for custom agents
62
+ - `ATRestClient` — HTTP client for AT Rest API
63
+ - `AgentBus` — Redis Streams pub/sub
64
+ - `TaskStore` — Neo4j AgentTask persistence
65
+ - `generate()` — Ollama LLM call
66
+ - `embed()` — Ollama embedding
67
+ - `search()` — Qdrant vector search
68
+
69
+ ## Optional: API Server
70
+
71
+ ```bash
72
+ pip install atrest[api]
73
+ python -m api.server
74
+ ```
75
+
76
+ ## Architecture
77
+
78
+ ```
79
+ User → REST API / Agent SDK → Agent Bus (Redis Streams)
80
+ → Planner → Researcher / Reviewer / Mentor
81
+ → Task Store (Neo4j) → Broadcast
82
+ → Knowledge (Qdrant + Neo4j)
83
+ ```
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,61 @@
1
+ # AT Rest — Agent Task Runtime & State Transport
2
+
3
+ Middleware untuk agent communication, memory, dan task lifecycle.
4
+
5
+ 4 agent built-in (planner, researcher, reviewer, mentor) via Redis Streams bus.
6
+ Neo4j task persistence. FastAPI REST API server. Qdrant + Neo4j knowledge retrieval.
7
+
8
+ ## Quick Start
9
+
10
+ ```bash
11
+ pip install atrest
12
+ ```
13
+
14
+ ```python
15
+ from atrest import Agent, generate, search
16
+
17
+ class MyAgent(Agent):
18
+ channel = "researcher"
19
+
20
+ def handle_request(self, msg):
21
+ goal = msg["payload"]["goal"]
22
+ results = search(goal, limit=3)
23
+ summary = generate(f"Summarize: {goal}\n\nContext: {results}")
24
+ return {"summary": summary, "sources": []}
25
+
26
+ MyAgent().run()
27
+ ```
28
+
29
+ ## Available Imports
30
+
31
+ ```python
32
+ from atrest import Agent, ATRestClient, AgentBus, TaskStore, generate, embed, search
33
+ ```
34
+
35
+ - `Agent` — base class for custom agents
36
+ - `ATRestClient` — HTTP client for AT Rest API
37
+ - `AgentBus` — Redis Streams pub/sub
38
+ - `TaskStore` — Neo4j AgentTask persistence
39
+ - `generate()` — Ollama LLM call
40
+ - `embed()` — Ollama embedding
41
+ - `search()` — Qdrant vector search
42
+
43
+ ## Optional: API Server
44
+
45
+ ```bash
46
+ pip install atrest[api]
47
+ python -m api.server
48
+ ```
49
+
50
+ ## Architecture
51
+
52
+ ```
53
+ User → REST API / Agent SDK → Agent Bus (Redis Streams)
54
+ → Planner → Researcher / Reviewer / Mentor
55
+ → Task Store (Neo4j) → Broadcast
56
+ → Knowledge (Qdrant + Neo4j)
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,6 @@
1
+ from atrest.agent import Agent
2
+ from atrest.client import ATRestClient
3
+ from atrest.bus import AgentBus
4
+ from atrest.task_store import TaskStore
5
+ from atrest.llm import generate, embed
6
+ from atrest.qdrant import search
@@ -0,0 +1,68 @@
1
+ """AT Rest — Base Agent class. Extend to create custom agents."""
2
+ import uuid
3
+ from datetime import datetime, timezone
4
+
5
+ from atrest.bus import AgentBus
6
+ from atrest.task_store import TaskStore
7
+ from shared.agent_memory import log_finding, log_decision
8
+
9
+ class Agent:
10
+ """Base class for AT Rest agents.
11
+
12
+ Usage:
13
+ class MyAgent(Agent):
14
+ channel = "my_agent"
15
+
16
+ def handle_request(self, msg):
17
+ goal = msg["payload"]["goal"]
18
+ # ... process ...
19
+ return {"summary": "done"}
20
+
21
+ MyAgent().run()
22
+ """
23
+
24
+ channel = None # override in subclass — e.g. "researcher", "reviewer"
25
+
26
+ def __init__(self, agent_id=None):
27
+ self.agent_id = agent_id or self.__class__.__name__.lower()
28
+ self.bus = AgentBus(agent_id=self.agent_id)
29
+ self.store = TaskStore()
30
+
31
+ def handle_request(self, msg):
32
+ """Override this. Return dict with at least 'summary' key."""
33
+ raise NotImplementedError
34
+
35
+ def _process(self, msg):
36
+ goal = msg["payload"].get("goal", "")
37
+ conversation_id = msg["conversation_id"]
38
+ task_id = msg.get("task_id", "")
39
+ log_finding(self.agent_id, f"Processing: {goal[:60]}", f"task={task_id[:8]}")
40
+
41
+ if task_id:
42
+ self.store.update_status(task_id, "in_progress")
43
+
44
+ try:
45
+ result = self.handle_request(msg)
46
+ if task_id:
47
+ self.store.update_status(task_id, "done")
48
+ except Exception as e:
49
+ log_finding(self.agent_id, f"Error: {e}")
50
+ result = {"summary": f"Error: {e}"}
51
+ if task_id:
52
+ self.store.update_status(task_id, "blocked")
53
+
54
+ self.bus.publish("planner", {
55
+ "from": self.agent_id, "to": "planner", "intent": "RESPONSE",
56
+ "conversation_id": conversation_id, "task_id": task_id,
57
+ "payload": result,
58
+ })
59
+
60
+ def run(self):
61
+ if not self.channel:
62
+ raise ValueError("Agent.channel must be set")
63
+ print(f"[{self.agent_id}] Starting. Subscribing to bus:{self.channel}...", flush=True)
64
+ for msg in self.bus.subscribe(self.channel):
65
+ if msg is None:
66
+ continue
67
+ if msg.get("intent") == "REQUEST":
68
+ self._process(msg)
@@ -0,0 +1,120 @@
1
+ """AT Rest — Redis Streams AgentBus wrapper."""
2
+ import json, uuid
3
+ from datetime import datetime, timezone
4
+
5
+ import redis
6
+
7
+ from atrest.config import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_DB
8
+
9
+ STREAMS = {
10
+ "planner": "bus:planner",
11
+ "researcher": "bus:researcher",
12
+ "reviewer": "bus:reviewer",
13
+ "mentor": "bus:mentor",
14
+ "broadcast": "bus:broadcast",
15
+ "conflict": "bus:conflict",
16
+ "dead_letter": "bus:dead_letter",
17
+ }
18
+
19
+ class AgentBus:
20
+ def __init__(self, agent_id="unknown"):
21
+ self.agent_id = agent_id
22
+ self.redis = redis.Redis(
23
+ host=REDIS_HOST, port=REDIS_PORT,
24
+ password=REDIS_PASSWORD, db=REDIS_DB,
25
+ decode_responses=True,
26
+ )
27
+ self.redis.ping()
28
+
29
+ # ── Stream helpers ────────────────────────────────────────────
30
+
31
+ def _stream(self, channel):
32
+ if channel in STREAMS:
33
+ return STREAMS[channel]
34
+ if isinstance(channel, str) and channel.startswith("bus:"):
35
+ return channel
36
+ return f"bus:{channel}"
37
+
38
+ # ── Envelope ──────────────────────────────────────────────────
39
+
40
+ def _envelope(self, msg):
41
+ return {
42
+ "msg_id": str(uuid.uuid4()),
43
+ "from": msg.get("from", self.agent_id),
44
+ "to": msg.get("to", ""),
45
+ "intent": msg.get("intent", "REQUEST"),
46
+ "task_id": msg.get("task_id") or "",
47
+ "payload": json.dumps(msg.get("payload", {}), ensure_ascii=False),
48
+ "conversation_id": msg.get("conversation_id") or str(uuid.uuid4()),
49
+ "timestamp": datetime.now(timezone.utc).isoformat(),
50
+ "ttl": str(msg.get("ttl", 30)),
51
+ }
52
+
53
+ def _decode(self, raw):
54
+ env = dict(raw)
55
+ if "payload" in env:
56
+ try:
57
+ env["payload"] = json.loads(env["payload"])
58
+ except (json.JSONDecodeError, TypeError):
59
+ env["payload"] = {}
60
+ return env
61
+
62
+ # ── Publish ───────────────────────────────────────────────────
63
+
64
+ def publish(self, channel, message):
65
+ stream = self._stream(channel)
66
+ env = self._envelope(message)
67
+ return self.redis.xadd(stream, env, maxlen=10000)
68
+
69
+ # ── Subscribe (consumer group) ───────────────────────────────
70
+
71
+ def subscribe(self, channel, group=None, consumer=None):
72
+ stream = self._stream(channel)
73
+ group_name = group or f"atrest-{stream}"
74
+ consumer_id = consumer or f"{self.agent_id}-{uuid.uuid4().hex[:8]}"
75
+
76
+ try:
77
+ self.redis.xgroup_create(stream, group_name, id="0", mkstream=True)
78
+ except redis.exceptions.ResponseError as e:
79
+ if "BUSYGROUP" not in str(e):
80
+ raise
81
+
82
+ while True:
83
+ try:
84
+ results = self.redis.xreadgroup(
85
+ group_name, consumer_id, {stream: ">"},
86
+ block=5000, count=1,
87
+ )
88
+ except redis.exceptions.ConnectionError:
89
+ yield None
90
+ continue
91
+ except redis.exceptions.TimeoutError:
92
+ continue
93
+ except redis.exceptions.ResponseError as e:
94
+ # NOGROUP — stream or consumer group was deleted, recreate
95
+ if "NOGROUP" in str(e):
96
+ try:
97
+ self.redis.xgroup_create(stream, group_name, id="0", mkstream=True)
98
+ except redis.exceptions.ResponseError:
99
+ pass
100
+ continue
101
+ raise
102
+
103
+ if results:
104
+ for stream_name, messages in results:
105
+ for msg_id, msg_data in messages:
106
+ yield self._decode(msg_data)
107
+ self.redis.xack(stream, group_name, msg_id)
108
+ else:
109
+ yield None # tick — allows caller to process timeouts
110
+
111
+ # ── Dead letter ──────────────────────────────────────────────
112
+
113
+ def dead_letter(self, original_msg, reason):
114
+ self.publish("dead_letter", {
115
+ "intent": "DEAD_LETTER",
116
+ "payload": {
117
+ "original_msg_id": original_msg.get("msg_id", ""),
118
+ "reason": str(reason),
119
+ },
120
+ })
@@ -0,0 +1,53 @@
1
+ """AT Rest — API Client. Wraps REST calls to AT Rest API server."""
2
+ import httpx
3
+
4
+ class ATRestClient:
5
+ """Minimal client for AT Rest API.
6
+
7
+ Usage:
8
+ client = ATRestClient("http://localhost:8080")
9
+ resp = client.research("Redis configuration")
10
+ print(resp["summary"])
11
+ """
12
+
13
+ def __init__(self, base_url="http://localhost:8080", timeout=30):
14
+ self.base_url = base_url.rstrip("/")
15
+ self.timeout = timeout
16
+
17
+ def _post(self, path, data):
18
+ r = httpx.post(f"{self.base_url}{path}", json=data, timeout=self.timeout)
19
+ r.raise_for_status()
20
+ return r.json()
21
+
22
+ def _get(self, path, params=None):
23
+ r = httpx.get(f"{self.base_url}{path}", params=params, timeout=self.timeout)
24
+ r.raise_for_status()
25
+ return r.json()
26
+
27
+ def health(self):
28
+ return self._get("/v1/health")
29
+
30
+ def create_task(self, goal, hint="researcher"):
31
+ return self._post("/v1/tasks", {"goal": goal, "hint": hint})
32
+
33
+ def get_task(self, task_id):
34
+ return self._get(f"/v1/tasks/{task_id}")
35
+
36
+ def list_tasks(self, status=None):
37
+ params = {"status": status} if status else {}
38
+ return self._get("/v1/tasks", params=params)
39
+
40
+ def task_lineage(self, task_id):
41
+ return self._get(f"/v1/tasks/{task_id}/lineage")
42
+
43
+ def research(self, query, limit=3):
44
+ return self._post("/v1/research", {"query": query, "limit": limit})
45
+
46
+ def graph_explore(self, query, limit=5):
47
+ return self._get("/v1/graph/explore", params={"query": query, "limit": limit})
48
+
49
+ def infra_dependencies(self):
50
+ return self._get("/v1/graph/dependencies")
51
+
52
+ def review(self, path):
53
+ return self._post("/v1/review", {"path": path})
@@ -0,0 +1,33 @@
1
+ """AT Rest — centralized configuration from environment."""
2
+ import os
3
+ from pathlib import Path
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ # Redis
9
+ REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
10
+ REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
11
+ REDIS_PASSWORD = os.getenv("REDIS_PASSWORD") or None
12
+ REDIS_DB = int(os.getenv("REDIS_DB", "0"))
13
+
14
+ # Neo4j
15
+ NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
16
+ NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
17
+ NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "")
18
+
19
+ # Qdrant
20
+ QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost")
21
+ QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333"))
22
+ QDRANT_URL = f"http://{QDRANT_HOST}:{QDRANT_PORT}"
23
+ QDRANT_COLLECTION = os.getenv("QDRANT_COLLECTION", "knowledge")
24
+
25
+ # Ollama
26
+ OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
27
+ OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b")
28
+ EMBED_MODEL = os.getenv("EMBED_MODEL", "nomic-embed-text")
29
+
30
+ # AT Rest
31
+ ATREST_LOG_DIR = Path(os.getenv("ATREST_LOG_DIR", "./logs"))
32
+ ATREST_ENV = os.getenv("ATREST_ENV", "development")
33
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
@@ -0,0 +1,28 @@
1
+ """AT Rest — thin cached wrapper for context-builder.py from memory/."""
2
+ import os, sys, importlib.util
3
+ from pathlib import Path
4
+
5
+ _MEMORY_ROOT = Path(os.getenv("ATREST_MEMORY_PATH", os.path.expanduser("~/memory")))
6
+ _CACHE = {}
7
+
8
+ def _load_cb():
9
+ if "cb" in _CACHE:
10
+ return _CACHE["cb"]
11
+ path = _MEMORY_ROOT / "context-builder.py"
12
+ if not path.exists():
13
+ return None
14
+ spec = importlib.util.spec_from_file_location("context_builder", str(path))
15
+ mod = importlib.util.module_from_spec(spec)
16
+ sys.path.insert(0, str(_MEMORY_ROOT))
17
+ try:
18
+ spec.loader.exec_module(mod)
19
+ except Exception:
20
+ return None
21
+ _CACHE["cb"] = mod
22
+ return mod
23
+
24
+ def infra_traverse(query):
25
+ mod = _load_cb()
26
+ if mod is None:
27
+ return {}
28
+ return mod.infra_traverse(query)
@@ -0,0 +1,16 @@
1
+ """AT Rest — shared Ollama helpers (generate + embed)."""
2
+ import httpx
3
+ from atrest.config import OLLAMA_BASE_URL, OLLAMA_MODEL, EMBED_MODEL
4
+
5
+ def generate(prompt, max_tokens=400, temperature=0.1, timeout=60):
6
+ r = httpx.post(f"{OLLAMA_BASE_URL}/api/generate", json={
7
+ "model": OLLAMA_MODEL, "prompt": prompt,
8
+ "stream": False, "options": {"temperature": temperature, "num_predict": max_tokens},
9
+ }, timeout=timeout)
10
+ r.raise_for_status()
11
+ return r.json()["response"].strip()
12
+
13
+ def embed(text, timeout=30):
14
+ r = httpx.post(f"{OLLAMA_BASE_URL}/api/embeddings",
15
+ json={"model": EMBED_MODEL, "prompt": text}, timeout=timeout)
16
+ return r.json()["embedding"]
@@ -0,0 +1,11 @@
1
+ """AT Rest — shared Qdrant search."""
2
+ import httpx
3
+ from atrest.config import QDRANT_URL, QDRANT_COLLECTION
4
+ from atrest.llm import embed
5
+
6
+ def search(query, limit=3):
7
+ vec = embed(query)
8
+ r = httpx.post(f"{QDRANT_URL}/collections/{QDRANT_COLLECTION}/points/search", json={
9
+ "vector": vec, "limit": limit, "with_payload": True,
10
+ }, timeout=10)
11
+ return r.json().get("result", [])
@@ -0,0 +1,100 @@
1
+ """AT Rest — Neo4j AgentTask persistence."""
2
+ import time, uuid
3
+ from datetime import datetime, timezone
4
+
5
+ from neo4j import GraphDatabase
6
+ from neo4j.exceptions import TransientError
7
+
8
+ from atrest.config import NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD
9
+
10
+ class TaskStore:
11
+ def __init__(self):
12
+ self.driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))
13
+ self._ensure_constraint()
14
+
15
+ def _ensure_constraint(self):
16
+ for attempt in range(3):
17
+ try:
18
+ with self.driver.session() as s:
19
+ s.run(
20
+ "CREATE CONSTRAINT IF NOT EXISTS FOR (t:AgentTask) REQUIRE t.id IS UNIQUE"
21
+ )
22
+ return
23
+ except TransientError:
24
+ if attempt < 2:
25
+ time.sleep(0.5 * (attempt + 1))
26
+ continue
27
+ raise
28
+
29
+ def create_task(self, goal, assigned_to, created_by, parent_id=None, parent_goal=None):
30
+ task_id = str(uuid.uuid4())
31
+ now = datetime.now(timezone.utc).isoformat()
32
+ with self.driver.session() as s:
33
+ s.run(
34
+ """CREATE (t:AgentTask {id: $id, goal: $goal, parent_goal: $parent_goal, status: 'pending',
35
+ assigned_to: $assigned_to, created_by: $created_by,
36
+ parent_task_id: $parent_id,
37
+ created_at: $now, updated_at: $now})""",
38
+ id=task_id, goal=goal, parent_goal=parent_goal, assigned_to=assigned_to,
39
+ created_by=created_by, parent_id=parent_id, now=now,
40
+ )
41
+ return task_id
42
+
43
+ def update_status(self, task_id, status):
44
+ now = datetime.now(timezone.utc).isoformat()
45
+ with self.driver.session() as s:
46
+ s.run(
47
+ "MATCH (t:AgentTask {id: $id}) SET t.status = $status, t.updated_at = $now",
48
+ id=task_id, status=status, now=now,
49
+ )
50
+
51
+ def get_task(self, task_id):
52
+ with self.driver.session() as s:
53
+ row = s.run(
54
+ "MATCH (t:AgentTask {id: $id}) RETURN t.id AS id, t.goal AS goal, "
55
+ "t.status AS status, t.assigned_to AS assigned_to, "
56
+ "t.created_by AS created_by, t.parent_task_id AS parent_task_id, "
57
+ "t.created_at AS created_at, t.updated_at AS updated_at",
58
+ id=task_id,
59
+ ).single()
60
+ return dict(row) if row else None
61
+
62
+ def find_duplicate(self, goal):
63
+ with self.driver.session() as s:
64
+ row = s.run(
65
+ "MATCH (t:AgentTask {goal: $goal, status: 'done'}) "
66
+ "RETURN t.id AS id LIMIT 1",
67
+ goal=goal,
68
+ ).single()
69
+ return row["id"] if row else None
70
+
71
+ def find_duplicate_by_parent_goal(self, parent_goal):
72
+ with self.driver.session() as s:
73
+ row = s.run(
74
+ "MATCH (t:AgentTask {parent_goal: $pg, status: 'done'}) "
75
+ "RETURN t.id AS id LIMIT 1",
76
+ pg=parent_goal,
77
+ ).single()
78
+ return row["id"] if row else None
79
+
80
+ def link_reference(self, task_id, node_id, node_label):
81
+ with self.driver.session() as s:
82
+ s.run(
83
+ f"MATCH (t:AgentTask {{id: $task_id}}) "
84
+ f"MATCH (n:{node_label} {{id: $node_id}}) "
85
+ f"MERGE (t)-[:REFERENCES]->(n)",
86
+ task_id=task_id, node_id=node_id,
87
+ )
88
+
89
+ def get_task_by_goal(self, goal):
90
+ with self.driver.session() as s:
91
+ rows = list(s.run(
92
+ "MATCH (t:AgentTask) WHERE t.goal = $goal OR t.parent_goal = $goal "
93
+ "RETURN t.id AS id, t.status AS status "
94
+ "ORDER BY t.created_at ASC",
95
+ goal=goal,
96
+ ))
97
+ return [dict(r) for r in rows]
98
+
99
+ def close(self):
100
+ self.driver.close()
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: axeon-atrest
3
+ Version: 0.1.0
4
+ Summary: Agent Task Runtime & State Transport — middleware for AI agent communication, memory, and task lifecycle
5
+ License-Expression: MIT
6
+ Keywords: ai,agents,multi-agent,orchestration,middleware,llm
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: redis>=5.0
15
+ Requires-Dist: neo4j>=5.0
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: rich>=13.0
18
+ Requires-Dist: python-dotenv>=1.0
19
+ Requires-Dist: pydantic>=2.0
20
+ Provides-Extra: api
21
+ Requires-Dist: fastapi>=0.100; extra == "api"
22
+ Requires-Dist: uvicorn>=0.22; extra == "api"
23
+ Provides-Extra: all
24
+ Requires-Dist: fastapi>=0.100; extra == "all"
25
+ Requires-Dist: uvicorn>=0.22; extra == "all"
26
+
27
+ # AT Rest — Agent Task Runtime & State Transport
28
+
29
+ Middleware untuk agent communication, memory, dan task lifecycle.
30
+
31
+ 4 agent built-in (planner, researcher, reviewer, mentor) via Redis Streams bus.
32
+ Neo4j task persistence. FastAPI REST API server. Qdrant + Neo4j knowledge retrieval.
33
+
34
+ ## Quick Start
35
+
36
+ ```bash
37
+ pip install atrest
38
+ ```
39
+
40
+ ```python
41
+ from atrest import Agent, generate, search
42
+
43
+ class MyAgent(Agent):
44
+ channel = "researcher"
45
+
46
+ def handle_request(self, msg):
47
+ goal = msg["payload"]["goal"]
48
+ results = search(goal, limit=3)
49
+ summary = generate(f"Summarize: {goal}\n\nContext: {results}")
50
+ return {"summary": summary, "sources": []}
51
+
52
+ MyAgent().run()
53
+ ```
54
+
55
+ ## Available Imports
56
+
57
+ ```python
58
+ from atrest import Agent, ATRestClient, AgentBus, TaskStore, generate, embed, search
59
+ ```
60
+
61
+ - `Agent` — base class for custom agents
62
+ - `ATRestClient` — HTTP client for AT Rest API
63
+ - `AgentBus` — Redis Streams pub/sub
64
+ - `TaskStore` — Neo4j AgentTask persistence
65
+ - `generate()` — Ollama LLM call
66
+ - `embed()` — Ollama embedding
67
+ - `search()` — Qdrant vector search
68
+
69
+ ## Optional: API Server
70
+
71
+ ```bash
72
+ pip install atrest[api]
73
+ python -m api.server
74
+ ```
75
+
76
+ ## Architecture
77
+
78
+ ```
79
+ User → REST API / Agent SDK → Agent Bus (Redis Streams)
80
+ → Planner → Researcher / Reviewer / Mentor
81
+ → Task Store (Neo4j) → Broadcast
82
+ → Knowledge (Qdrant + Neo4j)
83
+ ```
84
+
85
+ ## License
86
+
87
+ MIT