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.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:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .app import App
|
||||
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:
|
||||
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)
|
||||
|
||||
@@ -36,7 +36,9 @@ def leave_thread(store: Store, tid: uuid.UUID, agent: Agent) -> Thread:
|
||||
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)
|
||||
|
||||
|
||||
@@ -48,7 +50,9 @@ def mark_read(store: Store, tid: uuid.UUID, agent: Agent, seq: int) -> None:
|
||||
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)
|
||||
msgs = store.list_messages(tid, after_seq=cur)
|
||||
if exclude_own:
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from typing import Protocol
|
||||
|
||||
import uuid
|
||||
from .types import Agent, Thread, Message
|
||||
from typing import Protocol
|
||||
|
||||
from .types import Agent, Message, Thread
|
||||
|
||||
|
||||
class Store(Protocol):
|
||||
@@ -17,7 +19,11 @@ class Store(Protocol):
|
||||
def add_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 append_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 append_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 set_cursor(self, thread_id: uuid.UUID, agent: Agent, seq: int) -> None: ...
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Agent:
|
||||
id: uuid.UUID
|
||||
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)
|
||||
class Message:
|
||||
@@ -18,6 +25,7 @@ class Message:
|
||||
seq: int
|
||||
ts: datetime.datetime
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Thread:
|
||||
id: uuid.UUID
|
||||
|
||||
@@ -58,7 +58,7 @@ class InMemoryStore:
|
||||
t = Thread(
|
||||
id=uuid.uuid4(),
|
||||
participants=frozenset(participants),
|
||||
created_at=datetime.datetime.now(datetime.timezone.utc),
|
||||
created_at=datetime.datetime.now(datetime.UTC),
|
||||
)
|
||||
self._threads[t.id] = t
|
||||
return t
|
||||
@@ -72,13 +72,17 @@ class InMemoryStore:
|
||||
|
||||
def add_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
|
||||
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
|
||||
return nt
|
||||
|
||||
def remove_participant(self, thread_id: uuid.UUID, agent: Agent) -> Thread:
|
||||
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
|
||||
return nt
|
||||
|
||||
@@ -90,7 +94,9 @@ class InMemoryStore:
|
||||
if k[0] == id:
|
||||
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:
|
||||
raise KeyError(thread_id)
|
||||
if sender not in self._threads[thread_id].participants:
|
||||
@@ -101,7 +107,7 @@ class InMemoryStore:
|
||||
sender=sender,
|
||||
content=content,
|
||||
seq=seq,
|
||||
ts=datetime.datetime.now(datetime.timezone.utc),
|
||||
ts=datetime.datetime.now(datetime.UTC),
|
||||
)
|
||||
self._msgs[thread_id].append(m)
|
||||
return m
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from agentmsgs.core.types import Agent, Message, Thread
|
||||
from agentmsgs.utils import WORKSPACE_ROOT
|
||||
|
||||
_SCHEMA = """
|
||||
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:
|
||||
def __init__(self, path: Path = Path.home() / ".agentmsgs.db") -> None:
|
||||
def __init__(self, path: Path = WORKSPACE_ROOT / ".agentmsgs.db") -> None:
|
||||
self.path = Path(path)
|
||||
# ensure 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:
|
||||
# INSERT OR IGNORE + SELECT pattern
|
||||
new_id = str(uuid.uuid4())
|
||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
self._db.execute(
|
||||
"INSERT OR IGNORE INTO agents (id, name, created_at) VALUES (?, ?, ?)",
|
||||
(new_id, name, ts),
|
||||
@@ -62,7 +62,7 @@ class SQLiteStore:
|
||||
|
||||
def create_agent(self, name: str) -> Agent:
|
||||
new_id = str(uuid.uuid4())
|
||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
try:
|
||||
self._db.execute(
|
||||
"INSERT INTO agents (id, name, created_at) VALUES (?, ?, ?)",
|
||||
@@ -104,10 +104,12 @@ class SQLiteStore:
|
||||
if len(participants) < 2:
|
||||
raise ValueError("need >=2 participants")
|
||||
tid = str(uuid.uuid4())
|
||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
self._db.execute("BEGIN")
|
||||
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:
|
||||
self._db.execute(
|
||||
"INSERT INTO thread_participants (thread_id, agent_id) VALUES (?, ?)",
|
||||
@@ -124,7 +126,9 @@ class SQLiteStore:
|
||||
)
|
||||
|
||||
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()
|
||||
if row is None:
|
||||
return None
|
||||
@@ -151,7 +155,9 @@ class SQLiteStore:
|
||||
if not c:
|
||||
cur = self._db.execute("SELECT id FROM threads")
|
||||
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]
|
||||
placeholders = ",".join("?" for _ in ids)
|
||||
# GROUP BY HAVING COUNT(*) = len(containing)
|
||||
@@ -199,7 +205,9 @@ class SQLiteStore:
|
||||
|
||||
# -- 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
|
||||
if self.get_thread(thread_id) is None:
|
||||
raise KeyError(thread_id)
|
||||
@@ -214,12 +222,13 @@ class SQLiteStore:
|
||||
try:
|
||||
self._db.execute("BEGIN IMMEDIATE")
|
||||
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]
|
||||
seq = int(max_seq) + 1
|
||||
mid = str(uuid.uuid4())
|
||||
ts = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
ts = datetime.datetime.now(datetime.UTC).isoformat()
|
||||
self._db.execute(
|
||||
"INSERT INTO messages (id, thread_id, sender_id, seq, content, ts) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(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
|
||||
+9
-4
@@ -1,10 +1,12 @@
|
||||
from agentmsgs.stores.memory import InMemoryStore
|
||||
from agentmsgs.core import ops
|
||||
from agentmsgs.stores.memory import InMemoryStore
|
||||
|
||||
|
||||
def test_ops_validates_sender_must_be_in_thread():
|
||||
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")
|
||||
b = ops.get_or_create_agent(s, "B")
|
||||
c = ops.get_or_create_agent(s, "C")
|
||||
t = ops.create_thread(s, {a, b})
|
||||
try:
|
||||
ops.append_message(s, t.id, c, "oops")
|
||||
@@ -15,7 +17,8 @@ def test_ops_validates_sender_must_be_in_thread():
|
||||
|
||||
def test_ops_has_unread_exclude_own():
|
||||
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")
|
||||
b = ops.get_or_create_agent(s, "B")
|
||||
t = ops.create_thread(s, {a, b})
|
||||
ops.append_message(s, t.id, a, "a1")
|
||||
# b has unread, a's own message shouldn't count if exclude_own
|
||||
@@ -28,7 +31,9 @@ def test_ops_has_unread_exclude_own():
|
||||
|
||||
def test_ops_join_and_delete():
|
||||
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")
|
||||
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)
|
||||
assert c in t2.participants
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from agentmsgs.stores.memory import InMemoryStore
|
||||
|
||||
|
||||
def test_memory_create_and_find_thread():
|
||||
s = InMemoryStore()
|
||||
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 s.find_threads({alice, bob, s.get_or_create_agent("Charlie")}) == []
|
||||
|
||||
|
||||
def test_memory_soft_unique_name():
|
||||
s = InMemoryStore()
|
||||
a1 = s.get_or_create_agent("Alice")
|
||||
a2 = s.get_or_create_agent("Alice")
|
||||
assert a1.id == a2.id
|
||||
|
||||
|
||||
def test_memory_delete_account():
|
||||
s = InMemoryStore()
|
||||
a = s.get_or_create_agent("Alice")
|
||||
@@ -23,9 +26,11 @@ def test_memory_delete_account():
|
||||
assert s.get_agent_by_id(a.id) is None
|
||||
assert s.get_agent_by_name("Alice") is None
|
||||
|
||||
|
||||
def test_memory_append_and_list():
|
||||
s = InMemoryStore()
|
||||
a = s.get_or_create_agent("A"); b = s.get_or_create_agent("B")
|
||||
a = s.get_or_create_agent("A")
|
||||
b = s.get_or_create_agent("B")
|
||||
t = s.create_thread({a, b})
|
||||
m1 = s.append_message(t.id, a, "hi")
|
||||
m2 = s.append_message(t.id, b, "yo")
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
from typing import Protocol
|
||||
|
||||
from agentmsgs.core.store import Store
|
||||
import inspect
|
||||
|
||||
|
||||
def test_store_is_protocol():
|
||||
assert issubclass(Store, Protocol)
|
||||
# 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}"
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import tempfile, pathlib
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
from agentmsgs.stores.sqlite import SQLiteStore
|
||||
|
||||
|
||||
def test_sqlite_persists_across_handles():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = pathlib.Path(d) / "test.db"
|
||||
s1 = SQLiteStore(p)
|
||||
try:
|
||||
a = s1.get_or_create_agent("Alice"); b = s1.get_or_create_agent("Bob")
|
||||
a = s1.get_or_create_agent("Alice")
|
||||
b = s1.get_or_create_agent("Bob")
|
||||
t = s1.create_thread({a, b})
|
||||
s1.append_message(t.id, a, "hi")
|
||||
# new handle same file
|
||||
s2 = SQLiteStore(p)
|
||||
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 s2.find_threads({a})[0].id == t.id
|
||||
finally:
|
||||
@@ -20,16 +26,27 @@ def test_sqlite_persists_across_handles():
|
||||
finally:
|
||||
s1.close()
|
||||
|
||||
|
||||
def test_sqlite_concurrent_append():
|
||||
import pathlib, tempfile
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
from agentmsgs.stores.sqlite import SQLiteStore
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = pathlib.Path(d) / "c.db"
|
||||
s1 = SQLiteStore(p); s2 = SQLiteStore(p)
|
||||
s1 = SQLiteStore(p)
|
||||
s2 = SQLiteStore(p)
|
||||
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
|
||||
a2 = s2.get_agent_by_name("A"); b2 = s2.get_agent_by_name("B")
|
||||
a2 = s2.get_agent_by_name("A")
|
||||
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")
|
||||
s2.append_message(t.id, b2, "from s2")
|
||||
|
||||
+19
-5
@@ -1,6 +1,8 @@
|
||||
import uuid
|
||||
from agentmsgs.core.types import Agent, Thread, Message
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from agentmsgs.core.types import Agent, Thread
|
||||
|
||||
|
||||
def test_agent_identity_on_id_not_name():
|
||||
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 hash(a1) != hash(a2)
|
||||
|
||||
|
||||
def test_agent_hash_on_id():
|
||||
uid = uuid.uuid4()
|
||||
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 hash(a1) == hash(a2)
|
||||
|
||||
|
||||
def test_thread_multiple_with_same_participants_allowed():
|
||||
a = Agent(id=uuid.uuid4(), name="A")
|
||||
b = Agent(id=uuid.uuid4(), name="B")
|
||||
t1 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc))
|
||||
t2 = Thread(id=uuid.uuid4(), participants=frozenset({a,b}), created_at=datetime.datetime.now(datetime.timezone.utc))
|
||||
t1 = Thread(
|
||||
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.participants == t2.participants
|
||||
|
||||
Reference in New Issue
Block a user