fixtureforge 0.1.0__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/PKG-INFO +13 -7
  2. fixtureforge-2.0.0/pyproject.toml +78 -0
  3. fixtureforge-2.0.0/src/fixtureforge/__init__.py +413 -0
  4. fixtureforge-2.0.0/src/fixtureforge/ai/engine.py +86 -0
  5. fixtureforge-2.0.0/src/fixtureforge/ai/prompts.py +156 -0
  6. fixtureforge-2.0.0/src/fixtureforge/config/__init__.py +4 -0
  7. fixtureforge-2.0.0/src/fixtureforge/config/flags.py +55 -0
  8. fixtureforge-2.0.0/src/fixtureforge/core/batch_engine.py +118 -0
  9. fixtureforge-2.0.0/src/fixtureforge/core/compression.py +285 -0
  10. fixtureforge-2.0.0/src/fixtureforge/core/dataset.py +161 -0
  11. fixtureforge-2.0.0/src/fixtureforge/core/generator.py +209 -0
  12. fixtureforge-2.0.0/src/fixtureforge/core/parser.py +119 -0
  13. fixtureforge-2.0.0/src/fixtureforge/core/recipe.py +106 -0
  14. fixtureforge-2.0.0/src/fixtureforge/core/router.py +111 -0
  15. fixtureforge-2.0.0/src/fixtureforge/core/swarm.py +139 -0
  16. fixtureforge-2.0.0/src/fixtureforge/memory/__init__.py +5 -0
  17. fixtureforge-2.0.0/src/fixtureforge/memory/dream.py +413 -0
  18. fixtureforge-2.0.0/src/fixtureforge/memory/store.py +287 -0
  19. fixtureforge-2.0.0/src/fixtureforge/providers/__init__.py +42 -0
  20. fixtureforge-2.0.0/src/fixtureforge/providers/anthropic.py +56 -0
  21. fixtureforge-2.0.0/src/fixtureforge/providers/base.py +37 -0
  22. fixtureforge-2.0.0/src/fixtureforge/providers/factory.py +100 -0
  23. fixtureforge-2.0.0/src/fixtureforge/providers/gemini.py +87 -0
  24. fixtureforge-2.0.0/src/fixtureforge/providers/groq.py +25 -0
  25. fixtureforge-2.0.0/src/fixtureforge/providers/ollama.py +77 -0
  26. fixtureforge-2.0.0/src/fixtureforge/providers/openai.py +70 -0
  27. fixtureforge-2.0.0/src/fixtureforge/security/__init__.py +8 -0
  28. fixtureforge-2.0.0/src/fixtureforge/security/permissions.py +257 -0
  29. fixtureforge-0.1.0/pyproject.toml +0 -37
  30. fixtureforge-0.1.0/src/fixtureforge/__init__.py +0 -70
  31. fixtureforge-0.1.0/src/fixtureforge/ai/engine.py +0 -44
  32. fixtureforge-0.1.0/src/fixtureforge/ai/prompts.py +0 -31
  33. fixtureforge-0.1.0/src/fixtureforge/core/generator.py +0 -135
  34. fixtureforge-0.1.0/src/fixtureforge/core/parser.py +0 -108
  35. fixtureforge-0.1.0/src/fixtureforge/core/recipe.py +0 -76
  36. fixtureforge-0.1.0/src/fixtureforge/core/router.py +0 -50
  37. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/LICENSE +0 -0
  38. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/README.md +0 -0
  39. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/ai/__init__.py +0 -0
  40. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/ai/cache.py +0 -0
  41. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/cli/__init__.py +0 -0
  42. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/cli/commands.py +0 -0
  43. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/__init__.py +0 -0
  44. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/analyzer.py +0 -0
  45. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/exporter.py +0 -0
  46. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/streamer.py +0 -0
  47. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/integrations/__init__.py +0 -0
  48. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/integrations/github.py +0 -0
  49. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/integrations/jira.py +0 -0
  50. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/library/__init__.py +0 -0
  51. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/library/sharing.py +0 -0
  52. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/library/storage.py +0 -0
  53. {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/pyproject.toml +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureforge
3
- Version: 0.1.0
4
- Summary: AI-powered realistic test data generation
3
+ Version: 2.0.0
4
+ Summary: Agentic Test Data Harness: memory, multi-agent swarms, permission gates, coverage analysis. Provider-agnostic (Gemini, OpenAI, Anthropic, Ollama).
5
5
  License: MIT
6
6
  License-File: LICENSE
7
- Keywords: testing,fixtures,test-data,qa,automation
7
+ Keywords: testing,fixtures,test-data,qa,automation,synthetic-data,llm
8
8
  Author: Yaniv Metuku
9
9
  Requires-Python: >=3.11,<4.0
10
10
  Classifier: License :: OSI Approved :: MIT License
@@ -13,15 +13,21 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
- Requires-Dist: anthropic (>=0.18.0,<0.19.0)
16
+ Provides-Extra: all
17
+ Provides-Extra: anthropic
18
+ Provides-Extra: gemini
19
+ Provides-Extra: openai
20
+ Provides-Extra: sql
21
+ Requires-Dist: anthropic (>=0.18.0,<0.19.0) ; extra == "anthropic" or extra == "all"
17
22
  Requires-Dist: click (>=8.1.0,<9.0.0)
18
23
  Requires-Dist: faker (>=22.0.0,<23.0.0)
19
- Requires-Dist: google-genai (>=1.62.0,<2.0.0)
20
- Requires-Dist: httpx (>=0.28.1,<0.29.0)
24
+ Requires-Dist: google-genai (>=1.0.0,<2.0.0) ; extra == "gemini" or extra == "all"
25
+ Requires-Dist: openai (>=1.0.0,<2.0.0) ; extra == "openai" or extra == "all"
21
26
  Requires-Dist: pydantic (>=2.5.0,<3.0.0)
22
27
  Requires-Dist: pyyaml (>=6.0,<7.0)
28
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
23
29
  Requires-Dist: rich (>=13.7.0,<14.0.0)
24
- Requires-Dist: sqlalchemy (>=2.0.0,<3.0.0)
30
+ Requires-Dist: sqlalchemy (>=2.0.0,<3.0.0) ; extra == "sql" or extra == "all"
25
31
  Project-URL: Homepage, https://fixtureforge.dev
26
32
  Project-URL: Repository, https://github.com/Yaniv2809/fixtureforge
27
33
  Description-Content-Type: text/markdown
@@ -0,0 +1,78 @@
1
+ [tool.poetry]
2
+ name = "fixtureforge"
3
+ version = "2.0.0"
4
+ description = "Agentic Test Data Harness: memory, multi-agent swarms, permission gates, coverage analysis. Provider-agnostic (Gemini, OpenAI, Anthropic, Ollama)."
5
+ authors = ["Yaniv Metuku"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ homepage = "https://fixtureforge.dev"
9
+ repository = "https://github.com/Yaniv2809/fixtureforge"
10
+ keywords = ["testing", "fixtures", "test-data", "qa", "automation", "synthetic-data", "llm"]
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Core dependencies — always installed, no AI required
14
+ # ---------------------------------------------------------------------------
15
+ [tool.poetry.dependencies]
16
+ python = "^3.11"
17
+ pydantic = "^2.5.0"
18
+ faker = "^22.0.0"
19
+ pyyaml = "^6.0"
20
+ click = "^8.1.0"
21
+ rich = "^13.7.0"
22
+ requests = "^2.31.0" # used by OllamaProvider + general HTTP
23
+
24
+ # SQLAlchemy is optional but common enough to keep as a soft dependency
25
+ sqlalchemy = { version = "^2.0.0", optional = true }
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # AI provider extras — install only what you need
29
+ #
30
+ # pip install fixtureforge[gemini] → Google Gemini
31
+ # pip install fixtureforge[openai] → OpenAI / Azure OpenAI
32
+ # pip install fixtureforge[anthropic] → Anthropic Claude
33
+ # pip install fixtureforge[all] → all cloud providers
34
+ #
35
+ # Ollama (local) needs no extra pip package — just run Ollama locally.
36
+ # ---------------------------------------------------------------------------
37
+ google-genai = { version = "^1.0.0", optional = true }
38
+ openai = { version = "^1.0.0", optional = true }
39
+ anthropic = { version = "^0.18.0", optional = true }
40
+
41
+ [tool.poetry.extras]
42
+ gemini = ["google-genai"]
43
+ openai = ["openai"]
44
+ anthropic = ["anthropic"]
45
+ sql = ["sqlalchemy"]
46
+ all = ["google-genai", "openai", "anthropic", "sqlalchemy"]
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Development dependencies
50
+ # ---------------------------------------------------------------------------
51
+ [tool.poetry.group.dev.dependencies]
52
+ pytest = "^7.4.0"
53
+ pytest-asyncio = "^0.23.0"
54
+ pytest-cov = "^4.1.0"
55
+ black = "^23.12.0"
56
+ ruff = "^0.1.9"
57
+ mypy = "^1.8.0"
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # CLI entry point
61
+ # ---------------------------------------------------------------------------
62
+ [tool.poetry.scripts]
63
+ forge = "fixtureforge.cli.commands:cli"
64
+
65
+ [build-system]
66
+ requires = ["poetry-core"]
67
+ build-backend = "poetry.core.masonry.api"
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Ruff (linting) config
71
+ # ---------------------------------------------------------------------------
72
+ [tool.ruff]
73
+ line-length = 100
74
+ target-version = "py311"
75
+
76
+ [tool.ruff.lint]
77
+ select = ["E", "F", "I", "UP"]
78
+ ignore = ["E501"]
@@ -0,0 +1,413 @@
1
+ """
2
+ FixtureForge v2.0 — Agentic Test Data Harness.
3
+
4
+ Quick start (auto-detects AI provider from env vars):
5
+ from fixtureforge import Forge
6
+ from pydantic import BaseModel
7
+
8
+ class User(BaseModel):
9
+ id: int
10
+ name: str
11
+ email: str
12
+ bio: str
13
+
14
+ forge = Forge()
15
+ users = forge.create_batch(User, count=50, context="SaaS platform users")
16
+
17
+ Parallel DataSwarm (multiple models, shared cache):
18
+ results = forge.swarm([User, Order, Product], counts=[10, 50, 100])
19
+ # → {"User": [...], "Order": [...], "Product": [...]}
20
+
21
+ Permission gates (safe / sensitive / dangerous):
22
+ forge = Forge(allow_pii=True) # auto-approve PII fields
23
+ forge = Forge(interactive=False) # CI mode — reject dangerous gates silently
24
+
25
+ Domain rules (persisted across sessions):
26
+ forge.memory.add_rule("financial", "Users under 18 get restricted account type")
27
+ forge.memory.add_rule("user", "Israeli phone numbers use format 05x-xxx-xxxx")
28
+
29
+ Coverage analysis (ForgeDream):
30
+ report = forge.dream(models=[User, Order], force=True)
31
+ print(report.summary())
32
+
33
+ Feature flags:
34
+ from fixtureforge.config import is_enabled
35
+ is_enabled("FORGE_SWARMS") # True
36
+ is_enabled("FORGE_DREAM") # False (enable with FORGE_FLAG_DREAM=1)
37
+ """
38
+ from __future__ import annotations
39
+
40
+ from pathlib import Path
41
+ from typing import Any, Dict, Generator, List, Optional, Type, TypeVar
42
+
43
+ from pydantic import BaseModel
44
+
45
+ from .ai.engine import AIEngine
46
+ from .config.flags import FORGE_FLAGS, flag_summary, is_enabled
47
+ from .core.batch_engine import SmartBatchEngine
48
+ from .core.compression import CompressionPipeline, ForgeSessionState
49
+ from .core.dataset import ForgeDataset
50
+ from .core.generator import BasicGenerator
51
+ from .core.streamer import DataStreamer
52
+ from .core.swarm import DataSwarm
53
+ from .memory.dream import ForgeDream
54
+ from .memory.store import ForgeMemory
55
+ from .providers.base import LLMProvider
56
+ from .security.permissions import (
57
+ DataSensitivity,
58
+ FieldPermissionChecker,
59
+ ForgeCoordinator,
60
+ )
61
+
62
+ __version__ = "2.0.0"
63
+
64
+ T = TypeVar("T", bound=BaseModel)
65
+
66
+
67
+ class Forge:
68
+ """
69
+ Main entry point for FixtureForge v2.0 — Agentic Test Data Harness.
70
+
71
+ Parameters
72
+ ----------
73
+ provider : LLMProvider, optional
74
+ A pre-constructed provider instance.
75
+ provider_name : str, optional
76
+ "gemini" | "openai" | "anthropic" | "ollama". Auto-detected from env.
77
+ api_key : str, optional
78
+ Falls back to the relevant env var.
79
+ model : str, optional
80
+ Model identifier. Provider default is used when omitted.
81
+ use_ai : bool
82
+ False = fully deterministic, zero-cost generation (CI safe).
83
+ use_cache : bool
84
+ Cache AI responses to disk (7-day TTL). Default True.
85
+ locale : str
86
+ Faker locale for standard fields (default "en_US").
87
+ allow_pii : bool, optional
88
+ Auto-approve SENSITIVE data generation (overrides FORGE_ALLOW_PII env var).
89
+ interactive : bool
90
+ When False, permission gates in CI mode reject silently instead of prompting.
91
+ memory_dir : Path, optional
92
+ Root for the .forge/ context directory (default: current working directory).
93
+ **provider_kwargs
94
+ Forwarded to the provider constructor.
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ provider: Optional[LLMProvider] = None,
100
+ provider_name: Optional[str] = None,
101
+ api_key: Optional[str] = None,
102
+ model: Optional[str] = None,
103
+ use_ai: bool = True,
104
+ use_cache: bool = True,
105
+ locale: str = "en_US",
106
+ allow_pii: Optional[bool] = None,
107
+ interactive: bool = True,
108
+ memory_dir: Optional[Path] = None,
109
+ **provider_kwargs,
110
+ ):
111
+ # ── Resolve AI provider ──────────────────────────────────────────
112
+ resolved_provider: Optional[LLMProvider] = None
113
+
114
+ if use_ai:
115
+ if provider is not None:
116
+ resolved_provider = provider
117
+ else:
118
+ try:
119
+ from .providers.factory import create_provider
120
+ resolved_provider = create_provider(
121
+ provider_name=provider_name,
122
+ api_key=api_key,
123
+ model=model,
124
+ **provider_kwargs,
125
+ )
126
+ except Exception as exc:
127
+ print(f"⚠️ Could not initialise AI provider: {exc}")
128
+ print(" Running in deterministic-only mode.")
129
+ resolved_provider = None
130
+
131
+ self._provider = resolved_provider
132
+
133
+ # ── Core generation stack ────────────────────────────────────────
134
+ self.ai_engine = AIEngine(provider=resolved_provider, use_cache=use_cache)
135
+ self.generator = BasicGenerator(locale=locale, ai_engine=self.ai_engine)
136
+ self.batch_engine = SmartBatchEngine(
137
+ generator=self.generator, ai_engine=self.ai_engine
138
+ )
139
+
140
+ # ── Security layer ───────────────────────────────────────────────
141
+ self.coordinator = ForgeCoordinator(
142
+ allow_pii=allow_pii,
143
+ interactive=interactive,
144
+ ) if is_enabled("FORGE_PERMISSIONS") else None
145
+
146
+ # ── Memory / context layer ───────────────────────────────────────
147
+ self.memory = ForgeMemory(base_dir=memory_dir)
148
+
149
+ # ── Session state + compression ──────────────────────────────────
150
+ self._session = ForgeSessionState()
151
+ self._compression = CompressionPipeline()
152
+
153
+ # ── ForgeDream (feature-flagged) ─────────────────────────────────
154
+ self._dream: Optional[ForgeDream] = (
155
+ ForgeDream(memory_dir=self.memory._root)
156
+ if is_enabled("FORGE_DREAM")
157
+ else None
158
+ )
159
+
160
+ # ------------------------------------------------------------------
161
+ # Properties
162
+ # ------------------------------------------------------------------
163
+
164
+ @property
165
+ def use_ai(self) -> bool:
166
+ """True when an AI provider is active."""
167
+ return self._provider is not None
168
+
169
+ @property
170
+ def provider_name(self) -> Optional[str]:
171
+ return self._provider.model_name if self._provider else None
172
+
173
+ @property
174
+ def flags(self) -> Dict[str, bool]:
175
+ """Snapshot of all feature flag values."""
176
+ return flag_summary()
177
+
178
+ # ------------------------------------------------------------------
179
+ # Public generation API
180
+ # ------------------------------------------------------------------
181
+
182
+ def create(
183
+ self,
184
+ model: Type[T],
185
+ count: int = 1,
186
+ context: str = None,
187
+ **overrides,
188
+ ) -> Any:
189
+ """
190
+ Generate *count* instances one-by-one.
191
+ Runs a permission check first (safe → auto, sensitive/dangerous → gate).
192
+ Returns a single instance when count=1, otherwise a list.
193
+ """
194
+ self._check_permission(model, count)
195
+
196
+ results: List[T] = []
197
+ domain_rules = self.memory.get_rules_for_prompt(model_name=model.__name__)
198
+
199
+ for i in range(count):
200
+ if count > 1:
201
+ print(f" ...generating {i + 1}/{count}...")
202
+ item = self.generator.generate(model, context=context, **overrides)
203
+ self._register(model, item)
204
+ results.append(item)
205
+
206
+ self._post_generation(model, count, list(model.model_fields.keys()))
207
+ _ = domain_rules # rules are read; future: pass into generator
208
+ return results[0] if count == 1 else results
209
+
210
+ def create_batch(
211
+ self,
212
+ model: Type[T],
213
+ count: int,
214
+ context: str = None,
215
+ **overrides,
216
+ ) -> List[T]:
217
+ """
218
+ Generate *count* instances efficiently (O(m) API calls, not O(n×m)).
219
+
220
+ When AI is active and no overrides are specified, SmartBatchEngine
221
+ batches all semantic fields across all records into one call per field.
222
+ Falls back to loop-based create() when overrides are specified.
223
+ """
224
+ self._check_permission(model, count)
225
+
226
+ if not self.use_ai or overrides:
227
+ return self.create(model, count=count, context=context, **overrides)
228
+
229
+ print(f"⚡ Smart-Batching {count} × '{model.__name__}'...", flush=True)
230
+ try:
231
+ items = self.batch_engine.generate_many(model, count=count, context=context)
232
+ for item in items:
233
+ self._register(model, item)
234
+ self._post_generation(model, count, list(model.model_fields.keys()))
235
+ return items
236
+ except Exception as exc:
237
+ print(f"⚠️ Batch error: {exc}. Falling back to loop.")
238
+ return self.create(model, count=count, context=context, **overrides)
239
+
240
+ def create_large(
241
+ self,
242
+ model: Type[T],
243
+ count: int,
244
+ context: str = None,
245
+ seed_ratio: float = 0.01,
246
+ ) -> "ForgeDataset[T]":
247
+ """
248
+ Efficient generation for very large datasets (10k+ records).
249
+
250
+ Uses Seed + Interpolation: generate only (count × seed_ratio) unique
251
+ AI values, then tile deterministically. Wraps the result in a
252
+ ForgeDataset which auto-spills to disk when > 50 KB.
253
+ """
254
+ self._check_permission(model, count)
255
+
256
+ print(
257
+ f"🌊 Large-batch: {count} × '{model.__name__}' "
258
+ f"(seed_ratio={seed_ratio:.0%})...",
259
+ flush=True,
260
+ )
261
+ items = self.batch_engine.generate_many_with_seeds(
262
+ model, count=count, context=context, seed_ratio=seed_ratio
263
+ )
264
+ for item in items:
265
+ self._register(model, item)
266
+ self._post_generation(model, count, list(model.model_fields.keys()))
267
+
268
+ dataset = ForgeDataset(items)
269
+ if dataset.is_spilled:
270
+ print(dataset.preview())
271
+ return dataset
272
+
273
+ def create_stream(
274
+ self,
275
+ model: Type[T],
276
+ count: int,
277
+ filename: str,
278
+ context: str = None,
279
+ **overrides,
280
+ ) -> Generator[T, None, None]:
281
+ """
282
+ Lazy evaluation: generate and write to disk one record at a time.
283
+ Prevents memory exhaustion for huge datasets.
284
+ Supports .json, .csv, .sql output formats.
285
+ """
286
+ self._check_permission(model, count)
287
+
288
+ streamer = DataStreamer(filename)
289
+ streamer.start()
290
+ print(f"🌊 Streaming {count} items to {filename}...")
291
+
292
+ for _ in range(count):
293
+ item = self.generator.generate(model, context=context, **overrides)
294
+ streamer.write(item)
295
+ self._register(model, item)
296
+ yield item
297
+
298
+ streamer.close()
299
+ self._post_generation(model, count, list(model.model_fields.keys()))
300
+ print(f"✅ Stream complete → {filename}")
301
+
302
+ def swarm(
303
+ self,
304
+ models: List[Type[BaseModel]],
305
+ counts: Optional[List[int]] = None,
306
+ contexts: Optional[List[Optional[str]]] = None,
307
+ max_workers: int = 4,
308
+ ) -> Dict[str, List[Any]]:
309
+ """
310
+ Generate multiple models in parallel, sharing the AI cache.
311
+
312
+ The first model warms the cache; subsequent models inherit it
313
+ for ~90% cost reduction per additional model.
314
+
315
+ Parameters
316
+ ----------
317
+ models : list of Pydantic model classes
318
+ counts : records per model (defaults to 10 each)
319
+ contexts : optional context string per model
320
+ max_workers : thread-pool size for the parallel phase
321
+
322
+ Returns
323
+ -------
324
+ dict mapping model_name → list of generated instances
325
+ """
326
+ if not is_enabled("FORGE_SWARMS"):
327
+ raise RuntimeError(
328
+ "DataSwarms are disabled. Set FORGE_FLAG_SWARMS=1 to enable."
329
+ )
330
+ swarm = DataSwarm(forge=self, max_workers=max_workers)
331
+ return swarm.run(models=models, counts=counts, contexts=contexts)
332
+
333
+ def dream(
334
+ self,
335
+ models: Optional[List[Type[BaseModel]]] = None,
336
+ force: bool = False,
337
+ ) -> "ForgeDream":
338
+ """
339
+ Run ForgeDream 4-phase coverage consolidation.
340
+
341
+ Analyses coverage gaps, merges contradictory rules, and trims the
342
+ memory index. Saves a coverage_gaps.json report to the .forge/ dir.
343
+
344
+ Requires FORGE_DREAM feature flag (FORGE_FLAG_DREAM=1) unless force=True.
345
+
346
+ Returns the DreamReport.
347
+ """
348
+ if not is_enabled("FORGE_DREAM") and not force:
349
+ raise RuntimeError(
350
+ "ForgeDream is feature-flagged off. "
351
+ "Set FORGE_FLAG_DREAM=1 or pass force=True."
352
+ )
353
+ if self._dream is None:
354
+ self._dream = ForgeDream(memory_dir=self.memory._root)
355
+
356
+ return self._dream.run(models=models, force=force)
357
+
358
+ # ------------------------------------------------------------------
359
+ # Utilities
360
+ # ------------------------------------------------------------------
361
+
362
+ def stats(self) -> dict:
363
+ """Return record counts per registered model, plus session state."""
364
+ registry_stats = {k: len(v) for k, v in self.generator.registry.items()}
365
+ return {
366
+ "registry": registry_stats,
367
+ "session_tokens": self._session.token_estimate,
368
+ "memory": self.memory.stats(),
369
+ "flags": {k: v for k, v in self.flags.items() if v}, # only enabled
370
+ }
371
+
372
+ def clear_registry(self) -> None:
373
+ """Reset FK registry and ID counters (useful between independent test scenarios)."""
374
+ self.generator.registry.clear()
375
+ self.generator._id_counters.clear()
376
+
377
+ # ------------------------------------------------------------------
378
+ # Internal helpers
379
+ # ------------------------------------------------------------------
380
+
381
+ def _check_permission(self, model: Type, count: int) -> None:
382
+ """Run permission gate when FORGE_PERMISSIONS is enabled."""
383
+ if self.coordinator is None:
384
+ return
385
+ approved = self.coordinator.check_and_approve(model, count)
386
+ if not approved:
387
+ sensitivity = FieldPermissionChecker.classify_model(model).value
388
+ raise PermissionError(
389
+ f"Generation of '{model.__name__}' ({sensitivity}) was denied by ForgeCoordinator."
390
+ )
391
+
392
+ def _register(self, model: Type, item: Any) -> None:
393
+ key = model.__name__.lower()
394
+ self.generator.registry.setdefault(key, []).append(item)
395
+
396
+ def _post_generation(
397
+ self, model: Type, count: int, fields: List[str]
398
+ ) -> None:
399
+ """Update session state and trigger compression if near budget."""
400
+ self._session.add_generation(model.__name__, count, fields)
401
+ layer = self._compression.maybe_compact(self._session)
402
+ if layer:
403
+ print(f" [compression: {layer} layer ran]")
404
+
405
+ # ForgeDream session counter
406
+ if self._dream is not None:
407
+ self._dream.record_session()
408
+
409
+
410
+ # ---------------------------------------------------------------------------
411
+ # Package-level convenience instance (auto-detects provider from env vars)
412
+ # ---------------------------------------------------------------------------
413
+ forge = Forge()
@@ -0,0 +1,86 @@
1
+ """
2
+ AIEngine — thin orchestration layer over any LLMProvider.
3
+
4
+ Responsibilities:
5
+ - Route generate() / generate_batch_semantic() calls to the active provider
6
+ - Transparently check/populate ResponseCache before hitting the API
7
+ - Provide a consistent interface so the rest of the codebase never imports
8
+ provider-specific classes directly
9
+ """
10
+ from typing import TYPE_CHECKING, Optional
11
+
12
+ from .cache import ResponseCache
13
+
14
+ if TYPE_CHECKING:
15
+ from ..providers.base import LLMProvider
16
+
17
+
18
+ class AIEngine:
19
+ """
20
+ Wraps an LLMProvider with optional response caching.
21
+ Pass provider=None for deterministic-only mode.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ provider: Optional["LLMProvider"] = None,
27
+ use_cache: bool = True,
28
+ ):
29
+ self.provider = provider
30
+ self.cache: Optional[ResponseCache] = ResponseCache() if use_cache else None
31
+
32
+ # ------------------------------------------------------------------
33
+ # Public API
34
+ # ------------------------------------------------------------------
35
+
36
+ @property
37
+ def is_available(self) -> bool:
38
+ return self.provider is not None
39
+
40
+ def generate_text(self, prompt: str, cache_key: Optional[str] = None) -> str:
41
+ """
42
+ Generate a single text value.
43
+ Uses cache when cache_key is provided and cache is enabled.
44
+ """
45
+ if not self.provider:
46
+ return "[AI Error: No provider configured]"
47
+
48
+ # Cache lookup
49
+ if self.cache and cache_key:
50
+ hit = self.cache.get(cache_key, None, {})
51
+ if hit and isinstance(hit, str):
52
+ return hit
53
+
54
+ result = self.provider.generate(prompt)
55
+
56
+ # Cache store (only on success)
57
+ if self.cache and cache_key and not result.startswith("[AI Error"):
58
+ self.cache.set(cache_key, None, {}, result)
59
+
60
+ return result
61
+
62
+ def generate_semantic_batch(
63
+ self, field_name: str, context: str, count: int
64
+ ) -> list[str]:
65
+ """
66
+ Generate `count` values for one semantic field in a single API call.
67
+ Falls back to repeated placeholders when no provider is configured.
68
+ """
69
+ if not self.provider:
70
+ return [f"[AI Placeholder for {field_name}]"] * count
71
+
72
+ cache_key = f"batch|{field_name}|{context or ''}|{count}"
73
+
74
+ # Cache lookup
75
+ if self.cache:
76
+ hit = self.cache.get(cache_key, None, {})
77
+ if hit and isinstance(hit, list) and len(hit) == count:
78
+ return hit
79
+
80
+ values = self.provider.generate_batch_semantic(field_name, context, count)
81
+
82
+ # Cache store
83
+ if self.cache and values:
84
+ self.cache.set(cache_key, None, {}, values)
85
+
86
+ return values