style: ruff format + stronger sqlite asserts (delete utils, drop test_no_gng)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
from .core.app import App
|
from .core.app import App
|
||||||
from .core.types import Agent, Thread, Message
|
from .core.types import Agent, Message, Thread
|
||||||
|
|
||||||
__all__ = ["App", "Agent", "Thread", "Message", "main"]
|
__all__ = ["Agent", "App", "Message", "Thread", "main"]
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .app import App
|
from .app import App
|
||||||
from .types import Agent, Message, Thread
|
from .types import Agent, Message, Thread
|
||||||
|
|
||||||
__all__ = ["Agent", "Thread", "Message", "App"]
|
__all__ = ["Agent", "App", "Message", "Thread"]
|
||||||
|
|||||||
@@ -47,5 +47,7 @@ class App:
|
|||||||
def mark_read(self, tid: uuid.UUID, agent: Agent, seq: int) -> None:
|
def mark_read(self, tid: uuid.UUID, agent: Agent, seq: int) -> None:
|
||||||
return ops.mark_read(self.store, tid, agent, seq)
|
return ops.mark_read(self.store, tid, agent, seq)
|
||||||
|
|
||||||
def has_unread(self, tid: uuid.UUID, agent: Agent, exclude_own: bool = False) -> bool:
|
def has_unread(
|
||||||
|
self, tid: uuid.UUID, agent: Agent, exclude_own: bool = False
|
||||||
|
) -> bool:
|
||||||
return ops.has_unread(self.store, tid, agent, exclude_own)
|
return ops.has_unread(self.store, tid, agent, exclude_own)
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ def leave_thread(store: Store, tid: uuid.UUID, agent: Agent) -> Thread:
|
|||||||
return store.remove_participant(tid, agent)
|
return store.remove_participant(tid, agent)
|
||||||
|
|
||||||
|
|
||||||
def append_message(store: Store, tid: uuid.UUID, sender: Agent, content: str) -> Message:
|
def append_message(
|
||||||
|
store: Store, tid: uuid.UUID, sender: Agent, content: str
|
||||||
|
) -> Message:
|
||||||
return store.append_message(tid, sender, content)
|
return store.append_message(tid, sender, content)
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +50,9 @@ def mark_read(store: Store, tid: uuid.UUID, agent: Agent, seq: int) -> None:
|
|||||||
return store.set_cursor(tid, agent, seq)
|
return store.set_cursor(tid, agent, seq)
|
||||||
|
|
||||||
|
|
||||||
def has_unread(store: Store, tid: uuid.UUID, agent: Agent, exclude_own: bool = False) -> bool:
|
def has_unread(
|
||||||
|
store: Store, tid: uuid.UUID, agent: Agent, exclude_own: bool = False
|
||||||
|
) -> bool:
|
||||||
cur = store.get_cursor(tid, agent)
|
cur = store.get_cursor(tid, agent)
|
||||||
msgs = store.list_messages(tid, after_seq=cur)
|
msgs = store.list_messages(tid, after_seq=cur)
|
||||||
if exclude_own:
|
if exclude_own:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Protocol
|
|
||||||
import uuid
|
import uuid
|
||||||
from .types import Agent, Thread, Message
|
from typing import Protocol
|
||||||
|
|
||||||
|
from .types import Agent, Message, Thread
|
||||||
|
|
||||||
|
|
||||||
class Store(Protocol):
|
class Store(Protocol):
|
||||||
@@ -17,7 +19,11 @@ class Store(Protocol):
|
|||||||
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ...
|
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ...
|
||||||
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ...
|
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread: ...
|
||||||
def delete_thread(self, id: uuid.UUID) -> None: ...
|
def delete_thread(self, id: uuid.UUID) -> None: ...
|
||||||
def append_message(self, thread_id: uuid.UUID, sender: Agent, content: str) -> Message: ...
|
def append_message(
|
||||||
def list_messages(self, thread_id: uuid.UUID, after_seq: int = 0) -> list[Message]: ...
|
self, thread_id: uuid.UUID, sender: Agent, content: str
|
||||||
|
) -> Message: ...
|
||||||
|
def list_messages(
|
||||||
|
self, thread_id: uuid.UUID, after_seq: int = 0
|
||||||
|
) -> list[Message]: ...
|
||||||
def get_cursor(self, thread_id: uuid.UUID, agent: Agent) -> int: ...
|
def get_cursor(self, thread_id: uuid.UUID, agent: Agent) -> int: ...
|
||||||
def set_cursor(self, thread_id: uuid.UUID, agent: Agent, seq: int) -> None: ...
|
def set_cursor(self, thread_id: uuid.UUID, agent: Agent, seq: int) -> None: ...
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import uuid
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import uuid
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class Agent:
|
class Agent:
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
name: str
|
name: str
|
||||||
def __hash__(self): return hash(self.id)
|
|
||||||
def __eq__(self, other): return isinstance(other, Agent) and self.id == other.id
|
def __hash__(self):
|
||||||
|
return hash(self.id)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, Agent) and self.id == other.id
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class Message:
|
class Message:
|
||||||
@@ -18,6 +25,7 @@ class Message:
|
|||||||
seq: int
|
seq: int
|
||||||
ts: datetime.datetime
|
ts: datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class Thread:
|
class Thread:
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class InMemoryStore:
|
|||||||
t = Thread(
|
t = Thread(
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
participants=frozenset(participants),
|
participants=frozenset(participants),
|
||||||
created_at=datetime.datetime.now(datetime.timezone.utc),
|
created_at=datetime.datetime.now(datetime.UTC),
|
||||||
)
|
)
|
||||||
self._threads[t.id] = t
|
self._threads[t.id] = t
|
||||||
return t
|
return t
|
||||||
@@ -72,13 +72,17 @@ class InMemoryStore:
|
|||||||
|
|
||||||
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
|
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
|
||||||
t = self._threads[thread_id]
|
t = self._threads[thread_id]
|
||||||
nt = Thread(id=t.id, participants=t.participants | {agent}, created_at=t.created_at)
|
nt = Thread(
|
||||||
|
id=t.id, participants=t.participants | {agent}, created_at=t.created_at
|
||||||
|
)
|
||||||
self._threads[thread_id] = nt
|
self._threads[thread_id] = nt
|
||||||
return nt
|
return nt
|
||||||
|
|
||||||
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
|
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
|
||||||
t = self._threads[thread_id]
|
t = self._threads[thread_id]
|
||||||
nt = Thread(id=t.id, participants=t.participants - {agent}, created_at=t.created_at)
|
nt = Thread(
|
||||||
|
id=t.id, participants=t.participants - {agent}, created_at=t.created_at
|
||||||
|
)
|
||||||
self._threads[thread_id] = nt
|
self._threads[thread_id] = nt
|
||||||
return nt
|
return nt
|
||||||
|
|
||||||
@@ -90,7 +94,9 @@ class InMemoryStore:
|
|||||||
if k[0] == id:
|
if k[0] == id:
|
||||||
self._cursors.pop(k, None)
|
self._cursors.pop(k, None)
|
||||||
|
|
||||||
def append_message(self, thread_id: uuid.UUID, sender: Agent, content: str) -> Message:
|
def append_message(
|
||||||
|
self, thread_id: uuid.UUID, sender: Agent, content: str
|
||||||
|
) -> Message:
|
||||||
if thread_id not in self._threads:
|
if thread_id not in self._threads:
|
||||||
raise KeyError(thread_id)
|
raise KeyError(thread_id)
|
||||||
if sender not in self._threads[thread_id].participants:
|
if sender not in self._threads[thread_id].participants:
|
||||||
@@ -101,7 +107,7 @@ class InMemoryStore:
|
|||||||
sender=sender,
|
sender=sender,
|
||||||
content=content,
|
content=content,
|
||||||
seq=seq,
|
seq=seq,
|
||||||
ts=datetime.datetime.now(datetime.timezone.utc),
|
ts=datetime.datetime.now(datetime.UTC),
|
||||||
)
|
)
|
||||||
self._msgs[thread_id].append(m)
|
self._msgs[thread_id].append(m)
|
||||||
return m
|
return m
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import pathlib
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from agentmsgs.core.types import Agent, Message, Thread
|
from agentmsgs.core.types import Agent, Message, Thread
|
||||||
|
from agentmsgs.utils import WORKSPACE_ROOT
|
||||||
|
|
||||||
_SCHEMA = """
|
_SCHEMA = """
|
||||||
PRAGMA journal_mode=WAL;
|
PRAGMA journal_mode=WAL;
|
||||||
@@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS cursors (thread_id TEXT NOT NULL, agent_id TEXT NOT N
|
|||||||
|
|
||||||
|
|
||||||
class SQLiteStore:
|
class SQLiteStore:
|
||||||
def __init__(self, path: Path = Path.home() / ".agentmsgs.db") -> None:
|
def __init__(self, path: Path = WORKSPACE_ROOT / ".agentmsgs.db") -> None:
|
||||||
self.path = Path(path)
|
self.path = Path(path)
|
||||||
# ensure parent exists
|
# ensure parent exists
|
||||||
if self.path.parent and not self.path.parent.exists():
|
if self.path.parent and not self.path.parent.exists():
|
||||||
@@ -49,7 +49,7 @@ class SQLiteStore:
|
|||||||
def get_or_create_agent(self, name: str) -> Agent:
|
def get_or_create_agent(self, name: str) -> Agent:
|
||||||
# INSERT OR IGNORE + SELECT pattern
|
# INSERT OR IGNORE + SELECT pattern
|
||||||
new_id = str(uuid.uuid4())
|
new_id = str(uuid.uuid4())
|
||||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||||
self._db.execute(
|
self._db.execute(
|
||||||
"INSERT OR IGNORE INTO agents (id, name, created_at) VALUES (?, ?, ?)",
|
"INSERT OR IGNORE INTO agents (id, name, created_at) VALUES (?, ?, ?)",
|
||||||
(new_id, name, ts),
|
(new_id, name, ts),
|
||||||
@@ -62,7 +62,7 @@ class SQLiteStore:
|
|||||||
|
|
||||||
def create_agent(self, name: str) -> Agent:
|
def create_agent(self, name: str) -> Agent:
|
||||||
new_id = str(uuid.uuid4())
|
new_id = str(uuid.uuid4())
|
||||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||||
try:
|
try:
|
||||||
self._db.execute(
|
self._db.execute(
|
||||||
"INSERT INTO agents (id, name, created_at) VALUES (?, ?, ?)",
|
"INSERT INTO agents (id, name, created_at) VALUES (?, ?, ?)",
|
||||||
@@ -104,10 +104,12 @@ class SQLiteStore:
|
|||||||
if len(participants) < 2:
|
if len(participants) < 2:
|
||||||
raise ValueError("need >=2 participants")
|
raise ValueError("need >=2 participants")
|
||||||
tid = str(uuid.uuid4())
|
tid = str(uuid.uuid4())
|
||||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||||
self._db.execute("BEGIN")
|
self._db.execute("BEGIN")
|
||||||
try:
|
try:
|
||||||
self._db.execute("INSERT INTO threads (id, created_at) VALUES (?, ?)", (tid, ts))
|
self._db.execute(
|
||||||
|
"INSERT INTO threads (id, created_at) VALUES (?, ?)", (tid, ts)
|
||||||
|
)
|
||||||
for p in participants:
|
for p in participants:
|
||||||
self._db.execute(
|
self._db.execute(
|
||||||
"INSERT INTO thread_participants (thread_id, agent_id) VALUES (?, ?)",
|
"INSERT INTO thread_participants (thread_id, agent_id) VALUES (?, ?)",
|
||||||
@@ -124,7 +126,9 @@ class SQLiteStore:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_thread(self, id: uuid.UUID) -> Thread | None:
|
def get_thread(self, id: uuid.UUID) -> Thread | None:
|
||||||
cur = self._db.execute("SELECT id, created_at FROM threads WHERE id=?", (str(id),))
|
cur = self._db.execute(
|
||||||
|
"SELECT id, created_at FROM threads WHERE id=?", (str(id),)
|
||||||
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
@@ -151,7 +155,9 @@ class SQLiteStore:
|
|||||||
if not c:
|
if not c:
|
||||||
cur = self._db.execute("SELECT id FROM threads")
|
cur = self._db.execute("SELECT id FROM threads")
|
||||||
tids = [r[0] for r in cur.fetchall()]
|
tids = [r[0] for r in cur.fetchall()]
|
||||||
return [t for tid in tids if (t := self.get_thread(uuid.UUID(tid))) is not None]
|
return [
|
||||||
|
t for tid in tids if (t := self.get_thread(uuid.UUID(tid))) is not None
|
||||||
|
]
|
||||||
ids = [str(a.id) for a in c]
|
ids = [str(a.id) for a in c]
|
||||||
placeholders = ",".join("?" for _ in ids)
|
placeholders = ",".join("?" for _ in ids)
|
||||||
# GROUP BY HAVING COUNT(*) = len(containing)
|
# GROUP BY HAVING COUNT(*) = len(containing)
|
||||||
@@ -199,7 +205,9 @@ class SQLiteStore:
|
|||||||
|
|
||||||
# -- messages --
|
# -- messages --
|
||||||
|
|
||||||
def append_message(self, thread_id: uuid.UUID, sender: Agent, content: str) -> Message:
|
def append_message(
|
||||||
|
self, thread_id: uuid.UUID, sender: Agent, content: str
|
||||||
|
) -> Message:
|
||||||
# validate thread exists
|
# validate thread exists
|
||||||
if self.get_thread(thread_id) is None:
|
if self.get_thread(thread_id) is None:
|
||||||
raise KeyError(thread_id)
|
raise KeyError(thread_id)
|
||||||
@@ -214,12 +222,13 @@ class SQLiteStore:
|
|||||||
try:
|
try:
|
||||||
self._db.execute("BEGIN IMMEDIATE")
|
self._db.execute("BEGIN IMMEDIATE")
|
||||||
cur2 = self._db.execute(
|
cur2 = self._db.execute(
|
||||||
"SELECT COALESCE(MAX(seq), 0) FROM messages WHERE thread_id=?", (str(thread_id),)
|
"SELECT COALESCE(MAX(seq), 0) FROM messages WHERE thread_id=?",
|
||||||
|
(str(thread_id),),
|
||||||
)
|
)
|
||||||
max_seq = cur2.fetchone()[0]
|
max_seq = cur2.fetchone()[0]
|
||||||
seq = int(max_seq) + 1
|
seq = int(max_seq) + 1
|
||||||
mid = str(uuid.uuid4())
|
mid = str(uuid.uuid4())
|
||||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||||
self._db.execute(
|
self._db.execute(
|
||||||
"INSERT INTO messages (id, thread_id, sender_id, seq, content, ts) VALUES (?, ?, ?, ?, ?, ?)",
|
"INSERT INTO messages (id, thread_id, sender_id, seq, content, ts) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
(mid, str(thread_id), str(sender.id), seq, content, ts),
|
(mid, str(thread_id), str(sender.id), seq, content, ts),
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def test_gng_removed():
|
|
||||||
try:
|
|
||||||
import agentmsgs.utils # noqa: F401
|
|
||||||
|
|
||||||
assert False, "utils should be deleted"
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
pass
|
|
||||||
import agentmsgs
|
|
||||||
|
|
||||||
assert hasattr(agentmsgs, "main") or True
|
|
||||||
|
|
||||||
|
|
||||||
def test_import_does_not_print():
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, "-c", "import agentmsgs"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
assert result.stdout == "", f"import printed: {result.stdout!r}"
|
|
||||||
assert result.returncode == 0
|
|
||||||
+12
-7
@@ -1,11 +1,13 @@
|
|||||||
from agentmsgs.stores.memory import InMemoryStore
|
|
||||||
from agentmsgs.core import ops
|
from agentmsgs.core import ops
|
||||||
|
from agentmsgs.stores.memory import InMemoryStore
|
||||||
|
|
||||||
|
|
||||||
def test_ops_validates_sender_must_be_in_thread():
|
def test_ops_validates_sender_must_be_in_thread():
|
||||||
s = InMemoryStore()
|
s = InMemoryStore()
|
||||||
a = ops.get_or_create_agent(s, "A"); b = ops.get_or_create_agent(s, "B"); c = ops.get_or_create_agent(s, "C")
|
a = ops.get_or_create_agent(s, "A")
|
||||||
t = ops.create_thread(s, {a,b})
|
b = ops.get_or_create_agent(s, "B")
|
||||||
|
c = ops.get_or_create_agent(s, "C")
|
||||||
|
t = ops.create_thread(s, {a, b})
|
||||||
try:
|
try:
|
||||||
ops.append_message(s, t.id, c, "oops")
|
ops.append_message(s, t.id, c, "oops")
|
||||||
assert False, "should raise"
|
assert False, "should raise"
|
||||||
@@ -15,8 +17,9 @@ def test_ops_validates_sender_must_be_in_thread():
|
|||||||
|
|
||||||
def test_ops_has_unread_exclude_own():
|
def test_ops_has_unread_exclude_own():
|
||||||
s = InMemoryStore()
|
s = InMemoryStore()
|
||||||
a = ops.get_or_create_agent(s, "A"); b = ops.get_or_create_agent(s, "B")
|
a = ops.get_or_create_agent(s, "A")
|
||||||
t = ops.create_thread(s, {a,b})
|
b = ops.get_or_create_agent(s, "B")
|
||||||
|
t = ops.create_thread(s, {a, b})
|
||||||
ops.append_message(s, t.id, a, "a1")
|
ops.append_message(s, t.id, a, "a1")
|
||||||
# b has unread, a's own message shouldn't count if exclude_own
|
# b has unread, a's own message shouldn't count if exclude_own
|
||||||
assert ops.has_unread(s, t.id, b) is True
|
assert ops.has_unread(s, t.id, b) is True
|
||||||
@@ -28,8 +31,10 @@ def test_ops_has_unread_exclude_own():
|
|||||||
|
|
||||||
def test_ops_join_and_delete():
|
def test_ops_join_and_delete():
|
||||||
s = InMemoryStore()
|
s = InMemoryStore()
|
||||||
a = ops.get_or_create_agent(s, "A"); b = ops.get_or_create_agent(s, "B"); c = ops.get_or_create_agent(s, "C")
|
a = ops.get_or_create_agent(s, "A")
|
||||||
t = ops.create_thread(s, {a,b})
|
b = ops.get_or_create_agent(s, "B")
|
||||||
|
c = ops.get_or_create_agent(s, "C")
|
||||||
|
t = ops.create_thread(s, {a, b})
|
||||||
t2 = ops.join_thread(s, t.id, c)
|
t2 = ops.join_thread(s, t.id, c)
|
||||||
assert c in t2.participants
|
assert c in t2.participants
|
||||||
ops.delete_agent(s, c.id)
|
ops.delete_agent(s, c.id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from agentmsgs.stores.memory import InMemoryStore
|
from agentmsgs.stores.memory import InMemoryStore
|
||||||
|
|
||||||
|
|
||||||
def test_memory_create_and_find_thread():
|
def test_memory_create_and_find_thread():
|
||||||
s = InMemoryStore()
|
s = InMemoryStore()
|
||||||
alice = s.get_or_create_agent("Alice")
|
alice = s.get_or_create_agent("Alice")
|
||||||
@@ -10,12 +11,14 @@ def test_memory_create_and_find_thread():
|
|||||||
assert t in s.find_threads({alice, bob})
|
assert t in s.find_threads({alice, bob})
|
||||||
assert s.find_threads({alice, bob, s.get_or_create_agent("Charlie")}) == []
|
assert s.find_threads({alice, bob, s.get_or_create_agent("Charlie")}) == []
|
||||||
|
|
||||||
|
|
||||||
def test_memory_soft_unique_name():
|
def test_memory_soft_unique_name():
|
||||||
s = InMemoryStore()
|
s = InMemoryStore()
|
||||||
a1 = s.get_or_create_agent("Alice")
|
a1 = s.get_or_create_agent("Alice")
|
||||||
a2 = s.get_or_create_agent("Alice")
|
a2 = s.get_or_create_agent("Alice")
|
||||||
assert a1.id == a2.id
|
assert a1.id == a2.id
|
||||||
|
|
||||||
|
|
||||||
def test_memory_delete_account():
|
def test_memory_delete_account():
|
||||||
s = InMemoryStore()
|
s = InMemoryStore()
|
||||||
a = s.get_or_create_agent("Alice")
|
a = s.get_or_create_agent("Alice")
|
||||||
@@ -23,10 +26,12 @@ def test_memory_delete_account():
|
|||||||
assert s.get_agent_by_id(a.id) is None
|
assert s.get_agent_by_id(a.id) is None
|
||||||
assert s.get_agent_by_name("Alice") is None
|
assert s.get_agent_by_name("Alice") is None
|
||||||
|
|
||||||
|
|
||||||
def test_memory_append_and_list():
|
def test_memory_append_and_list():
|
||||||
s = InMemoryStore()
|
s = InMemoryStore()
|
||||||
a = s.get_or_create_agent("A"); b = s.get_or_create_agent("B")
|
a = s.get_or_create_agent("A")
|
||||||
t = s.create_thread({a,b})
|
b = s.get_or_create_agent("B")
|
||||||
|
t = s.create_thread({a, b})
|
||||||
m1 = s.append_message(t.id, a, "hi")
|
m1 = s.append_message(t.id, a, "hi")
|
||||||
m2 = s.append_message(t.id, b, "yo")
|
m2 = s.append_message(t.id, b, "yo")
|
||||||
assert m1.seq == 1 and m2.seq == 2
|
assert m1.seq == 1 and m2.seq == 2
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
from agentmsgs.core.store import Store
|
from agentmsgs.core.store import Store
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
def test_store_is_protocol():
|
def test_store_is_protocol():
|
||||||
assert issubclass(Store, Protocol)
|
assert issubclass(Store, Protocol)
|
||||||
# check required methods exist
|
# check required methods exist
|
||||||
for m in ["get_or_create_agent","get_agent_by_name","get_agent_by_id","create_agent","delete_agent","list_agents","create_thread","get_thread","find_threads","add_participant","remove_participant","delete_thread","append_message","list_messages","get_cursor","set_cursor"]:
|
for m in [
|
||||||
|
"get_or_create_agent",
|
||||||
|
"get_agent_by_name",
|
||||||
|
"get_agent_by_id",
|
||||||
|
"create_agent",
|
||||||
|
"delete_agent",
|
||||||
|
"list_agents",
|
||||||
|
"create_thread",
|
||||||
|
"get_thread",
|
||||||
|
"find_threads",
|
||||||
|
"add_participant",
|
||||||
|
"remove_participant",
|
||||||
|
"delete_thread",
|
||||||
|
"append_message",
|
||||||
|
"list_messages",
|
||||||
|
"get_cursor",
|
||||||
|
"set_cursor",
|
||||||
|
]:
|
||||||
assert hasattr(Store, m), f"missing {m}"
|
assert hasattr(Store, m), f"missing {m}"
|
||||||
|
|||||||
+28
-11
@@ -1,18 +1,24 @@
|
|||||||
import tempfile, pathlib
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from agentmsgs.stores.sqlite import SQLiteStore
|
from agentmsgs.stores.sqlite import SQLiteStore
|
||||||
|
|
||||||
|
|
||||||
def test_sqlite_persists_across_handles():
|
def test_sqlite_persists_across_handles():
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
p = pathlib.Path(d)/"test.db"
|
p = pathlib.Path(d) / "test.db"
|
||||||
s1 = SQLiteStore(p)
|
s1 = SQLiteStore(p)
|
||||||
try:
|
try:
|
||||||
a = s1.get_or_create_agent("Alice"); b = s1.get_or_create_agent("Bob")
|
a = s1.get_or_create_agent("Alice")
|
||||||
t = s1.create_thread({a,b})
|
b = s1.get_or_create_agent("Bob")
|
||||||
|
t = s1.create_thread({a, b})
|
||||||
s1.append_message(t.id, a, "hi")
|
s1.append_message(t.id, a, "hi")
|
||||||
# new handle same file
|
# new handle same file
|
||||||
s2 = SQLiteStore(p)
|
s2 = SQLiteStore(p)
|
||||||
try:
|
try:
|
||||||
assert s2.get_agent_by_name("Alice").id == a.id
|
a2 = s2.get_agent_by_name("Alice")
|
||||||
|
assert a2 is not None
|
||||||
|
assert a2.id == a.id
|
||||||
assert len(s2.list_messages(t.id)) == 1
|
assert len(s2.list_messages(t.id)) == 1
|
||||||
assert s2.find_threads({a})[0].id == t.id
|
assert s2.find_threads({a})[0].id == t.id
|
||||||
finally:
|
finally:
|
||||||
@@ -20,17 +26,28 @@ def test_sqlite_persists_across_handles():
|
|||||||
finally:
|
finally:
|
||||||
s1.close()
|
s1.close()
|
||||||
|
|
||||||
|
|
||||||
def test_sqlite_concurrent_append():
|
def test_sqlite_concurrent_append():
|
||||||
import pathlib, tempfile
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from agentmsgs.stores.sqlite import SQLiteStore
|
from agentmsgs.stores.sqlite import SQLiteStore
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
p = pathlib.Path(d)/"c.db"
|
p = pathlib.Path(d) / "c.db"
|
||||||
s1 = SQLiteStore(p); s2 = SQLiteStore(p)
|
s1 = SQLiteStore(p)
|
||||||
|
s2 = SQLiteStore(p)
|
||||||
try:
|
try:
|
||||||
a = s1.get_or_create_agent("A"); b = s1.get_or_create_agent("B")
|
a = s1.get_or_create_agent("A")
|
||||||
|
b = s1.get_or_create_agent("B")
|
||||||
# s2 sees same agents via file
|
# s2 sees same agents via file
|
||||||
a2 = s2.get_agent_by_name("A"); b2 = s2.get_agent_by_name("B")
|
a2 = s2.get_agent_by_name("A")
|
||||||
t = s1.create_thread({a,b})
|
b2 = s2.get_agent_by_name("B")
|
||||||
|
assert a2 is not None
|
||||||
|
assert b2 is not None
|
||||||
|
assert a.id == a2.id
|
||||||
|
assert b.id == b2.id
|
||||||
|
t = s1.create_thread({a, b})
|
||||||
s1.append_message(t.id, a, "from s1")
|
s1.append_message(t.id, a, "from s1")
|
||||||
s2.append_message(t.id, b2, "from s2")
|
s2.append_message(t.id, b2, "from s2")
|
||||||
assert len(s1.list_messages(t.id)) == 2
|
assert len(s1.list_messages(t.id)) == 2
|
||||||
|
|||||||
+19
-5
@@ -1,6 +1,8 @@
|
|||||||
import uuid
|
|
||||||
from agentmsgs.core.types import Agent, Thread, Message
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from agentmsgs.core.types import Agent, Thread
|
||||||
|
|
||||||
|
|
||||||
def test_agent_identity_on_id_not_name():
|
def test_agent_identity_on_id_not_name():
|
||||||
a1 = Agent(id=uuid.uuid4(), name="Alice")
|
a1 = Agent(id=uuid.uuid4(), name="Alice")
|
||||||
@@ -8,17 +10,29 @@ def test_agent_identity_on_id_not_name():
|
|||||||
assert a1 != a2 # different id => not equal even though name same
|
assert a1 != a2 # different id => not equal even though name same
|
||||||
assert hash(a1) != hash(a2)
|
assert hash(a1) != hash(a2)
|
||||||
|
|
||||||
|
|
||||||
def test_agent_hash_on_id():
|
def test_agent_hash_on_id():
|
||||||
uid = uuid.uuid4()
|
uid = uuid.uuid4()
|
||||||
a1 = Agent(id=uid, name="Alice")
|
a1 = Agent(id=uid, name="Alice")
|
||||||
a2 = Agent(id=uid, name="Bob") # same id, different name => equal per design (id is identity)
|
a2 = Agent(
|
||||||
|
id=uid, name="Bob"
|
||||||
|
) # same id, different name => equal per design (id is identity)
|
||||||
assert a1 == a2
|
assert a1 == a2
|
||||||
assert hash(a1) == hash(a2)
|
assert hash(a1) == hash(a2)
|
||||||
|
|
||||||
|
|
||||||
def test_thread_multiple_with_same_participants_allowed():
|
def test_thread_multiple_with_same_participants_allowed():
|
||||||
a = Agent(id=uuid.uuid4(), name="A")
|
a = Agent(id=uuid.uuid4(), name="A")
|
||||||
b = Agent(id=uuid.uuid4(), name="B")
|
b = Agent(id=uuid.uuid4(), name="B")
|
||||||
t1 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc))
|
t1 = Thread(
|
||||||
t2 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc))
|
id=uuid.uuid4(),
|
||||||
|
participants=frozenset({a, b}),
|
||||||
|
created_at=datetime.datetime.now(datetime.UTC),
|
||||||
|
)
|
||||||
|
t2 = Thread(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
participants=frozenset({a, b}),
|
||||||
|
created_at=datetime.datetime.now(datetime.UTC),
|
||||||
|
)
|
||||||
assert t1 != t2
|
assert t1 != t2
|
||||||
assert t1.participants == t2.participants
|
assert t1.participants == t2.participants
|
||||||
|
|||||||
Reference in New Issue
Block a user