forgeoptimizer 0.1.0__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.
Files changed (159) hide show
  1. forgecli/__init__.py +4 -0
  2. forgecli/build/__init__.py +135 -0
  3. forgecli/build/apply.py +218 -0
  4. forgecli/build/caveman_optimize.py +37 -0
  5. forgecli/build/diff_extract.py +218 -0
  6. forgecli/build/llm.py +167 -0
  7. forgecli/build/optimize.py +37 -0
  8. forgecli/build/pipeline.py +76 -0
  9. forgecli/build/retrieval.py +157 -0
  10. forgecli/build/summarize.py +76 -0
  11. forgecli/build/test_run.py +75 -0
  12. forgecli/builder/__init__.py +11 -0
  13. forgecli/builder/builder.py +53 -0
  14. forgecli/builder/editor.py +36 -0
  15. forgecli/builder/formatter.py +31 -0
  16. forgecli/cli/__init__.py +5 -0
  17. forgecli/cli/bootstrap.py +281 -0
  18. forgecli/cli/commands_graph.py +149 -0
  19. forgecli/cli/commands_wrappers.py +80 -0
  20. forgecli/cli/daemon.py +744 -0
  21. forgecli/cli/main.py +234 -0
  22. forgecli/cli/ui.py +56 -0
  23. forgecli/config/__init__.py +28 -0
  24. forgecli/config/loader.py +85 -0
  25. forgecli/config/settings.py +206 -0
  26. forgecli/config/writer.py +90 -0
  27. forgecli/core/__init__.py +35 -0
  28. forgecli/core/container.py +68 -0
  29. forgecli/core/context.py +54 -0
  30. forgecli/core/credentials.py +114 -0
  31. forgecli/core/errors.py +27 -0
  32. forgecli/core/events.py +67 -0
  33. forgecli/core/logging.py +47 -0
  34. forgecli/core/models.py +214 -0
  35. forgecli/core/plugins.py +54 -0
  36. forgecli/core/service.py +22 -0
  37. forgecli/docs/__init__.py +5 -0
  38. forgecli/docs/generator.py +115 -0
  39. forgecli/engine/__init__.py +105 -0
  40. forgecli/engine/context.py +163 -0
  41. forgecli/engine/defaults.py +89 -0
  42. forgecli/engine/events.py +185 -0
  43. forgecli/engine/execution.py +519 -0
  44. forgecli/engine/plugins.py +145 -0
  45. forgecli/engine/runner.py +158 -0
  46. forgecli/engine/stages/__init__.py +33 -0
  47. forgecli/engine/stages/caveman_optimizer.py +47 -0
  48. forgecli/engine/stages/context_optimizer.py +44 -0
  49. forgecli/engine/stages/execution_engine_stage.py +102 -0
  50. forgecli/engine/stages/git_engine.py +30 -0
  51. forgecli/engine/stages/intent_analyzer.py +38 -0
  52. forgecli/engine/stages/model_router.py +65 -0
  53. forgecli/engine/stages/planning_engine.py +45 -0
  54. forgecli/engine/stages/repository_analyzer.py +54 -0
  55. forgecli/engine/stages/validation_engine.py +87 -0
  56. forgecli/git/__init__.py +9 -0
  57. forgecli/git/repo.py +55 -0
  58. forgecli/git/service.py +45 -0
  59. forgecli/graph/__init__.py +56 -0
  60. forgecli/graph/backend_graphify.py +448 -0
  61. forgecli/graph/edge.py +19 -0
  62. forgecli/graph/graph.py +85 -0
  63. forgecli/graph/graphify.py +405 -0
  64. forgecli/graph/indexer.py +82 -0
  65. forgecli/graph/node.py +47 -0
  66. forgecli/graph/repository.py +164 -0
  67. forgecli/memory/__init__.py +12 -0
  68. forgecli/memory/cache.py +61 -0
  69. forgecli/memory/history.py +136 -0
  70. forgecli/memory/store.py +85 -0
  71. forgecli/optimizer/__init__.py +13 -0
  72. forgecli/optimizer/caveman/__init__.py +169 -0
  73. forgecli/optimizer/caveman/cli.py +62 -0
  74. forgecli/optimizer/caveman/decorator.py +67 -0
  75. forgecli/optimizer/caveman/factory.py +51 -0
  76. forgecli/optimizer/caveman/ruleset.py +156 -0
  77. forgecli/optimizer/caveman/state.py +50 -0
  78. forgecli/optimizer/chunker.py +85 -0
  79. forgecli/optimizer/optimizer.py +81 -0
  80. forgecli/optimizer/ponytail/__init__.py +181 -0
  81. forgecli/optimizer/ponytail/cli.py +159 -0
  82. forgecli/optimizer/ponytail/decorator.py +70 -0
  83. forgecli/optimizer/ponytail/factory.py +51 -0
  84. forgecli/optimizer/ponytail/ruleset.py +168 -0
  85. forgecli/optimizer/ponytail/state.py +51 -0
  86. forgecli/optimizer/ranker.py +37 -0
  87. forgecli/optimizer/summarizer.py +44 -0
  88. forgecli/orchestrator/__init__.py +706 -0
  89. forgecli/planner/__init__.py +56 -0
  90. forgecli/planner/agent.py +61 -0
  91. forgecli/planner/plan.py +65 -0
  92. forgecli/planner/planner.py +17 -0
  93. forgecli/planner/render.py +267 -0
  94. forgecli/planner/serialize.py +104 -0
  95. forgecli/planner/software.py +832 -0
  96. forgecli/platform/__init__.py +91 -0
  97. forgecli/platform/core.py +201 -0
  98. forgecli/platform/deps.py +361 -0
  99. forgecli/platform/paths.py +234 -0
  100. forgecli/platform/shell.py +176 -0
  101. forgecli/platform/update.py +253 -0
  102. forgecli/plugins/__init__.py +249 -0
  103. forgecli/prompts/__init__.py +11 -0
  104. forgecli/prompts/loader.py +28 -0
  105. forgecli/prompts/registry.py +32 -0
  106. forgecli/prompts/renderer.py +23 -0
  107. forgecli/providers/__init__.py +39 -0
  108. forgecli/providers/anthropic.py +207 -0
  109. forgecli/providers/base.py +204 -0
  110. forgecli/providers/builtin.py +26 -0
  111. forgecli/providers/conversation.py +74 -0
  112. forgecli/providers/google.py +290 -0
  113. forgecli/providers/http_base.py +207 -0
  114. forgecli/providers/mock.py +111 -0
  115. forgecli/providers/openai.py +206 -0
  116. forgecli/providers/openai_compatible.py +787 -0
  117. forgecli/providers/router.py +340 -0
  118. forgecli/providers/router_state.py +89 -0
  119. forgecli/review/__init__.py +45 -0
  120. forgecli/review/analyzer.py +124 -0
  121. forgecli/review/analyzers/__init__.py +1 -0
  122. forgecli/review/analyzers/architecture.py +255 -0
  123. forgecli/review/analyzers/complexity.py +162 -0
  124. forgecli/review/analyzers/dead_code.py +255 -0
  125. forgecli/review/analyzers/duplicates.py +161 -0
  126. forgecli/review/analyzers/performance.py +161 -0
  127. forgecli/review/analyzers/security.py +244 -0
  128. forgecli/review/finding.py +68 -0
  129. forgecli/review/report.py +321 -0
  130. forgecli/review/repository.py +130 -0
  131. forgecli/review/suggestions.py +98 -0
  132. forgecli/runtime/__init__.py +6 -0
  133. forgecli/runtime/cache_store.py +75 -0
  134. forgecli/runtime/mcp_config.py +118 -0
  135. forgecli/runtime/prepare.py +203 -0
  136. forgecli/runtime/wrappers.py +150 -0
  137. forgecli/sdk/__init__.py +132 -0
  138. forgecli/sdk/events.py +206 -0
  139. forgecli/sdk/interfaces.py +282 -0
  140. forgecli/sdk/loader.py +239 -0
  141. forgecli/sdk/manager.py +678 -0
  142. forgecli/sdk/manifest.py +395 -0
  143. forgecli/sdk/sandbox.py +247 -0
  144. forgecli/sdk/version.py +313 -0
  145. forgecli/templates/__init__.py +9 -0
  146. forgecli/templates/engine.py +37 -0
  147. forgecli/templates/registry.py +32 -0
  148. forgecli/utils/__init__.py +19 -0
  149. forgecli/utils/fs.py +121 -0
  150. forgecli/utils/ids.py +11 -0
  151. forgecli/utils/io.py +27 -0
  152. forgecli/utils/paths.py +78 -0
  153. forgecli/utils/stats.py +179 -0
  154. forgecli/utils/timing.py +25 -0
  155. forgeoptimizer-0.1.0.dist-info/METADATA +177 -0
  156. forgeoptimizer-0.1.0.dist-info/RECORD +159 -0
  157. forgeoptimizer-0.1.0.dist-info/WHEEL +4 -0
  158. forgeoptimizer-0.1.0.dist-info/entry_points.txt +2 -0
  159. forgeoptimizer-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,164 @@
1
+ """Abstract interface for a repository knowledge graph.
2
+
3
+ This module is the boundary between ForgeCLI's graph-aware features and any
4
+ concrete back-end (Graphify today; custom AST-based backends tomorrow). The
5
+ interface is intentionally small: build, query, explain, path, affected.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from collections.abc import Iterable
12
+ from dataclasses import dataclass, field
13
+ from typing import Any
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class GraphNode:
18
+ """A typed view of a single node from the knowledge graph."""
19
+
20
+ id: str
21
+ label: str
22
+ file_type: str | None = None
23
+ source_file: str | None = None
24
+ source_location: str | None = None
25
+ community: int | None = None
26
+ norm_label: str | None = None
27
+ extra: dict[str, Any] = field(default_factory=dict)
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class GraphEdge:
32
+ """A typed view of a directed/undirected edge."""
33
+
34
+ source: str
35
+ target: str
36
+ relation: str
37
+ confidence: str | None = None
38
+ confidence_score: float | None = None
39
+ source_file: str | None = None
40
+ source_location: str | None = None
41
+ weight: float = 1.0
42
+ extra: dict[str, Any] = field(default_factory=dict)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class GraphCommunity:
47
+ """A Leiden-detected community of nodes."""
48
+
49
+ id: int
50
+ size: int
51
+ label: str | None = None
52
+ members: tuple[str, ...] = ()
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class GraphSnapshot:
57
+ """An immutable view of a fully-built knowledge graph."""
58
+
59
+ root: str
60
+ nodes: tuple[GraphNode, ...]
61
+ edges: tuple[GraphEdge, ...]
62
+ communities: tuple[GraphCommunity, ...] = ()
63
+ directed: bool = False
64
+ multigraph: bool = False
65
+ metadata: dict[str, Any] = field(default_factory=dict)
66
+
67
+ def node(self, node_id: str) -> GraphNode | None:
68
+ """Return the node with ``node_id`` or ``None``."""
69
+ for node in self.nodes:
70
+ if node.id == node_id:
71
+ return node
72
+ return None
73
+
74
+ def neighbors(self, node_id: str) -> list[GraphEdge]:
75
+ """Return all edges incident to ``node_id`` (in or out)."""
76
+ return [e for e in self.edges if e.source == node_id or e.target == node_id]
77
+
78
+ def search(self, query: str, *, limit: int = 20) -> list[GraphNode]:
79
+ """Return nodes whose ``label`` or ``norm_label`` contain ``query``."""
80
+ needle = query.lower()
81
+ hits: list[GraphNode] = []
82
+ for node in self.nodes:
83
+ if needle in (node.label or "").lower() or needle in (node.norm_label or "").lower():
84
+ hits.append(node)
85
+ if len(hits) >= limit:
86
+ break
87
+ return hits
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class BuildResult:
92
+ """The outcome of :meth:`RepositoryGraph.build`."""
93
+
94
+ snapshot: GraphSnapshot
95
+ artifacts: dict[str, str] = field(default_factory=dict)
96
+ raw_output: str = ""
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class QueryResult:
101
+ """A natural-language answer produced by :meth:`RepositoryGraph.query`."""
102
+
103
+ question: str
104
+ answer: str
105
+ cited_nodes: tuple[str, ...] = ()
106
+ extra: dict[str, Any] = field(default_factory=dict)
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class ExplainResult:
111
+ """A plain-language explanation of a node and its neighbors."""
112
+
113
+ target: str
114
+ explanation: str
115
+ related: tuple[GraphNode, ...] = ()
116
+ extra: dict[str, Any] = field(default_factory=dict)
117
+
118
+
119
+ class RepositoryGraph(ABC):
120
+ """Strategy interface for any repository knowledge graph backend."""
121
+
122
+ name: str = "abstract"
123
+
124
+ @abstractmethod
125
+ async def is_available(self) -> bool:
126
+ """Return True if the back-end is installed and runnable."""
127
+
128
+ @abstractmethod
129
+ async def build(self, *, force: bool = False) -> BuildResult:
130
+ """Build (or rebuild) the graph rooted at the configured path."""
131
+
132
+ @abstractmethod
133
+ async def load(self) -> GraphSnapshot:
134
+ """Load a previously-built graph from disk without rebuilding."""
135
+
136
+ @abstractmethod
137
+ async def query(self, question: str, *, budget: int = 2000) -> QueryResult:
138
+ """Ask a free-form question; the back-end traverses the graph."""
139
+
140
+ @abstractmethod
141
+ async def explain(self, target: str) -> ExplainResult:
142
+ """Return a plain-language explanation of ``target`` and its neighbors."""
143
+
144
+ @abstractmethod
145
+ async def shortest_path(self, a: str, b: str) -> list[GraphEdge]:
146
+ """Return the shortest edge path between two node labels/ids."""
147
+
148
+ @abstractmethod
149
+ async def affected(
150
+ self, target: str, *, relation: Iterable[str] | None = None, depth: int = 2
151
+ ) -> list[GraphEdge]:
152
+ """Return the reverse-traversal edges from ``target``."""
153
+
154
+
155
+ __all__ = [
156
+ "BuildResult",
157
+ "ExplainResult",
158
+ "GraphCommunity",
159
+ "GraphEdge",
160
+ "GraphNode",
161
+ "GraphSnapshot",
162
+ "QueryResult",
163
+ "RepositoryGraph",
164
+ ]
@@ -0,0 +1,12 @@
1
+ """Local memory store and history (SQLite-backed)."""
2
+
3
+ from forgecli.memory.cache import Cache
4
+ from forgecli.memory.history import HistoryEntry, HistoryRepository
5
+ from forgecli.memory.store import MemoryStore
6
+
7
+ __all__ = [
8
+ "Cache",
9
+ "HistoryEntry",
10
+ "HistoryRepository",
11
+ "MemoryStore",
12
+ ]
@@ -0,0 +1,61 @@
1
+ """Lightweight in-memory cache with optional TTL.
2
+
3
+ A future iteration can swap this for SQLite-backed or Redis-backed caching
4
+ without affecting call sites.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from dataclasses import dataclass
11
+ from threading import RLock
12
+ from typing import Generic, TypeVar
13
+
14
+ K = TypeVar("K")
15
+ V = TypeVar("V")
16
+
17
+
18
+ @dataclass
19
+ class _Entry(Generic[V]):
20
+ value: V
21
+ expires_at: float | None # epoch seconds, or None for "no expiry"
22
+
23
+
24
+ class Cache(Generic[K, V]):
25
+ """Thread-safe key/value cache supporting optional per-entry TTL."""
26
+
27
+ def __init__(self, *, default_ttl: float | None = None) -> None:
28
+ self._store: dict[K, _Entry[V]] = {}
29
+ self._default_ttl = default_ttl
30
+ self._lock = RLock()
31
+
32
+ def get(self, key: K) -> V | None:
33
+ with self._lock:
34
+ entry = self._store.get(key)
35
+ if entry is None:
36
+ return None
37
+ if entry.expires_at is not None and entry.expires_at < time.time():
38
+ self._store.pop(key, None)
39
+ return None
40
+ return entry.value
41
+
42
+ def set(self, key: K, value: V, *, ttl: float | None = None) -> None:
43
+ effective_ttl = self._default_ttl if ttl is None else ttl
44
+ expires = time.time() + effective_ttl if effective_ttl else None
45
+ with self._lock:
46
+ self._store[key] = _Entry(value=value, expires_at=expires)
47
+
48
+ def delete(self, key: K) -> None:
49
+ with self._lock:
50
+ self._store.pop(key, None)
51
+
52
+ def clear(self) -> None:
53
+ with self._lock:
54
+ self._store.clear()
55
+
56
+ def __contains__(self, key: K) -> bool:
57
+ return self.get(key) is not None
58
+
59
+ def __len__(self) -> int:
60
+ with self._lock:
61
+ return len(self._store)
@@ -0,0 +1,136 @@
1
+ """Append-only history of CLI invocations and AI interactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import UTC, datetime
7
+ from typing import Any
8
+
9
+ from forgecli.memory.store import MemoryStore
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class HistoryEntry:
14
+ """A single persisted history record."""
15
+
16
+ id: int
17
+ timestamp: datetime
18
+ command: str
19
+ provider: str | None
20
+ model: str | None
21
+ prompt_tokens: int
22
+ completion_tokens: int
23
+ duration_ms: int
24
+ success: bool
25
+ error: str | None
26
+
27
+
28
+ class HistoryRepository:
29
+ """Persistence of CLI/AI invocations keyed by time."""
30
+
31
+ def __init__(self, store: MemoryStore, *, history_limit: int = 1000) -> None:
32
+ self._store = store
33
+ self._limit = history_limit
34
+ self._ensure_schema()
35
+
36
+ def _ensure_schema(self) -> None:
37
+ self._store.execute(
38
+ """
39
+ CREATE TABLE IF NOT EXISTS history (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ ts TEXT NOT NULL,
42
+ command TEXT NOT NULL,
43
+ provider TEXT,
44
+ model TEXT,
45
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
46
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
47
+ duration_ms INTEGER NOT NULL DEFAULT 0,
48
+ success INTEGER NOT NULL DEFAULT 1,
49
+ error TEXT
50
+ )
51
+ """
52
+ )
53
+ self._store.execute("CREATE INDEX IF NOT EXISTS idx_history_ts ON history(ts DESC)")
54
+
55
+ def record(
56
+ self,
57
+ *,
58
+ command: str,
59
+ provider: str | None = None,
60
+ model: str | None = None,
61
+ prompt_tokens: int = 0,
62
+ completion_tokens: int = 0,
63
+ duration_ms: int = 0,
64
+ success: bool = True,
65
+ error: str | None = None,
66
+ ) -> int:
67
+ """Insert a new history row and return its id."""
68
+ cursor = self._store.execute(
69
+ """
70
+ INSERT INTO history (
71
+ ts, command, provider, model,
72
+ prompt_tokens, completion_tokens,
73
+ duration_ms, success, error
74
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
75
+ """,
76
+ (
77
+ datetime.now(UTC).isoformat(timespec="seconds"),
78
+ command,
79
+ provider,
80
+ model,
81
+ prompt_tokens,
82
+ completion_tokens,
83
+ duration_ms,
84
+ int(success),
85
+ error,
86
+ ),
87
+ )
88
+ self._trim()
89
+ return int(cursor.lastrowid or 0)
90
+
91
+ def list_recent(self, limit: int = 50) -> list[HistoryEntry]:
92
+ """Return the most recent history entries (newest first)."""
93
+ rows = self._store.execute(
94
+ """
95
+ SELECT id, ts, command, provider, model,
96
+ prompt_tokens, completion_tokens,
97
+ duration_ms, success, error
98
+ FROM history
99
+ ORDER BY id DESC
100
+ LIMIT ?
101
+ """,
102
+ (limit,),
103
+ ).fetchall()
104
+ return [self._to_entry(r) for r in rows]
105
+
106
+ def _trim(self) -> None:
107
+ """Drop oldest rows past the configured retention limit."""
108
+ row = self._store.execute("SELECT COUNT(*) AS n FROM history").fetchone()
109
+ total = int(row["n"]) if row else 0
110
+ if total <= self._limit:
111
+ return
112
+ excess = total - self._limit
113
+ self._store.execute(
114
+ """
115
+ DELETE FROM history
116
+ WHERE id IN (
117
+ SELECT id FROM history ORDER BY id ASC LIMIT ?
118
+ )
119
+ """,
120
+ (excess,),
121
+ )
122
+
123
+ @staticmethod
124
+ def _to_entry(row: Any) -> HistoryEntry:
125
+ return HistoryEntry(
126
+ id=int(row["id"]),
127
+ timestamp=datetime.fromisoformat(row["ts"]),
128
+ command=str(row["command"]),
129
+ provider=row["provider"],
130
+ model=row["model"],
131
+ prompt_tokens=int(row["prompt_tokens"] or 0),
132
+ completion_tokens=int(row["completion_tokens"] or 0),
133
+ duration_ms=int(row["duration_ms"] or 0),
134
+ success=bool(row["success"]),
135
+ error=row["error"],
136
+ )
@@ -0,0 +1,85 @@
1
+ """SQLite-backed local memory store (singleton lifecycle owned by AppContext)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from forgecli.utils.fs import ensure_dir
10
+
11
+
12
+ class MemoryStore:
13
+ """Thin wrapper around a local SQLite database.
14
+
15
+ The store is intentionally minimal: it owns the connection and provides
16
+ a few named-table helpers. Higher-level repositories (e.g. history)
17
+ layer domain logic on top.
18
+ """
19
+
20
+ SCHEMA_VERSION = 1
21
+
22
+ def __init__(self, db_path: Path) -> None:
23
+ self._db_path = Path(db_path)
24
+ ensure_dir(self._db_path.parent)
25
+ self._conn: sqlite3.Connection | None = None
26
+
27
+ @property
28
+ def db_path(self) -> Path:
29
+ return self._db_path
30
+
31
+ def connect(self) -> None:
32
+ """Open the database and run schema migrations."""
33
+ if self._conn is not None:
34
+ return
35
+ self._conn = sqlite3.connect(
36
+ str(self._db_path),
37
+ check_same_thread=False,
38
+ isolation_level=None, # autocommit; we use explicit transactions
39
+ )
40
+ self._conn.row_factory = sqlite3.Row
41
+ self._conn.execute("PRAGMA journal_mode = WAL")
42
+ self._conn.execute("PRAGMA foreign_keys = ON")
43
+ self._migrate()
44
+
45
+ def close(self) -> None:
46
+ """Close the underlying connection if open."""
47
+ if self._conn is not None:
48
+ self._conn.close()
49
+ self._conn = None
50
+
51
+ def __enter__(self) -> MemoryStore:
52
+ self.connect()
53
+ return self
54
+
55
+ def __exit__(self, *exc: Any) -> None:
56
+ self.close()
57
+
58
+ @property
59
+ def conn(self) -> sqlite3.Connection:
60
+ if self._conn is None:
61
+ raise RuntimeError("MemoryStore.connect() must be called first")
62
+ return self._conn
63
+
64
+ def _migrate(self) -> None:
65
+ self.conn.execute(
66
+ """
67
+ CREATE TABLE IF NOT EXISTS schema_meta (
68
+ key TEXT PRIMARY KEY,
69
+ value TEXT NOT NULL
70
+ )
71
+ """
72
+ )
73
+ row = self.conn.execute("SELECT value FROM schema_meta WHERE key = 'version'").fetchone()
74
+ if row is None:
75
+ self.conn.execute(
76
+ "INSERT INTO schema_meta (key, value) VALUES ('version', ?)",
77
+ (str(self.SCHEMA_VERSION),),
78
+ )
79
+ # Future migrations can be chained here by inspecting ``row``.
80
+
81
+ def execute(self, sql: str, params: tuple[Any, ...] = ()) -> sqlite3.Cursor:
82
+ return self.conn.execute(sql, params)
83
+
84
+ def executemany(self, sql: str, params: list[tuple[Any, ...]]) -> sqlite3.Cursor:
85
+ return self.conn.executemany(sql, params)
@@ -0,0 +1,13 @@
1
+ """Context optimization: chunking, ranking, summarization, token optimization."""
2
+
3
+ from forgecli.optimizer.chunker import Chunker
4
+ from forgecli.optimizer.optimizer import ContextOptimizer
5
+ from forgecli.optimizer.ranker import Ranker
6
+ from forgecli.optimizer.summarizer import Summarizer
7
+
8
+ __all__ = [
9
+ "Chunker",
10
+ "ContextOptimizer",
11
+ "Ranker",
12
+ "Summarizer",
13
+ ]
@@ -0,0 +1,169 @@
1
+ """Caveman token-optimizer integration.
2
+
3
+ Caveman is a prompt-level optimization technique that reduces LLM output
4
+ tokens by ~65% on average. It works by injecting a system prompt that
5
+ tells the model to communicate in a compressed, token-efficient style —
6
+ dropping filler words, pleasantries, and using fragments — while keeping
7
+ all technical terms exact.
8
+
9
+ Two implementations are shipped:
10
+
11
+ * :class:`CavemanRulesetOptimizer` — a self-contained Python
12
+ implementation of the Caveman ruleset (lite / full / ultra / wenyan).
13
+ Always available, no external dependencies.
14
+ * :class:`CavemanCLIOptimizer` — an optional adapter that shells out
15
+ to an external ``caveman`` binary if one is on ``PATH``.
16
+
17
+ The :class:`CavemanCompositeOptimizer` picks between them at runtime.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from abc import ABC, abstractmethod
23
+ from collections.abc import Sequence
24
+ from enum import Enum
25
+
26
+ from forgecli.optimizer.ponytail import OptimizedRequest
27
+ from forgecli.providers.base import ChatMessage, ChatRequest
28
+
29
+
30
+ class CavemanIntensity(str, Enum):
31
+ """How aggressively to compress LLM output.
32
+
33
+ * ``off`` - no rewriting; pass prompts through unchanged.
34
+ * ``lite`` - be concise; drop filler words and pleasantries.
35
+ * ``full`` - full caveman: fragments, [thing][action][reason] pattern.
36
+ * ``ultra`` - maximum compression; grunt-level communication.
37
+ * ``wenyan`` - Classical Chinese literary style (文言) for max density.
38
+ """
39
+
40
+ OFF = "off"
41
+ LITE = "lite"
42
+ FULL = "full"
43
+ ULTRA = "ultra"
44
+ WENYAN = "wenyan"
45
+
46
+ @classmethod
47
+ def parse(cls, value: str | CavemanIntensity | None) -> CavemanIntensity:
48
+ """Parse a string into an :class:`CavemanIntensity`, falling back to LITE."""
49
+ if value is None or value == "":
50
+ return cls.LITE
51
+ if isinstance(value, cls):
52
+ return value
53
+ try:
54
+ return cls(value.lower())
55
+ except ValueError as exc:
56
+ raise ValueError(
57
+ f"Unknown caveman intensity {value!r}; expected one of "
58
+ f"{', '.join(i.value for i in cls)}"
59
+ ) from exc
60
+
61
+
62
+ class CavemanPromptOptimizer(ABC):
63
+ """Strategy interface for Caveman prompt optimization.
64
+
65
+ Implementations must be deterministic and side-effect free
66
+ (other than logging), so they can be invoked transparently before
67
+ every model call.
68
+ """
69
+
70
+ name: str = "abstract-caveman"
71
+
72
+ @abstractmethod
73
+ async def optimize_chat(self, request: ChatRequest) -> OptimizedRequest:
74
+ """Return an optimized copy of ``request``."""
75
+
76
+ async def is_available(self) -> bool:
77
+ """Return whether this optimizer is available to use."""
78
+ return True
79
+
80
+
81
+ class CavemanCompositeOptimizer(CavemanPromptOptimizer):
82
+ """Pick the right optimizer for the configured :class:`CavemanIntensity`."""
83
+
84
+ name = "caveman-composite"
85
+
86
+ def __init__(
87
+ self,
88
+ *,
89
+ intensity: CavemanIntensity = CavemanIntensity.LITE,
90
+ ruleset: CavemanPromptOptimizer | None = None,
91
+ external: CavemanPromptOptimizer | None = None,
92
+ ) -> None:
93
+ self._intensity = intensity
94
+ self._ruleset = ruleset
95
+ self._external = external
96
+ self._sync_ruleset_intensity()
97
+
98
+ def _sync_ruleset_intensity(self) -> None:
99
+ """Propagate the composite's intensity to the ruleset, if compatible."""
100
+ ruleset = self._ruleset
101
+ if isinstance(ruleset, CavemanRulesetOptimizer):
102
+ ruleset.set_intensity(self._intensity)
103
+
104
+ @property
105
+ def intensity(self) -> CavemanIntensity:
106
+ return self._intensity
107
+
108
+ def set_intensity(self, intensity: CavemanIntensity | str) -> None:
109
+ self._intensity = CavemanIntensity.parse(intensity)
110
+ if isinstance(self._ruleset, CavemanRulesetOptimizer):
111
+ self._ruleset.set_intensity(self._intensity)
112
+
113
+ async def optimize_chat(self, request: ChatRequest) -> OptimizedRequest:
114
+ if self._intensity is CavemanIntensity.OFF:
115
+ return OptimizedRequest(
116
+ request=request,
117
+ notes=("caveman off",),
118
+ intensity=self._intensity,
119
+ source="caveman-passthrough",
120
+ )
121
+
122
+ if self._external is not None and await self._external.is_available():
123
+ result = await self._external.optimize_chat(request)
124
+ return OptimizedRequest(
125
+ request=result.request,
126
+ notes=(*result.notes, "caveman external"),
127
+ intensity=self._intensity,
128
+ source="caveman-external",
129
+ )
130
+
131
+ if self._ruleset is None:
132
+ return OptimizedRequest(
133
+ request=request,
134
+ notes=("no caveman ruleset registered",),
135
+ intensity=self._intensity,
136
+ source="caveman-passthrough",
137
+ )
138
+
139
+ result = await self._ruleset.optimize_chat(request)
140
+ return OptimizedRequest(
141
+ request=result.request,
142
+ notes=result.notes,
143
+ intensity=self._intensity,
144
+ source="caveman-ruleset",
145
+ )
146
+
147
+
148
+ def _ensure_user_message(messages: Sequence[ChatMessage]) -> bool:
149
+ return any(m.role.value == "user" for m in messages)
150
+
151
+
152
+ def _clone_request(request: ChatRequest, messages: Sequence[ChatMessage]) -> ChatRequest:
153
+ """Return a copy of ``request`` with ``messages`` replaced."""
154
+ return request.model_copy(update={"messages": list(messages)})
155
+
156
+
157
+ __all__ = [
158
+ "CavemanCLIOptimizer",
159
+ "CavemanCompositeOptimizer",
160
+ "CavemanIntensity",
161
+ "CavemanPromptOptimizer",
162
+ "CavemanProvider",
163
+ "CavemanRulesetOptimizer",
164
+ "OptimizedRequest",
165
+ ]
166
+
167
+ from forgecli.optimizer.caveman.cli import CavemanCLIOptimizer # noqa: E402
168
+ from forgecli.optimizer.caveman.decorator import CavemanProvider # noqa: E402
169
+ from forgecli.optimizer.caveman.ruleset import CavemanRulesetOptimizer # noqa: E402