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.
Files changed (44) hide show
  1. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/PKG-INFO +2 -2
  2. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/__init__.py +9 -4
  3. gateforge_sdk-0.2.6/gateforge/prompt.py +537 -0
  4. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/pyproject.toml +2 -2
  5. gateforge_sdk-0.2.6/tests/test_phase3.py +630 -0
  6. gateforge_sdk-0.2.5/gateforge/prompt.py +0 -46
  7. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/.env.example +0 -0
  8. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/.gitignore +0 -0
  9. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/.pypirc.example +0 -0
  10. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/INSTALL.md +0 -0
  11. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/LICENSE +0 -0
  12. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/MANIFEST.in +0 -0
  13. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/PUBLISHING.md +0 -0
  14. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/PUBLISH_NOW.md +0 -0
  15. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/README.md +0 -0
  16. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/ab/__init__.py +0 -0
  17. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/ab/engine.py +0 -0
  18. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/client.py +0 -0
  19. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/config.py +0 -0
  20. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/context.py +0 -0
  21. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/features/__init__.py +0 -0
  22. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/guardrails/__init__.py +0 -0
  23. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/guardrails/engine.py +0 -0
  24. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/metrics.py +0 -0
  25. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/options.py +0 -0
  26. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/pii.py +0 -0
  27. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/pricing.py +0 -0
  28. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/__init__.py +0 -0
  29. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/anthropic.py +0 -0
  30. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/gemini.py +0 -0
  31. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/providers/openai.py +0 -0
  32. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/response.py +0 -0
  33. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/session_manager.py +0 -0
  34. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/tracing.py +0 -0
  35. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/__init__.py +0 -0
  36. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/anthropic.py +0 -0
  37. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/gemini.py +0 -0
  38. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/gateforge/wrappers/openai.py +0 -0
  39. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/__init__.py +0 -0
  40. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_metrics.py +0 -0
  41. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_phase1.py +0 -0
  42. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_phase2.py +0 -0
  43. {gateforge_sdk-0.2.5 → gateforge_sdk-0.2.6}/tests/test_pii.py +0 -0
  44. {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.5
4
- Summary: Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session management, nested conversation support
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.5"
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.5"
8
- description = "Privacy-first LLMOps SDK — Auto-init, tool/agent decorators, session management, nested conversation support"
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"