gateforge-sdk 0.2.5__tar.gz → 0.2.6__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.
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/PKG-INFO +2 -2
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/__init__.py +9 -4
- gateforge_sdk-0.2.6/gateforge/prompt.py +537 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/pyproject.toml +2 -2
- gateforge_sdk-0.2.6/tests/test_phase3.py +630 -0
- gateforge_sdk-0.2.5/gateforge/prompt.py +0 -46
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/.env.example +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/.gitignore +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/.pypirc.example +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/INSTALL.md +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/LICENSE +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/MANIFEST.in +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/PUBLISHING.md +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/PUBLISH_NOW.md +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/README.md +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/ab/__init__.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/ab/engine.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/client.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/config.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/context.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/features/__init__.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/guardrails/__init__.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/guardrails/engine.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/metrics.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/options.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/pii.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/pricing.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/__init__.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/anthropic.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/gemini.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/openai.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/response.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/session_manager.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/tracing.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/__init__.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/anthropic.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/gemini.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/openai.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/__init__.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_metrics.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_phase1.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_phase2.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_pii.py +0 -0
- {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_providers.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gateforge-sdk
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: Privacy-first LLMOps SDK — Auto-init,
|
|
3
|
+
Version: 0.2.6
|
|
4
|
+
Summary: Privacy-first LLMOps SDK — Auto-init, decorators, session management, prompt system with cache
|
|
5
5
|
Project-URL: Homepage, https://gateforge.dev
|
|
6
6
|
Project-URL: Documentation, https://gateforge.dev/docs
|
|
7
7
|
Project-URL: Repository, https://github.com/gateforge/gateforge-sdk
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.6"
|
|
4
4
|
|
|
5
5
|
# Auto-install spaCy model if missing (required for PII detection)
|
|
6
6
|
def _ensure_spacy_model():
|
|
@@ -21,7 +21,7 @@ from gateforge.client import GatforgeClient
|
|
|
21
21
|
from gateforge.context import trace, continue_session, session, tool, agent
|
|
22
22
|
from gateforge.features import FeatureFlags
|
|
23
23
|
from gateforge.options import CallOptions
|
|
24
|
-
from gateforge.prompt import Prompt
|
|
24
|
+
from gateforge.prompt import Prompt, PromptCache, PromptBuilder, get_prompt, set_prompt, get_prompt_cache
|
|
25
25
|
from gateforge.response import GatforgeResponse
|
|
26
26
|
from gateforge.tracing import configure_otel
|
|
27
27
|
from gateforge.ab.engine import VariantAssignment
|
|
@@ -423,10 +423,15 @@ __all__ = [
|
|
|
423
423
|
"wrap_openai",
|
|
424
424
|
"wrap_anthropic",
|
|
425
425
|
"wrap_gemini",
|
|
426
|
-
# Prompt library
|
|
426
|
+
# Prompt library (Phase 3)
|
|
427
|
+
"Prompt",
|
|
428
|
+
"PromptCache",
|
|
429
|
+
"PromptBuilder",
|
|
430
|
+
"get_prompt",
|
|
431
|
+
"set_prompt",
|
|
432
|
+
"get_prompt_cache",
|
|
427
433
|
"get_prompt",
|
|
428
434
|
"get_prompts",
|
|
429
|
-
"Prompt",
|
|
430
435
|
# Conversation tracing
|
|
431
436
|
"trace",
|
|
432
437
|
"continue_session",
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase 3: Prompt System
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Prompt class for versioned prompts with metadata
|
|
6
|
+
- PromptCache for multi-level caching (memory + file + backend)
|
|
7
|
+
- PromptBuilder for prompt composition
|
|
8
|
+
- get_prompt() for fetching prompts from backend
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Optional, Dict, Any, List
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Prompt:
|
|
24
|
+
"""
|
|
25
|
+
Represents a versioned prompt with metadata.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
name: Prompt identifier (e.g., "agent-system")
|
|
29
|
+
content: The actual prompt text
|
|
30
|
+
version: Version number
|
|
31
|
+
variables: Dictionary of variable names to default values
|
|
32
|
+
metadata: Additional metadata (tags, author, created_at, etc.)
|
|
33
|
+
source: Where the prompt came from ("cache", "backend", "file", "builder")
|
|
34
|
+
"""
|
|
35
|
+
name: str
|
|
36
|
+
content: str
|
|
37
|
+
version: int = 1
|
|
38
|
+
variables: Dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
source: str = "unknown"
|
|
41
|
+
|
|
42
|
+
def render(self, **overrides: Any) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Render the prompt with variable substitution.
|
|
45
|
+
|
|
46
|
+
Variables in content should be formatted as {{variable_name}}.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
**overrides: Variable values to override defaults
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Rendered prompt string
|
|
53
|
+
|
|
54
|
+
Usage::
|
|
55
|
+
prompt = Prompt(
|
|
56
|
+
name="greeting",
|
|
57
|
+
content="Hello, {{name}}! You are {{role}}.",
|
|
58
|
+
variables={"name": "User", "role": "a developer"}
|
|
59
|
+
)
|
|
60
|
+
rendered = prompt.render(name="Alice")
|
|
61
|
+
# → "Hello, Alice! You are a developer."
|
|
62
|
+
"""
|
|
63
|
+
variables = {**self.variables, **overrides}
|
|
64
|
+
result = self.content
|
|
65
|
+
|
|
66
|
+
for name, value in variables.items():
|
|
67
|
+
placeholder = "{{" + name + "}}"
|
|
68
|
+
result = result.replace(placeholder, str(value))
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
"""Serialize prompt to dictionary."""
|
|
74
|
+
return {
|
|
75
|
+
"name": self.name,
|
|
76
|
+
"content": self.content,
|
|
77
|
+
"version": self.version,
|
|
78
|
+
"variables": self.variables,
|
|
79
|
+
"metadata": self.metadata,
|
|
80
|
+
"source": self.source,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict[str, Any]) -> Prompt:
|
|
85
|
+
"""Deserialize prompt from dictionary."""
|
|
86
|
+
return cls(
|
|
87
|
+
name=data["name"],
|
|
88
|
+
content=data["content"],
|
|
89
|
+
version=data.get("version", 1),
|
|
90
|
+
variables=data.get("variables", {}),
|
|
91
|
+
metadata=data.get("metadata", {}),
|
|
92
|
+
source=data.get("source", "unknown"),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def fingerprint(self) -> str:
|
|
96
|
+
"""Generate a unique fingerprint for this prompt (for caching)."""
|
|
97
|
+
content = json.dumps(self.to_dict(), sort_keys=True)
|
|
98
|
+
return hashlib.sha256(content.encode()).hexdigest()[:16]
|
|
99
|
+
|
|
100
|
+
def __str__(self) -> str:
|
|
101
|
+
return f"Prompt(name={self.name}, version={self.version}, source={self.source})"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# PromptCache - Multi-level caching
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
class PromptCache:
|
|
109
|
+
"""
|
|
110
|
+
Multi-level cache for prompts.
|
|
111
|
+
|
|
112
|
+
Levels:
|
|
113
|
+
1. Memory (L1) - TTL default 5 minutes
|
|
114
|
+
2. File (L2) - TTL default 1 hour
|
|
115
|
+
3. Backend (L3) - Source of truth
|
|
116
|
+
|
|
117
|
+
Usage::
|
|
118
|
+
cache = PromptCache(memory_ttl=300, file_ttl=3600)
|
|
119
|
+
|
|
120
|
+
# Get from cache (auto-fetches if missing/expired)
|
|
121
|
+
prompt = cache.get("agent-system", version=2)
|
|
122
|
+
|
|
123
|
+
# Set manually
|
|
124
|
+
cache.set(prompt)
|
|
125
|
+
|
|
126
|
+
# Clear cache
|
|
127
|
+
cache.clear()
|
|
128
|
+
cache.clear_memory()
|
|
129
|
+
cache.clear_file()
|
|
130
|
+
|
|
131
|
+
# Check if exists
|
|
132
|
+
if cache.has("agent-system"):
|
|
133
|
+
prompt = cache.get("agent-system")
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
memory_ttl: int = 300, # 5 minutes
|
|
139
|
+
file_ttl: int = 3600, # 1 hour
|
|
140
|
+
cache_dir: Optional[str] = None,
|
|
141
|
+
):
|
|
142
|
+
"""
|
|
143
|
+
Initialize PromptCache.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
memory_ttl: TTL for in-memory cache (seconds)
|
|
147
|
+
file_ttl: TTL for file cache (seconds)
|
|
148
|
+
cache_dir: Directory for file cache (default: ~/.gateforge/prompts)
|
|
149
|
+
"""
|
|
150
|
+
self._memory_cache: Dict[str, Dict[str, Any]] = {}
|
|
151
|
+
self._memory_ttl = memory_ttl
|
|
152
|
+
self._file_ttl = file_ttl
|
|
153
|
+
|
|
154
|
+
# File cache directory
|
|
155
|
+
if cache_dir is None:
|
|
156
|
+
cache_dir = os.path.join(Path.home(), ".gateforge", "prompts")
|
|
157
|
+
self._cache_dir = Path(cache_dir)
|
|
158
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
|
|
160
|
+
def _memory_key(self, name: str, version: Optional[int]) -> str:
|
|
161
|
+
"""Generate memory cache key."""
|
|
162
|
+
v = version if version is not None else "latest"
|
|
163
|
+
return f"{name}:{v}"
|
|
164
|
+
|
|
165
|
+
def _file_path(self, name: str, version: Optional[int]) -> Path:
|
|
166
|
+
"""Generate file cache path."""
|
|
167
|
+
v = version if version is not None else "latest"
|
|
168
|
+
safe_name = name.replace("/", "_").replace("\\", "_")
|
|
169
|
+
return self._cache_dir / f"{safe_name}_v{v}.json"
|
|
170
|
+
|
|
171
|
+
def has(self, name: str, version: Optional[int] = None) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Check if prompt exists in cache (not expired).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
name: Prompt name
|
|
177
|
+
version: Prompt version (None for latest = version 1)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if exists and not expired
|
|
181
|
+
"""
|
|
182
|
+
# If version is None, default to version 1 (latest for new prompts)
|
|
183
|
+
if version is None:
|
|
184
|
+
version = 1
|
|
185
|
+
|
|
186
|
+
key = self._memory_key(name, version)
|
|
187
|
+
|
|
188
|
+
# Check memory
|
|
189
|
+
if key in self._memory_cache:
|
|
190
|
+
entry = self._memory_cache[key]
|
|
191
|
+
if time.time() - entry["timestamp"] < self._memory_ttl:
|
|
192
|
+
return True
|
|
193
|
+
else:
|
|
194
|
+
del self._memory_cache[key]
|
|
195
|
+
|
|
196
|
+
# Check file
|
|
197
|
+
file_path = self._file_path(name, version)
|
|
198
|
+
if file_path.exists():
|
|
199
|
+
try:
|
|
200
|
+
with open(file_path, "r") as f:
|
|
201
|
+
entry = json.load(f)
|
|
202
|
+
if time.time() - entry["timestamp"] < self._file_ttl:
|
|
203
|
+
return True
|
|
204
|
+
else:
|
|
205
|
+
file_path.unlink()
|
|
206
|
+
except (json.JSONDecodeError, KeyError):
|
|
207
|
+
file_path.unlink()
|
|
208
|
+
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
def get(self, name: str, version: Optional[int] = None) -> Optional[Prompt]:
|
|
212
|
+
"""
|
|
213
|
+
Get prompt from cache (returns None if missing/expired).
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
name: Prompt name
|
|
217
|
+
version: Prompt version (None for latest = version 1)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Prompt if found and not expired, None otherwise
|
|
221
|
+
"""
|
|
222
|
+
# If version is None, default to version 1 (latest for new prompts)
|
|
223
|
+
if version is None:
|
|
224
|
+
version = 1
|
|
225
|
+
|
|
226
|
+
key = self._memory_key(name, version)
|
|
227
|
+
|
|
228
|
+
# Check memory (L1)
|
|
229
|
+
if key in self._memory_cache:
|
|
230
|
+
entry = self._memory_cache[key]
|
|
231
|
+
if time.time() - entry["timestamp"] < self._memory_ttl:
|
|
232
|
+
prompt = Prompt.from_dict(entry["data"])
|
|
233
|
+
prompt.source = "memory_cache"
|
|
234
|
+
return prompt
|
|
235
|
+
else:
|
|
236
|
+
del self._memory_cache[key]
|
|
237
|
+
|
|
238
|
+
# Check file (L2)
|
|
239
|
+
file_path = self._file_path(name, version)
|
|
240
|
+
if file_path.exists():
|
|
241
|
+
try:
|
|
242
|
+
with open(file_path, "r") as f:
|
|
243
|
+
entry = json.load(f)
|
|
244
|
+
if time.time() - entry["timestamp"] < self._file_ttl:
|
|
245
|
+
prompt = Prompt.from_dict(entry["data"])
|
|
246
|
+
prompt.source = "file_cache"
|
|
247
|
+
# Promote to memory
|
|
248
|
+
self._memory_cache[key] = {
|
|
249
|
+
"data": entry["data"],
|
|
250
|
+
"timestamp": time.time(),
|
|
251
|
+
}
|
|
252
|
+
return prompt
|
|
253
|
+
else:
|
|
254
|
+
file_path.unlink()
|
|
255
|
+
except (json.JSONDecodeError, KeyError):
|
|
256
|
+
file_path.unlink()
|
|
257
|
+
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
def set(self, prompt: Prompt, version: Optional[int] = None) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Set prompt in cache (both memory and file).
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
prompt: Prompt to cache
|
|
266
|
+
version: Override version (uses prompt.version if None)
|
|
267
|
+
"""
|
|
268
|
+
v = version if version is not None else prompt.version
|
|
269
|
+
key = self._memory_key(prompt.name, v)
|
|
270
|
+
timestamp = time.time()
|
|
271
|
+
|
|
272
|
+
# Set memory (L1)
|
|
273
|
+
self._memory_cache[key] = {
|
|
274
|
+
"data": prompt.to_dict(),
|
|
275
|
+
"timestamp": timestamp,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Set file (L2)
|
|
279
|
+
file_path = self._file_path(prompt.name, v)
|
|
280
|
+
with open(file_path, "w") as f:
|
|
281
|
+
json.dump({
|
|
282
|
+
"data": prompt.to_dict(),
|
|
283
|
+
"timestamp": timestamp,
|
|
284
|
+
}, f, indent=2)
|
|
285
|
+
|
|
286
|
+
def delete(self, name: str, version: Optional[int] = None) -> bool:
|
|
287
|
+
"""
|
|
288
|
+
Delete prompt from cache.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
name: Prompt name
|
|
292
|
+
version: Prompt version (None for all versions)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if deleted, False if not found
|
|
296
|
+
"""
|
|
297
|
+
deleted = False
|
|
298
|
+
|
|
299
|
+
# Delete from memory
|
|
300
|
+
if version is not None:
|
|
301
|
+
key = self._memory_key(name, version)
|
|
302
|
+
if key in self._memory_cache:
|
|
303
|
+
del self._memory_cache[key]
|
|
304
|
+
deleted = True
|
|
305
|
+
else:
|
|
306
|
+
# Delete all versions
|
|
307
|
+
keys_to_delete = [k for k in self._memory_cache if k.startswith(f"{name}:")]
|
|
308
|
+
for key in keys_to_delete:
|
|
309
|
+
del self._memory_cache[key]
|
|
310
|
+
deleted = True
|
|
311
|
+
|
|
312
|
+
# Delete from file
|
|
313
|
+
pattern = name.replace("/", "_").replace("\\", "_")
|
|
314
|
+
for file_path in self._cache_dir.glob(f"{pattern}_v*.json"):
|
|
315
|
+
file_path.unlink()
|
|
316
|
+
deleted = True
|
|
317
|
+
|
|
318
|
+
return deleted
|
|
319
|
+
|
|
320
|
+
def clear(self) -> None:
|
|
321
|
+
"""Clear all caches (memory + file)."""
|
|
322
|
+
self._memory_cache.clear()
|
|
323
|
+
for file_path in self._cache_dir.glob("*.json"):
|
|
324
|
+
file_path.unlink()
|
|
325
|
+
|
|
326
|
+
def clear_memory(self) -> None:
|
|
327
|
+
"""Clear memory cache only."""
|
|
328
|
+
self._memory_cache.clear()
|
|
329
|
+
|
|
330
|
+
def clear_file(self) -> None:
|
|
331
|
+
"""Clear file cache only."""
|
|
332
|
+
for file_path in self._cache_dir.glob("*.json"):
|
|
333
|
+
file_path.unlink()
|
|
334
|
+
|
|
335
|
+
def stats(self) -> Dict[str, Any]:
|
|
336
|
+
"""Get cache statistics."""
|
|
337
|
+
memory_count = len(self._memory_cache)
|
|
338
|
+
file_count = len(list(self._cache_dir.glob("*.json")))
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
"memory_count": memory_count,
|
|
342
|
+
"file_count": file_count,
|
|
343
|
+
"memory_ttl": self._memory_ttl,
|
|
344
|
+
"file_ttl": self._file_ttl,
|
|
345
|
+
"cache_dir": str(self._cache_dir),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# PromptBuilder - Composition
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
class PromptBuilder:
|
|
354
|
+
"""
|
|
355
|
+
Builder for composing prompts from multiple sources.
|
|
356
|
+
|
|
357
|
+
Usage::
|
|
358
|
+
builder = PromptBuilder()
|
|
359
|
+
builder.add_system("You are a helpful assistant")
|
|
360
|
+
builder.add_user("What's the weather?")
|
|
361
|
+
builder.add_variable("location", "Madrid")
|
|
362
|
+
builder.add(gateforge.get_prompt("domain-expert"))
|
|
363
|
+
prompt = builder.build()
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def __init__(self, name: str = "composed-prompt"):
|
|
367
|
+
"""
|
|
368
|
+
Initialize PromptBuilder.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
name: Name for the composed prompt
|
|
372
|
+
"""
|
|
373
|
+
self._name = name
|
|
374
|
+
self._parts: List[Dict[str, str]] = []
|
|
375
|
+
self._variables: Dict[str, Any] = {}
|
|
376
|
+
self._metadata: Dict[str, Any] = {}
|
|
377
|
+
|
|
378
|
+
def add_system(self, text: str) -> PromptBuilder:
|
|
379
|
+
"""Add system message."""
|
|
380
|
+
self._parts.append({"role": "system", "content": text})
|
|
381
|
+
return self
|
|
382
|
+
|
|
383
|
+
def add_user(self, text: str) -> PromptBuilder:
|
|
384
|
+
"""Add user message."""
|
|
385
|
+
self._parts.append({"role": "user", "content": text})
|
|
386
|
+
return self
|
|
387
|
+
|
|
388
|
+
def add_assistant(self, text: str) -> PromptBuilder:
|
|
389
|
+
"""Add assistant message (for few-shot examples)."""
|
|
390
|
+
self._parts.append({"role": "assistant", "content": text})
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def add(self, content: str | Prompt) -> PromptBuilder:
|
|
394
|
+
"""
|
|
395
|
+
Add content or another prompt.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
content: String or Prompt to add
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Self for chaining
|
|
402
|
+
"""
|
|
403
|
+
if isinstance(content, Prompt):
|
|
404
|
+
self._parts.append({"role": "system", "content": content.content})
|
|
405
|
+
self._variables.update(content.variables)
|
|
406
|
+
self._metadata.update(content.metadata)
|
|
407
|
+
else:
|
|
408
|
+
self._parts.append({"role": "system", "content": content})
|
|
409
|
+
return self
|
|
410
|
+
|
|
411
|
+
def add_variable(self, name: str, value: Any) -> PromptBuilder:
|
|
412
|
+
"""Add a variable for rendering."""
|
|
413
|
+
self._variables[name] = value
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
def add_variables(self, variables: Dict[str, Any]) -> PromptBuilder:
|
|
417
|
+
"""Add multiple variables."""
|
|
418
|
+
self._variables.update(variables)
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
def set_metadata(self, key: str, value: Any) -> PromptBuilder:
|
|
422
|
+
"""Set metadata field."""
|
|
423
|
+
self._metadata[key] = value
|
|
424
|
+
return self
|
|
425
|
+
|
|
426
|
+
def build(self, version: int = 1) -> Prompt:
|
|
427
|
+
"""
|
|
428
|
+
Build the final prompt.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
version: Version number for the composed prompt
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Prompt object with all parts combined
|
|
435
|
+
"""
|
|
436
|
+
# Combine all parts into content
|
|
437
|
+
content_parts = []
|
|
438
|
+
for part in self._parts:
|
|
439
|
+
role = part["role"].upper()
|
|
440
|
+
text = part["content"]
|
|
441
|
+
content_parts.append(f"[{role}]\n{text}\n")
|
|
442
|
+
|
|
443
|
+
content = "\n".join(content_parts)
|
|
444
|
+
|
|
445
|
+
return Prompt(
|
|
446
|
+
name=self._name,
|
|
447
|
+
content=content,
|
|
448
|
+
version=version,
|
|
449
|
+
variables=self._variables,
|
|
450
|
+
metadata=self._metadata,
|
|
451
|
+
source="builder",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def build_and_render(self, version: int = 1, **overrides: Any) -> str:
|
|
455
|
+
"""
|
|
456
|
+
Build and render the prompt in one step.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
version: Version number
|
|
460
|
+
**overrides: Variable overrides
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Rendered prompt string
|
|
464
|
+
"""
|
|
465
|
+
prompt = self.build(version)
|
|
466
|
+
return prompt.render(**overrides)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
# Global cache instance
|
|
471
|
+
# ---------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
_default_cache: Optional[PromptCache] = None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def get_prompt_cache() -> PromptCache:
|
|
477
|
+
"""Get or create the default prompt cache."""
|
|
478
|
+
global _default_cache
|
|
479
|
+
if _default_cache is None:
|
|
480
|
+
_default_cache = PromptCache()
|
|
481
|
+
return _default_cache
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def get_prompt(
|
|
485
|
+
name: str,
|
|
486
|
+
version: Optional[int] = None,
|
|
487
|
+
cache: Optional[PromptCache] = None,
|
|
488
|
+
force_refresh: bool = False,
|
|
489
|
+
) -> Optional[Prompt]:
|
|
490
|
+
"""
|
|
491
|
+
Get a prompt by name and version.
|
|
492
|
+
|
|
493
|
+
Currently returns None (backend not implemented).
|
|
494
|
+
Use PromptBuilder for local composition.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
name: Prompt name
|
|
498
|
+
version: Prompt version (None for latest)
|
|
499
|
+
cache: Optional cache instance (uses default if None)
|
|
500
|
+
force_refresh: Force refresh from backend (ignore cache)
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
Prompt if found, None otherwise
|
|
504
|
+
|
|
505
|
+
Usage::
|
|
506
|
+
prompt = gateforge.get_prompt("agent-system", version=2)
|
|
507
|
+
if prompt:
|
|
508
|
+
print(prompt.content)
|
|
509
|
+
"""
|
|
510
|
+
# TODO: Implement backend API call
|
|
511
|
+
# For now, check cache only
|
|
512
|
+
cache = cache or get_prompt_cache()
|
|
513
|
+
|
|
514
|
+
if force_refresh:
|
|
515
|
+
# Delete from cache and re-fetch (TODO: backend call)
|
|
516
|
+
cache.delete(name, version)
|
|
517
|
+
|
|
518
|
+
return cache.get(name, version)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def set_prompt(
|
|
522
|
+
prompt: Prompt,
|
|
523
|
+
cache: Optional[PromptCache] = None,
|
|
524
|
+
) -> None:
|
|
525
|
+
"""
|
|
526
|
+
Set a prompt in the cache.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
prompt: Prompt to cache
|
|
530
|
+
cache: Optional cache instance (uses default if None)
|
|
531
|
+
|
|
532
|
+
Usage::
|
|
533
|
+
prompt = Prompt(name="test", content="Hello", version=1)
|
|
534
|
+
gateforge.set_prompt(prompt)
|
|
535
|
+
"""
|
|
536
|
+
cache = cache or get_prompt_cache()
|
|
537
|
+
cache.set(prompt)
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gateforge-sdk"
|
|
7
|
-
version = "0.2.
|
|
8
|
-
description = "Privacy-first LLMOps SDK — Auto-init,
|
|
7
|
+
version = "0.2.6"
|
|
8
|
+
description = "Privacy-first LLMOps SDK — Auto-init, decorators, session management, prompt system with cache"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.10"
|