agentforge-chat 0.2.1__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.
@@ -0,0 +1,276 @@
1
+ """`SqliteChatHistory` — `ChatHistoryStore` over SQLite via aiosqlite.
2
+
3
+ Two tables: ``chat_turns`` (one row per turn) and ``chat_sessions``
4
+ (one row per session). Indexed on ``(session_id, created_at)`` so
5
+ ``load()`` is sub-linear w.r.t. total turn count.
6
+
7
+ Schema is created via ``init_schema()`` / ``from_path()``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from collections.abc import Mapping
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+ from types import TracebackType
17
+ from typing import Any
18
+
19
+ import aiosqlite
20
+ from agentforge_core.contracts.chat import ChatHistoryStore
21
+ from agentforge_core.production.exceptions import ModuleError
22
+ from agentforge_core.values.chat import ChatTurn, SessionInfo
23
+ from agentforge_core.values.messages import ToolCall
24
+
25
+ _SCHEMA_SQL = """
26
+ CREATE TABLE IF NOT EXISTS chat_sessions (
27
+ id TEXT PRIMARY KEY,
28
+ owner TEXT,
29
+ created_at TEXT NOT NULL,
30
+ last_active_at TEXT NOT NULL,
31
+ metadata TEXT NOT NULL
32
+ );
33
+ CREATE INDEX IF NOT EXISTS idx_sessions_owner
34
+ ON chat_sessions(owner);
35
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_active
36
+ ON chat_sessions(last_active_at);
37
+ CREATE TABLE IF NOT EXISTS chat_turns (
38
+ id TEXT PRIMARY KEY,
39
+ session_id TEXT NOT NULL,
40
+ role TEXT NOT NULL,
41
+ content TEXT NOT NULL,
42
+ timestamp TEXT NOT NULL,
43
+ run_id TEXT,
44
+ tool_calls TEXT NOT NULL,
45
+ tool_call_id TEXT,
46
+ tokens_in INTEGER NOT NULL,
47
+ tokens_out INTEGER NOT NULL,
48
+ cost_usd REAL NOT NULL,
49
+ metadata TEXT NOT NULL
50
+ );
51
+ CREATE INDEX IF NOT EXISTS idx_turns_session_ts
52
+ ON chat_turns(session_id, timestamp);
53
+ """
54
+
55
+
56
+ class SqliteChatHistory(ChatHistoryStore):
57
+ """`ChatHistoryStore` backed by a single SQLite file."""
58
+
59
+ def __init__(self, *, connection: aiosqlite.Connection) -> None:
60
+ self._db = connection
61
+
62
+ @classmethod
63
+ async def from_path(cls, path: str | Path) -> SqliteChatHistory:
64
+ """Open or create a SQLite database at ``path``.
65
+
66
+ ``":memory:"`` is allowed for tests; on disk the parent
67
+ directory must already exist.
68
+ """
69
+ connection = await aiosqlite.connect(str(path))
70
+ connection.row_factory = aiosqlite.Row
71
+ await connection.executescript(_SCHEMA_SQL)
72
+ await connection.commit()
73
+ return cls(connection=connection)
74
+
75
+ async def __aenter__(self) -> SqliteChatHistory:
76
+ return self
77
+
78
+ async def __aexit__(
79
+ self,
80
+ exc_type: type[BaseException] | None,
81
+ exc: BaseException | None,
82
+ tb: TracebackType | None,
83
+ ) -> None:
84
+ await self.close()
85
+
86
+ async def close(self) -> None:
87
+ await self._db.close()
88
+
89
+ async def append(self, turn: ChatTurn) -> None:
90
+ ts_iso = turn.timestamp.isoformat()
91
+ await self._db.execute(
92
+ """INSERT OR REPLACE INTO chat_turns
93
+ (id, session_id, role, content, timestamp, run_id,
94
+ tool_calls, tool_call_id, tokens_in, tokens_out,
95
+ cost_usd, metadata)
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
97
+ (
98
+ turn.id,
99
+ turn.session_id,
100
+ turn.role,
101
+ turn.content,
102
+ ts_iso,
103
+ turn.run_id,
104
+ json.dumps([tc.model_dump(mode="json") for tc in turn.tool_calls]),
105
+ turn.tool_call_id,
106
+ turn.tokens_in,
107
+ turn.tokens_out,
108
+ turn.cost_usd,
109
+ json.dumps(turn.metadata),
110
+ ),
111
+ )
112
+ await self._upsert_session(turn.session_id, ts_iso)
113
+ await self._db.commit()
114
+
115
+ async def _upsert_session(self, session_id: str, now_iso: str) -> None:
116
+ await self._db.execute(
117
+ """INSERT INTO chat_sessions
118
+ (id, owner, created_at, last_active_at, metadata)
119
+ VALUES (?, NULL, ?, ?, ?)
120
+ ON CONFLICT(id) DO UPDATE SET last_active_at=excluded.last_active_at""",
121
+ (session_id, now_iso, now_iso, "{}"),
122
+ )
123
+
124
+ async def load(
125
+ self,
126
+ session_id: str,
127
+ *,
128
+ limit: int | None = None,
129
+ before: datetime | None = None,
130
+ after: datetime | None = None,
131
+ roles: list[str] | None = None,
132
+ ) -> list[ChatTurn]:
133
+ where = ["session_id = ?"]
134
+ params: list[Any] = [session_id]
135
+ if before is not None:
136
+ where.append("timestamp < ?")
137
+ params.append(before.isoformat())
138
+ if after is not None:
139
+ where.append("timestamp > ?")
140
+ params.append(after.isoformat())
141
+ if roles is not None:
142
+ placeholders = ", ".join("?" * len(roles))
143
+ where.append(f"role IN ({placeholders})") # nosec B608 — `?` placeholders only
144
+ params.extend(roles)
145
+ sql = (
146
+ "SELECT * FROM chat_turns WHERE " # noqa: S608 # nosec B608
147
+ + " AND ".join(where)
148
+ + " ORDER BY timestamp"
149
+ )
150
+ if limit is not None:
151
+ sql += " LIMIT ?"
152
+ params.append(limit)
153
+ async with self._db.execute(sql, params) as cur:
154
+ rows = await cur.fetchall()
155
+ return [_row_to_turn(row) for row in rows]
156
+
157
+ async def count(self, session_id: str) -> int:
158
+ async with self._db.execute(
159
+ "SELECT COUNT(*) FROM chat_turns WHERE session_id = ?",
160
+ (session_id,),
161
+ ) as cur:
162
+ row = await cur.fetchone()
163
+ return int(row[0]) if row else 0
164
+
165
+ async def delete_session(self, session_id: str) -> int:
166
+ cur = await self._db.execute(
167
+ "DELETE FROM chat_turns WHERE session_id = ?",
168
+ (session_id,),
169
+ )
170
+ removed = cur.rowcount or 0
171
+ await self._db.execute(
172
+ "DELETE FROM chat_sessions WHERE id = ?",
173
+ (session_id,),
174
+ )
175
+ await self._db.commit()
176
+ return removed
177
+
178
+ async def list_sessions(
179
+ self,
180
+ *,
181
+ owner: str | None = None,
182
+ limit: int = 100,
183
+ before: datetime | None = None,
184
+ ) -> list[SessionInfo]:
185
+ where: list[str] = []
186
+ params: list[Any] = []
187
+ if owner is not None:
188
+ where.append("owner = ?")
189
+ params.append(owner)
190
+ if before is not None:
191
+ where.append("last_active_at < ?")
192
+ params.append(before.isoformat())
193
+ sql = "SELECT * FROM chat_sessions"
194
+ if where:
195
+ sql += " WHERE " + " AND ".join(where)
196
+ sql += " ORDER BY last_active_at DESC LIMIT ?" # nosec B608
197
+ params.append(limit)
198
+ async with self._db.execute(sql, params) as cur:
199
+ rows = await cur.fetchall()
200
+ return [await self._row_to_info(row) for row in rows]
201
+
202
+ async def update_session_metadata(self, session_id: str, metadata: Mapping[str, Any]) -> None:
203
+ async with self._db.execute(
204
+ "SELECT metadata, owner FROM chat_sessions WHERE id = ?",
205
+ (session_id,),
206
+ ) as cur:
207
+ row = await cur.fetchone()
208
+ if row is None:
209
+ raise ModuleError(f"Cannot update metadata for unknown session {session_id!r}")
210
+ existing = json.loads(row["metadata"])
211
+ existing.update(dict(metadata))
212
+ owner = metadata.get("owner", row["owner"])
213
+ await self._db.execute(
214
+ "UPDATE chat_sessions SET metadata = ?, owner = ? WHERE id = ?",
215
+ (json.dumps(existing), owner, session_id),
216
+ )
217
+ await self._db.commit()
218
+
219
+ async def expire_before(self, cutoff: datetime) -> int:
220
+ cutoff_iso = cutoff.isoformat()
221
+ await self._db.execute(
222
+ """DELETE FROM chat_turns WHERE session_id IN (
223
+ SELECT id FROM chat_sessions WHERE last_active_at < ?
224
+ )""",
225
+ (cutoff_iso,),
226
+ )
227
+ cur = await self._db.execute(
228
+ "DELETE FROM chat_sessions WHERE last_active_at < ?",
229
+ (cutoff_iso,),
230
+ )
231
+ removed = cur.rowcount or 0
232
+ await self._db.commit()
233
+ return removed
234
+
235
+ def capabilities(self) -> set[str]:
236
+ return {"ttl"}
237
+
238
+ async def _row_to_info(self, row: Any) -> SessionInfo:
239
+ async with self._db.execute(
240
+ "SELECT COUNT(*), COALESCE(SUM(cost_usd), 0.0) FROM chat_turns WHERE session_id = ?",
241
+ (row["id"],),
242
+ ) as cur:
243
+ agg = await cur.fetchone()
244
+ count = int(agg[0]) if agg else 0
245
+ cost = float(agg[1]) if agg else 0.0
246
+ return SessionInfo(
247
+ id=row["id"],
248
+ owner=row["owner"],
249
+ created_at=datetime.fromisoformat(row["created_at"]),
250
+ last_active_at=datetime.fromisoformat(row["last_active_at"]),
251
+ turn_count=count,
252
+ total_cost_usd=cost,
253
+ metadata=json.loads(row["metadata"]),
254
+ )
255
+
256
+
257
+ def _row_to_turn(row: Any) -> ChatTurn:
258
+ raw_calls = json.loads(row["tool_calls"])
259
+ tool_calls = tuple(ToolCall.model_validate(tc) for tc in raw_calls)
260
+ return ChatTurn(
261
+ id=row["id"],
262
+ session_id=row["session_id"],
263
+ role=row["role"],
264
+ content=row["content"],
265
+ timestamp=datetime.fromisoformat(row["timestamp"]),
266
+ run_id=row["run_id"],
267
+ tool_calls=tool_calls,
268
+ tool_call_id=row["tool_call_id"],
269
+ tokens_in=int(row["tokens_in"]),
270
+ tokens_out=int(row["tokens_out"]),
271
+ cost_usd=float(row["cost_usd"]),
272
+ metadata=json.loads(row["metadata"]),
273
+ )
274
+
275
+
276
+ __all__ = ["SqliteChatHistory"]
@@ -0,0 +1,91 @@
1
+ """Provider-aware tokenisers for `TokenBudget` (feat-020 v0.2).
2
+
3
+ The default `TokenBudget` ships a 4-chars-per-token heuristic
4
+ that works for English-ish prose but drifts widely on code +
5
+ non-ASCII content. For accurate budget enforcement, supply a
6
+ `Tokeniser`: any callable that maps a string to a token count.
7
+
8
+ Two built-ins:
9
+
10
+ - :func:`tiktoken_tokeniser` — counts via the
11
+ `tiktoken` library used by OpenAI-compatible models.
12
+ - :func:`anthropic_tokeniser` — counts via the Anthropic
13
+ SDK's `count_tokens` API.
14
+
15
+ Both lazy-import their backing SDKs and raise :class:`ModuleError`
16
+ with pip remediation when the SDK isn't installed. Users
17
+ provide their own callable when they want a different
18
+ tokeniser.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Callable
24
+ from functools import lru_cache
25
+ from typing import Any, cast
26
+
27
+ from agentforge_core.production.exceptions import ModuleError
28
+
29
+ Tokeniser = Callable[[str], int]
30
+ """Map an input string to a non-negative integer token count."""
31
+
32
+
33
+ @lru_cache(maxsize=8)
34
+ def _load_tiktoken_encoding(model: str) -> Any: # pragma: no cover — exercised via build
35
+ try:
36
+ import tiktoken # noqa: PLC0415
37
+ except ImportError as exc:
38
+ msg = (
39
+ "tiktoken is not installed. Install via "
40
+ "`pip install tiktoken` to use tiktoken_tokeniser."
41
+ )
42
+ raise ModuleError(msg) from exc
43
+ try:
44
+ return tiktoken.encoding_for_model(model)
45
+ except KeyError:
46
+ return tiktoken.get_encoding("cl100k_base")
47
+
48
+
49
+ def tiktoken_tokeniser(model: str = "gpt-4o-mini") -> Tokeniser:
50
+ """Build a tiktoken-backed `Tokeniser` for ``model``.
51
+
52
+ Falls back to the ``cl100k_base`` encoding when the model
53
+ name isn't in tiktoken's registry.
54
+ """
55
+ enc = _load_tiktoken_encoding(model)
56
+
57
+ def _count(text: str) -> int:
58
+ return len(enc.encode(text))
59
+
60
+ return _count
61
+
62
+
63
+ def anthropic_tokeniser() -> Tokeniser: # pragma: no cover — exercised via build
64
+ """Build an Anthropic-SDK-backed `Tokeniser`.
65
+
66
+ Uses the synchronous `count_tokens(text)` method on a
67
+ process-wide Anthropic client. Suitable for offline token
68
+ counting where no API key is needed.
69
+ """
70
+ try:
71
+ import anthropic # noqa: PLC0415
72
+ except ImportError as exc:
73
+ msg = (
74
+ "anthropic SDK is not installed. Install via "
75
+ "`pip install anthropic` to use anthropic_tokeniser."
76
+ )
77
+ raise ModuleError(msg) from exc
78
+
79
+ client = anthropic.Anthropic()
80
+
81
+ def _count(text: str) -> int:
82
+ return int(cast("Any", client).count_tokens(text))
83
+
84
+ return _count
85
+
86
+
87
+ __all__ = [
88
+ "Tokeniser",
89
+ "anthropic_tokeniser",
90
+ "tiktoken_tokeniser",
91
+ ]
@@ -0,0 +1,206 @@
1
+ """Truncation strategies (feat-020).
2
+
3
+ Four built-ins ship in v0.2:
4
+
5
+ - `SlidingWindow(max_turns=N)` — keep the last N turns.
6
+ - `TokenBudget(max_tokens=N)` — keep as many recent turns as fit
7
+ under N tokens (approximate, using a 4-chars-per-token heuristic
8
+ in v0.2; provider-aware tokenisation is a follow-up).
9
+ - `SummariseOldest(threshold_turns=N, summariser=cb)` — keep the
10
+ last N turns verbatim; everything older condenses to a single
11
+ ``system`` turn via the supplied summariser callback.
12
+ - `Hybrid(*strategies)` — pipe input through each strategy in
13
+ order; later strategies see the previous strategy's output.
14
+
15
+ Each respects the conformance invariants documented in
16
+ `agentforge_core.contracts.chat.HistoryTruncationStrategy`:
17
+ order-preserving + tool-call/tool-result pair atomicity.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Awaitable, Callable, Mapping
23
+ from typing import Any
24
+
25
+ from agentforge_core.contracts.chat import HistoryTruncationStrategy
26
+ from agentforge_core.values.chat import ChatTurn
27
+
28
+ from agentforge_chat.tokenisers import Tokeniser
29
+
30
+ SummariserCallback = Callable[[list[ChatTurn]], Awaitable[str]]
31
+ """Async callback that turns a batch of turns into a single
32
+ summary string. The default implementation in `SummariseOldest`
33
+ concatenates contents — production users supply an LLM-backed
34
+ summariser."""
35
+
36
+ _CHARS_PER_TOKEN = 4
37
+ """Approximate chars-per-token heuristic for `TokenBudget`."""
38
+
39
+
40
+ def _approx_tokens(turn: ChatTurn) -> int:
41
+ return max(1, len(turn.content) // _CHARS_PER_TOKEN)
42
+
43
+
44
+ def _keep_pair_atomic(turns: list[ChatTurn]) -> list[ChatTurn]:
45
+ """Drop a leading orphan tool turn that lost its act partner.
46
+
47
+ The truncation contract says tool-call/tool-result pairs must
48
+ not be split. We enforce by dropping any tool turn whose
49
+ preceding assistant turn isn't in the selection.
50
+ """
51
+ if not turns:
52
+ return turns
53
+ out: list[ChatTurn] = []
54
+ last_role: str | None = None
55
+ for t in turns:
56
+ if t.role == "tool" and last_role != "assistant":
57
+ continue
58
+ out.append(t)
59
+ last_role = t.role
60
+ return out
61
+
62
+
63
+ class SlidingWindow(HistoryTruncationStrategy):
64
+ """Keep the most recent ``max_turns`` turns."""
65
+
66
+ def __init__(self, max_turns: int = 50) -> None:
67
+ if max_turns < 1:
68
+ raise ValueError(f"max_turns must be >= 1, got {max_turns}")
69
+ self.max_turns = max_turns
70
+
71
+ async def select(
72
+ self,
73
+ all_turns: list[ChatTurn],
74
+ next_user_message: str,
75
+ context: Mapping[str, Any],
76
+ ) -> list[ChatTurn]:
77
+ del next_user_message, context
78
+ kept = all_turns[-self.max_turns :]
79
+ return _keep_pair_atomic(kept)
80
+
81
+
82
+ class TokenBudget(HistoryTruncationStrategy):
83
+ """Keep recent turns until ``max_tokens`` is exhausted.
84
+
85
+ Token counting defaults to the 4-chars-per-token heuristic.
86
+ Pass a ``tokeniser`` callable to use a provider-aware
87
+ encoder — e.g. :func:`agentforge_chat.tokenisers.tiktoken_tokeniser`
88
+ for OpenAI-compatible models or
89
+ :func:`agentforge_chat.tokenisers.anthropic_tokeniser` for
90
+ Anthropic. The callable maps a string to its token count.
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ max_tokens: int = 64_000,
96
+ *,
97
+ tokeniser: Tokeniser | None = None,
98
+ ) -> None:
99
+ if max_tokens < 1:
100
+ raise ValueError(f"max_tokens must be >= 1, got {max_tokens}")
101
+ self.max_tokens = max_tokens
102
+ self._tokeniser = tokeniser
103
+
104
+ def _tokens_for_text(self, text: str) -> int:
105
+ if self._tokeniser is None:
106
+ return max(1, len(text) // _CHARS_PER_TOKEN)
107
+ return max(0, int(self._tokeniser(text)))
108
+
109
+ def _tokens_for_turn(self, turn: ChatTurn) -> int:
110
+ if self._tokeniser is None:
111
+ return _approx_tokens(turn)
112
+ return max(1, int(self._tokeniser(turn.content)))
113
+
114
+ async def select(
115
+ self,
116
+ all_turns: list[ChatTurn],
117
+ next_user_message: str,
118
+ context: Mapping[str, Any],
119
+ ) -> list[ChatTurn]:
120
+ del context
121
+ # Reserve budget for the next user message itself.
122
+ reserved = self._tokens_for_text(next_user_message)
123
+ remaining = self.max_tokens - reserved
124
+ chosen: list[ChatTurn] = []
125
+ for turn in reversed(all_turns):
126
+ cost = self._tokens_for_turn(turn)
127
+ if cost > remaining:
128
+ break
129
+ chosen.append(turn)
130
+ remaining -= cost
131
+ chosen.reverse()
132
+ return _keep_pair_atomic(chosen)
133
+
134
+
135
+ class SummariseOldest(HistoryTruncationStrategy):
136
+ """Keep the last ``threshold_turns``; condense older turns
137
+ into a single ``system`` summary turn."""
138
+
139
+ def __init__(
140
+ self,
141
+ *,
142
+ threshold_turns: int = 30,
143
+ summariser: SummariserCallback | None = None,
144
+ ) -> None:
145
+ if threshold_turns < 1:
146
+ raise ValueError(f"threshold_turns must be >= 1, got {threshold_turns}")
147
+ self.threshold_turns = threshold_turns
148
+ self.summariser: SummariserCallback = (
149
+ summariser if summariser is not None else _default_summariser
150
+ )
151
+
152
+ async def select(
153
+ self,
154
+ all_turns: list[ChatTurn],
155
+ next_user_message: str,
156
+ context: Mapping[str, Any],
157
+ ) -> list[ChatTurn]:
158
+ del next_user_message, context
159
+ if len(all_turns) <= self.threshold_turns:
160
+ return _keep_pair_atomic(list(all_turns))
161
+ older = all_turns[: -self.threshold_turns]
162
+ recent = all_turns[-self.threshold_turns :]
163
+ summary_text = await self.summariser(older)
164
+ summary = ChatTurn(
165
+ id=f"summary-{older[0].id}-{older[-1].id}",
166
+ session_id=older[0].session_id,
167
+ role="system",
168
+ content=f"[Summary of {len(older)} older turns] {summary_text}",
169
+ timestamp=older[0].timestamp,
170
+ metadata={"agentforge_chat.summary": True},
171
+ )
172
+ return _keep_pair_atomic([summary, *recent])
173
+
174
+
175
+ class Hybrid(HistoryTruncationStrategy):
176
+ """Compose strategies in series: each runs against the
177
+ previous strategy's output."""
178
+
179
+ def __init__(self, *strategies: HistoryTruncationStrategy) -> None:
180
+ if not strategies:
181
+ raise ValueError("Hybrid requires at least one strategy")
182
+ self.strategies = strategies
183
+
184
+ async def select(
185
+ self,
186
+ all_turns: list[ChatTurn],
187
+ next_user_message: str,
188
+ context: Mapping[str, Any],
189
+ ) -> list[ChatTurn]:
190
+ current = list(all_turns)
191
+ for s in self.strategies:
192
+ current = await s.select(current, next_user_message, context)
193
+ return current
194
+
195
+
196
+ async def _default_summariser(turns: list[ChatTurn]) -> str:
197
+ pieces = [f"{t.role}: {t.content}" for t in turns]
198
+ return " | ".join(pieces)[:2000]
199
+
200
+
201
+ __all__ = [
202
+ "Hybrid",
203
+ "SlidingWindow",
204
+ "SummariseOldest",
205
+ "TokenBudget",
206
+ ]
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentforge-chat
3
+ Version: 0.2.1
4
+ Summary: Chat-agent runtime (ChatSession + history drivers + truncation) for AgentForge
5
+ Project-URL: Homepage, https://github.com/Scaffoldic/agentforge-py
6
+ Project-URL: Repository, https://github.com/Scaffoldic/agentforge-py
7
+ Project-URL: Documentation, https://github.com/Scaffoldic/agentforge-py
8
+ Project-URL: Changelog, https://github.com/Scaffoldic/agentforge-py/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/Scaffoldic/agentforge-py/issues
10
+ Author: The AgentForge Authors
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE
13
+ Keywords: agent,ai,chat,chatbot,conversation
14
+ Classifier: Development Status :: 2 - Pre-Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.13
22
+ Requires-Dist: agentforge-core~=0.2.1
23
+ Requires-Dist: agentforge-py~=0.2.1
24
+ Provides-Extra: sqlite
25
+ Requires-Dist: aiosqlite>=0.20; extra == 'sqlite'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # agentforge-chat
29
+
30
+ Chat-agent runtime for AgentForge: `ChatSession`,
31
+ `InMemoryChatHistory` / `SqliteChatHistory` drivers, and four
32
+ truncation strategies (sliding-window, token-budget,
33
+ summarise-oldest, hybrid).
34
+
35
+ See [`docs/features/feat-020-chat-agents.md`](https://github.com/Scaffoldic/agentforge-py/blob/main/docs/features/feat-020-chat-agents.md)
36
+ for the design and runbook.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install agentforge-chat
42
+ # or, with the SQLite driver pre-pulled:
43
+ pip install "agentforge-chat[sqlite]"
44
+ ```
45
+
46
+ ## Three-line chat from a one-shot agent
47
+
48
+ ```python
49
+ from agentforge import Agent
50
+ from agentforge_chat import ChatSession, SqliteChatHistory
51
+
52
+ agent = Agent(model="anthropic:claude-sonnet-4-6", strategy="react")
53
+ session = ChatSession(
54
+ agent=agent,
55
+ history_store=await SqliteChatHistory.from_path("./chat.db"),
56
+ )
57
+ print((await session.send("Hi")).content)
58
+ print((await session.send("What did I just say?")).content)
59
+ ```
@@ -0,0 +1,18 @@
1
+ agentforge_chat/__init__.py,sha256=av4ETJyvjnKJ1uCub6QlpgA82Mu0kEpA03yb3vrmzbA,1050
2
+ agentforge_chat/_idempotency.py,sha256=1ekiRDJC6SxVYklxsKCfW0A80BawNMzD6FHkcpHZS5Q,1232
3
+ agentforge_chat/_locks.py,sha256=stV_5isaEmyk4T1CW5Bs2ExiSZ7F2ayoltJN_PFIwg4,3411
4
+ agentforge_chat/_segment.py,sha256=5EPSLgukC5sroU5lhIr0TK4Tjh52VzU1eMlEg7d4lAg,1339
5
+ agentforge_chat/_window.py,sha256=HOT6vmMkX7Xxm1UPPyoTtBjy_Esm8YZNEsUqV_pjGLc,2944
6
+ agentforge_chat/build.py,sha256=9UXGAWhO8elY98k-_AOmnwc4FxtHBsXQJaPMPnniaL4,4039
7
+ agentforge_chat/history.py,sha256=Uv7rzlOrSHMegXKDJgxTIPoCSgNzguMYVmidSO56DqQ,4510
8
+ agentforge_chat/manifest.yaml,sha256=i9rASO_QPu_74ZxRweKmeiIG9gu8pXR_A_s6VgB0QTc,1023
9
+ agentforge_chat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ agentforge_chat/session.py,sha256=7KA8hZ1efeWUlraZ33WVgrFkObwXnCCcIX93oD6Trhc,19448
11
+ agentforge_chat/sqlite.py,sha256=-p2w8MAiTqLLbCSIiRypNvVwreONCWyhtfKsX-6owks,9591
12
+ agentforge_chat/tokenisers.py,sha256=JgU8ELf6rjuexC4Ha1LLQZyJBT8LZFyfCUs7Y-3WX9Y,2704
13
+ agentforge_chat/truncation.py,sha256=7FX2Mu4RiJ5KmTV4aPOOFeqXyz5TmXmJ0OdHR8jg7aM,6922
14
+ agentforge_chat-0.2.1.dist-info/METADATA,sha256=W4KH6lEmzqpK12K9sYAM6BuhD1psJoeaaADQA4Sg8Ec,2150
15
+ agentforge_chat-0.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
16
+ agentforge_chat-0.2.1.dist-info/entry_points.txt,sha256=hZaTWW0rtnD_FSFT21Cou6kkmcYa7jSSwyr1U2fzGmA,376
17
+ agentforge_chat-0.2.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
18
+ agentforge_chat-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ [agentforge.chat.history]
2
+ memory = agentforge_chat.history:InMemoryChatHistory
3
+ sqlite = agentforge_chat.sqlite:SqliteChatHistory
4
+
5
+ [agentforge.chat.truncation]
6
+ hybrid = agentforge_chat.truncation:Hybrid
7
+ sliding_window = agentforge_chat.truncation:SlidingWindow
8
+ summarise_oldest = agentforge_chat.truncation:SummariseOldest
9
+ token_budget = agentforge_chat.truncation:TokenBudget