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.
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/PKG-INFO +13 -7
- fixtureforge-2.0.0/pyproject.toml +78 -0
- fixtureforge-2.0.0/src/fixtureforge/__init__.py +413 -0
- fixtureforge-2.0.0/src/fixtureforge/ai/engine.py +86 -0
- fixtureforge-2.0.0/src/fixtureforge/ai/prompts.py +156 -0
- fixtureforge-2.0.0/src/fixtureforge/config/__init__.py +4 -0
- fixtureforge-2.0.0/src/fixtureforge/config/flags.py +55 -0
- fixtureforge-2.0.0/src/fixtureforge/core/batch_engine.py +118 -0
- fixtureforge-2.0.0/src/fixtureforge/core/compression.py +285 -0
- fixtureforge-2.0.0/src/fixtureforge/core/dataset.py +161 -0
- fixtureforge-2.0.0/src/fixtureforge/core/generator.py +209 -0
- fixtureforge-2.0.0/src/fixtureforge/core/parser.py +119 -0
- fixtureforge-2.0.0/src/fixtureforge/core/recipe.py +106 -0
- fixtureforge-2.0.0/src/fixtureforge/core/router.py +111 -0
- fixtureforge-2.0.0/src/fixtureforge/core/swarm.py +139 -0
- fixtureforge-2.0.0/src/fixtureforge/memory/__init__.py +5 -0
- fixtureforge-2.0.0/src/fixtureforge/memory/dream.py +413 -0
- fixtureforge-2.0.0/src/fixtureforge/memory/store.py +287 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/__init__.py +42 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/anthropic.py +56 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/base.py +37 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/factory.py +100 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/gemini.py +87 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/groq.py +25 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/ollama.py +77 -0
- fixtureforge-2.0.0/src/fixtureforge/providers/openai.py +70 -0
- fixtureforge-2.0.0/src/fixtureforge/security/__init__.py +8 -0
- fixtureforge-2.0.0/src/fixtureforge/security/permissions.py +257 -0
- fixtureforge-0.1.0/pyproject.toml +0 -37
- fixtureforge-0.1.0/src/fixtureforge/__init__.py +0 -70
- fixtureforge-0.1.0/src/fixtureforge/ai/engine.py +0 -44
- fixtureforge-0.1.0/src/fixtureforge/ai/prompts.py +0 -31
- fixtureforge-0.1.0/src/fixtureforge/core/generator.py +0 -135
- fixtureforge-0.1.0/src/fixtureforge/core/parser.py +0 -108
- fixtureforge-0.1.0/src/fixtureforge/core/recipe.py +0 -76
- fixtureforge-0.1.0/src/fixtureforge/core/router.py +0 -50
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/LICENSE +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/README.md +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/ai/__init__.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/ai/cache.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/cli/__init__.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/cli/commands.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/__init__.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/analyzer.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/exporter.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/core/streamer.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/integrations/__init__.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/integrations/github.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/integrations/jira.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/library/__init__.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/library/sharing.py +0 -0
- {fixtureforge-0.1.0 → fixtureforge-2.0.0}/src/fixtureforge/library/storage.py +0 -0
- {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.
|
|
4
|
-
Summary:
|
|
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
|
-
|
|
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.
|
|
20
|
-
Requires-Dist:
|
|
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
|