swiftpy-framework 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. swiftpy/.gitignore +0 -0
  2. swiftpy/__init__.py +572 -0
  3. swiftpy/admin/__init__.py +50 -0
  4. swiftpy/admin/auth.py +522 -0
  5. swiftpy/admin/registry.py +567 -0
  6. swiftpy/admin/views.py +1583 -0
  7. swiftpy/ai/__init__.py +199 -0
  8. swiftpy/ai/_deterministic.py +40 -0
  9. swiftpy/ai/_fake.py +604 -0
  10. swiftpy/ai/agents/__init__.py +30 -0
  11. swiftpy/ai/agents/base_agent.py +506 -0
  12. swiftpy/ai/agents/memory.py +864 -0
  13. swiftpy/ai/client.py +1011 -0
  14. swiftpy/ai/conversation.py +171 -0
  15. swiftpy/ai/embeddings.py +321 -0
  16. swiftpy/ai/exceptions.py +75 -0
  17. swiftpy/ai/history_strategies.py +157 -0
  18. swiftpy/ai/middleware.py +918 -0
  19. swiftpy/ai/observability/__init__.py +28 -0
  20. swiftpy/ai/observability/drivers.py +763 -0
  21. swiftpy/ai/observability/observer.py +364 -0
  22. swiftpy/ai/pool.py +102 -0
  23. swiftpy/ai/pricing.py +101 -0
  24. swiftpy/ai/prompts/__init__.py +6 -0
  25. swiftpy/ai/prompts/loader.py +343 -0
  26. swiftpy/ai/providers/__init__.py +22 -0
  27. swiftpy/ai/providers/anthropic_provider.py +353 -0
  28. swiftpy/ai/providers/base.py +273 -0
  29. swiftpy/ai/providers/openai_provider.py +275 -0
  30. swiftpy/ai/rag/__init__.py +53 -0
  31. swiftpy/ai/rag/context_formatter.py +263 -0
  32. swiftpy/ai/rag/pipeline_builder.py +544 -0
  33. swiftpy/ai/rag/reranker.py +484 -0
  34. swiftpy/ai/rag/result.py +78 -0
  35. swiftpy/ai/rag/retriever.py +315 -0
  36. swiftpy/ai/redis_store.py +227 -0
  37. swiftpy/ai/streaming.py +115 -0
  38. swiftpy/ai/tools/__init__.py +22 -0
  39. swiftpy/ai/tools/base_tool.py +489 -0
  40. swiftpy/ai/tools/first_party.py +384 -0
  41. swiftpy/ai/tools/tool_loop.py +417 -0
  42. swiftpy/ai/vectorstore/__init__.py +46 -0
  43. swiftpy/ai/vectorstore/base.py +542 -0
  44. swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
  45. swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
  46. swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
  47. swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
  48. swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
  49. swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
  50. swiftpy/auth/__init__.py +50 -0
  51. swiftpy/auth/api_key_guard.py +544 -0
  52. swiftpy/auth/auth_manager.py +259 -0
  53. swiftpy/auth/exceptions.py +71 -0
  54. swiftpy/auth/jwt_guard.py +230 -0
  55. swiftpy/auth/jwt_issuer.py +658 -0
  56. swiftpy/auth/token_blacklist.py +300 -0
  57. swiftpy/cache/__init__.py +14 -0
  58. swiftpy/cache/cache.py +251 -0
  59. swiftpy/cache/driver.py +216 -0
  60. swiftpy/cache/drivers/memory.py +199 -0
  61. swiftpy/cache/drivers/redis.py +421 -0
  62. swiftpy/console/__init__.py +0 -0
  63. swiftpy/console/cli.py +68 -0
  64. swiftpy/console/commands/__init__.py +0 -0
  65. swiftpy/console/commands/cache.py +164 -0
  66. swiftpy/console/commands/make.py +935 -0
  67. swiftpy/console/commands/migrate.py +280 -0
  68. swiftpy/console/commands/new.py +1806 -0
  69. swiftpy/console/commands/queue.py +306 -0
  70. swiftpy/console/commands/seed.py +252 -0
  71. swiftpy/console/commands/start.py +171 -0
  72. swiftpy/console/commands/storage.py +104 -0
  73. swiftpy/console/loader.py +21 -0
  74. swiftpy/console/stubs/__init__.py +26 -0
  75. swiftpy/console/stubs/controller.stub +14 -0
  76. swiftpy/console/stubs/event.stub +14 -0
  77. swiftpy/console/stubs/job.stub +22 -0
  78. swiftpy/console/stubs/listener.stub +14 -0
  79. swiftpy/console/stubs/mail.stub +20 -0
  80. swiftpy/console/stubs/middleware.stub +16 -0
  81. swiftpy/console/stubs/migration.alter.stub +17 -0
  82. swiftpy/console/stubs/migration.api_keys.stub +22 -0
  83. swiftpy/console/stubs/migration.blank.stub +11 -0
  84. swiftpy/console/stubs/migration.create.stub +15 -0
  85. swiftpy/console/stubs/migration.stub +11 -0
  86. swiftpy/console/stubs/migration.update.stub +17 -0
  87. swiftpy/console/stubs/model.stub +17 -0
  88. swiftpy/console/stubs/provider.stub +18 -0
  89. swiftpy/console/stubs/repository.stub +10 -0
  90. swiftpy/console/stubs/request.stub +16 -0
  91. swiftpy/console/stubs/resource.stub +21 -0
  92. swiftpy/console/stubs/schema.stub +12 -0
  93. swiftpy/console/stubs/seeder.stub +14 -0
  94. swiftpy/console/stubs/service.stub +10 -0
  95. swiftpy/core/__init__.py +0 -0
  96. swiftpy/core/application.py +1306 -0
  97. swiftpy/core/bootstrap.py +30 -0
  98. swiftpy/core/config.py +432 -0
  99. swiftpy/core/container.py +596 -0
  100. swiftpy/core/container_exceptions.py +18 -0
  101. swiftpy/core/context.py +148 -0
  102. swiftpy/core/exceptions.py +352 -0
  103. swiftpy/core/providers/base.py +35 -0
  104. swiftpy/database/__init__.py +41 -0
  105. swiftpy/database/exceptions.py +42 -0
  106. swiftpy/database/identifiers.py +105 -0
  107. swiftpy/database/migrations/__init__.py +36 -0
  108. swiftpy/database/migrations/blueprint.py +431 -0
  109. swiftpy/database/migrations/lock.py +104 -0
  110. swiftpy/database/migrations/migration.py +22 -0
  111. swiftpy/database/migrations/migrator.py +507 -0
  112. swiftpy/database/migrations/schema.py +435 -0
  113. swiftpy/database/orm/__init__.py +33 -0
  114. swiftpy/database/orm/fields.py +92 -0
  115. swiftpy/database/orm/model.py +2034 -0
  116. swiftpy/database/orm/registry.py +33 -0
  117. swiftpy/database/orm/relationships.py +836 -0
  118. swiftpy/database/pagination/__init__.py +16 -0
  119. swiftpy/database/pagination/contracts.py +60 -0
  120. swiftpy/database/pagination/paginator.py +112 -0
  121. swiftpy/database/pagination/simple_paginator.py +97 -0
  122. swiftpy/database/pool.py +218 -0
  123. swiftpy/database/query_builder.py +1803 -0
  124. swiftpy/database/seeder.py +39 -0
  125. swiftpy/database/transaction.py +276 -0
  126. swiftpy/events/__init__.py +10 -0
  127. swiftpy/events/dispatcher.py +308 -0
  128. swiftpy/events/redis_bus.py +552 -0
  129. swiftpy/filesystem/__init__.py +227 -0
  130. swiftpy/filesystem/disk.py +378 -0
  131. swiftpy/filesystem/drivers/__init__.py +11 -0
  132. swiftpy/filesystem/drivers/base.py +156 -0
  133. swiftpy/filesystem/drivers/local.py +559 -0
  134. swiftpy/filesystem/drivers/s3.py +655 -0
  135. swiftpy/filesystem/events.py +37 -0
  136. swiftpy/filesystem/exceptions.py +127 -0
  137. swiftpy/filesystem/manager.py +138 -0
  138. swiftpy/filesystem/metadata.py +22 -0
  139. swiftpy/filesystem/paths.py +99 -0
  140. swiftpy/filesystem/service_provider.py +17 -0
  141. swiftpy/filesystem/uploads/__init__.py +13 -0
  142. swiftpy/filesystem/uploads/handlers.py +362 -0
  143. swiftpy/filesystem/uploads/uploaded_file.py +222 -0
  144. swiftpy/filesystem/uploads/validators.py +186 -0
  145. swiftpy/filesystem/visibility.py +22 -0
  146. swiftpy/helpers/__init__.py +42 -0
  147. swiftpy/helpers/helper.py +180 -0
  148. swiftpy/http/exceptions.py +41 -0
  149. swiftpy/http/middleware.py +307 -0
  150. swiftpy/http/request.py +375 -0
  151. swiftpy/http/response.py +442 -0
  152. swiftpy/http/router.py +1050 -0
  153. swiftpy/mail/__init__.py +58 -0
  154. swiftpy/mail/attachments.py +255 -0
  155. swiftpy/mail/config.py +71 -0
  156. swiftpy/mail/envelope.py +183 -0
  157. swiftpy/mail/exceptions.py +90 -0
  158. swiftpy/mail/facade.py +312 -0
  159. swiftpy/mail/mailable.py +53 -0
  160. swiftpy/mail/manager.py +240 -0
  161. swiftpy/mail/message.py +186 -0
  162. swiftpy/mail/models.py +19 -0
  163. swiftpy/mail/queue/__init__.py +5 -0
  164. swiftpy/mail/queue/job.py +279 -0
  165. swiftpy/mail/service_provider.py +19 -0
  166. swiftpy/mail/templates/__init__.py +5 -0
  167. swiftpy/mail/templates/renderer.py +173 -0
  168. swiftpy/mail/transports/__init__.py +21 -0
  169. swiftpy/mail/transports/api/__init__.py +13 -0
  170. swiftpy/mail/transports/api/base.py +194 -0
  171. swiftpy/mail/transports/api/mailgun.py +158 -0
  172. swiftpy/mail/transports/api/resend.py +156 -0
  173. swiftpy/mail/transports/api/sendgrid.py +172 -0
  174. swiftpy/mail/transports/base.py +24 -0
  175. swiftpy/mail/transports/console.py +91 -0
  176. swiftpy/mail/transports/sendmail.py +128 -0
  177. swiftpy/mail/transports/smtp.py +466 -0
  178. swiftpy/queue/__init__.py +13 -0
  179. swiftpy/queue/dispatcher.py +813 -0
  180. swiftpy/queue/job.py +541 -0
  181. swiftpy/queue/worker.py +556 -0
  182. swiftpy/support/__init__.py +12 -0
  183. swiftpy/support/keys.py +108 -0
  184. swiftpy/support/redis_config.py +223 -0
  185. swiftpy/support/strings.py +114 -0
  186. swiftpy/testing/__init__.py +20 -0
  187. swiftpy/testing/fakes/__init__.py +11 -0
  188. swiftpy/testing/fakes/ai_fake.py +21 -0
  189. swiftpy/testing/fakes/vectorstore_fake.py +194 -0
  190. swiftpy/testing/fixtures.py +124 -0
  191. swiftpy/testing/http_client.py +730 -0
  192. swiftpy/testing/test_case.py +126 -0
  193. swiftpy/validation/__init__.py +97 -0
  194. swiftpy/validation/exceptions.py +26 -0
  195. swiftpy/validation/form_request.py +302 -0
  196. swiftpy/validation/protocols.py +33 -0
  197. swiftpy/validation/rules/__init__.py +89 -0
  198. swiftpy/validation/rules/builtin.py +391 -0
  199. swiftpy/validation/rules/exists.py +86 -0
  200. swiftpy/validation/rules/registry.py +290 -0
  201. swiftpy/validation/rules/required.py +55 -0
  202. swiftpy/validation/rules/unique.py +138 -0
  203. swiftpy/validation/validator.py +612 -0
  204. swiftpy/websockets/__init__.py +19 -0
  205. swiftpy/websockets/connection.py +317 -0
  206. swiftpy/websockets/manager.py +271 -0
  207. swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
  208. swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
  209. swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
  210. swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
  211. swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import importlib.util
6
+ import json
7
+ import logging
8
+ import re
9
+ import threading
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from swiftpy.core.config import Config
14
+
15
+ logger = logging.getLogger("swiftpy.ai.prompts")
16
+
17
+ _DEFAULT_CACHE_DIR = Path("bootstrap/cache")
18
+
19
+ _TEMPLATE_TOKEN_PATTERN = re.compile(
20
+ r"(?P<pair>(?:\\\\)+)(?=(?:\\?(?:\{\{|\}\})))"
21
+ r"|\\(?P<escaped>\{\{|\}\})"
22
+ r"|\{\{\s*(?P<var>[a-zA-Z0-9_]+)\s*\}\}"
23
+ )
24
+ VARIABLE_PATTERN = re.compile(r"(?<!\\)\{\{\s*([a-zA-Z0-9_]+)\s*\}\}")
25
+ ESCAPED_OPEN_PATTERN = re.compile(r"\\\{\{")
26
+ ESCAPED_CLOSE_PATTERN = re.compile(r"\\\}\}")
27
+
28
+
29
+ def _extract_variables(template: str) -> list[str]:
30
+ """Extract unescaped {{ variable }} placeholder names in left-to-right order."""
31
+ return [
32
+ m.group("var")
33
+ for m in _TEMPLATE_TOKEN_PATTERN.finditer(template)
34
+ if m.group("var") is not None
35
+ ]
36
+
37
+
38
+ class PromptLoader:
39
+ """
40
+ Prompt manager responsible for loading prompt templates from disk,
41
+ interpolating {{ variable }} placeholders, and caching in production.
42
+ """
43
+
44
+ _instance: PromptLoader | None = None
45
+
46
+ def __init__(
47
+ self,
48
+ base_dir: str | Path | None = None,
49
+ cache: bool | None = None,
50
+ config: Config | None = None,
51
+ ) -> None:
52
+ if base_dir is None:
53
+ self._base_dir = Path("prompts")
54
+ else:
55
+ self._base_dir = Path(base_dir)
56
+
57
+ self._cache_override = cache
58
+ if config is None:
59
+ try:
60
+ self._config = Config.get_instance()
61
+ except RuntimeError:
62
+ self._config = Config()
63
+ else:
64
+ self._config = config
65
+
66
+ self._lock = threading.Lock()
67
+ self._cache: dict[str, str] = {}
68
+ self._load_compiled_cache()
69
+
70
+ def _default_cache_path(self) -> Path:
71
+ """Compute deterministic precompiled cache path isolated to this loader's resolved base_dir."""
72
+ base_hash = hashlib.sha256(
73
+ str(self._base_dir.resolve()).encode("utf-8")
74
+ ).hexdigest()[:16]
75
+ return _DEFAULT_CACHE_DIR / f"prompts_{base_hash}.json"
76
+
77
+ @staticmethod
78
+ def _sidecar_meta_path(cache_path: Path) -> Path:
79
+ return cache_path.with_name(f"{cache_path.name}.meta.json")
80
+
81
+ def _load_compiled_cache(self, cache_path: str | Path | None = None) -> None:
82
+ p = Path(cache_path) if cache_path is not None else self._default_cache_path()
83
+ if not p.is_file():
84
+ return
85
+
86
+ meta_path = self._sidecar_meta_path(p)
87
+ expected_base = str(self._base_dir.resolve())
88
+ if meta_path.is_file():
89
+ try:
90
+ meta = json.loads(meta_path.read_text(encoding="utf-8"))
91
+ if isinstance(meta, dict) and meta.get("base_dir") != expected_base:
92
+ return
93
+ except Exception as exc: # noqa: BLE001
94
+ logger.debug("Could not read prompt cache metadata: %s", exc)
95
+ return
96
+ elif cache_path is None:
97
+ # Scoped default filename already hashes resolved base_dir
98
+ pass
99
+
100
+ try:
101
+ with open(p, "r", encoding="utf-8") as f:
102
+ cached = json.load(f)
103
+ if isinstance(cached, dict):
104
+ with self._lock:
105
+ for key, value in cached.items():
106
+ if isinstance(key, str) and isinstance(value, str):
107
+ self._cache[key] = value
108
+ except Exception as exc: # noqa: BLE001
109
+ logger.debug("Could not read prompt cache: %s", exc)
110
+
111
+ def dump_cache(self, cache_path: str | Path | None = None) -> Path:
112
+ """Recursively scan base prompts directory and precompile templates into a serialized cache."""
113
+ target = Path(cache_path) if cache_path is not None else self._default_cache_path()
114
+ target.parent.mkdir(parents=True, exist_ok=True)
115
+ data: dict[str, str] = {}
116
+ base = self._base_dir.resolve()
117
+ if base.is_dir():
118
+ candidates = sorted(
119
+ (
120
+ p
121
+ for p in base.rglob("*")
122
+ if p.is_file()
123
+ and p.suffix in (".md", ".py")
124
+ and not any(part.startswith(("_", ".")) for part in p.relative_to(base).parts)
125
+ ),
126
+ key=lambda p: (0 if p.suffix == ".md" else 1, p.relative_to(base).as_posix()),
127
+ )
128
+ for p in candidates:
129
+ rel_name = p.relative_to(base).with_suffix("").as_posix()
130
+ if rel_name not in data:
131
+ try:
132
+ data[rel_name] = self.get_template(rel_name)
133
+ except Exception as exc: # noqa: BLE001
134
+ logger.debug("Skipping prompt '%s': %s", rel_name, exc)
135
+
136
+ with open(target, "w", encoding="utf-8") as f:
137
+ json.dump(data, f, indent=2)
138
+
139
+ meta_path = self._sidecar_meta_path(target)
140
+ meta_path.write_text(
141
+ json.dumps({"base_dir": str(base)}, indent=2),
142
+ encoding="utf-8",
143
+ )
144
+ return target
145
+
146
+ @classmethod
147
+ def clear_compiled_cache(cls, cache_path: str | Path | None = None) -> bool:
148
+ """Remove serialized prompt cache file(s) and metadata sidecars if present."""
149
+ removed = False
150
+ if cache_path is not None:
151
+ target = Path(cache_path)
152
+ if target.is_file():
153
+ target.unlink()
154
+ removed = True
155
+ meta = cls._sidecar_meta_path(target)
156
+ if meta.is_file():
157
+ meta.unlink()
158
+ return removed
159
+
160
+ if _DEFAULT_CACHE_DIR.is_dir():
161
+ for p in _DEFAULT_CACHE_DIR.glob("prompts*.json*"):
162
+ if p.is_file():
163
+ p.unlink()
164
+ removed = True
165
+ return removed
166
+
167
+ @classmethod
168
+ def get_instance(cls) -> PromptLoader:
169
+ """Return or create a singleton instance of PromptLoader."""
170
+ if cls._instance is None:
171
+ cls._instance = cls()
172
+ return cls._instance
173
+
174
+ @classmethod
175
+ def reset_instance(cls) -> None:
176
+ """Reset singleton instance (useful in tests)."""
177
+ cls._instance = None
178
+
179
+ @property
180
+ def base_dir(self) -> Path:
181
+ """Return the base prompts directory."""
182
+ return self._base_dir
183
+
184
+ def is_cache_enabled(self) -> bool:
185
+ """
186
+ Check if prompt caching is active.
187
+
188
+ Caching is enabled when explicitly configured or when running in production
189
+ (APP_ENV == 'production').
190
+ """
191
+ if self._cache_override is not None:
192
+ return self._cache_override
193
+
194
+ cfg_cache = self._config.get("ai.prompts.cache")
195
+ if cfg_cache is not None:
196
+ return bool(cfg_cache)
197
+
198
+ app_env = str(self._config.get("app.env", "local")).lower()
199
+ return app_env == "production"
200
+
201
+ def clear_cache(self) -> None:
202
+ """Clear all cached prompt templates."""
203
+ with self._lock:
204
+ self._cache.clear()
205
+
206
+ def get_template(self, name: str, /) -> str:
207
+ """
208
+ Load raw template string for the given prompt name without variable substitution.
209
+
210
+ Searches for {name}.md then {name}.py in the base directory (explicit precedence: .md preferred).
211
+ Guards against directory traversal by verifying resolved path containment.
212
+ Caches the template string when caching is enabled.
213
+ """
214
+ clean_name = name.strip()
215
+ if not clean_name:
216
+ raise ValueError("Prompt name cannot be empty.")
217
+
218
+ # Guard against path traversal
219
+ base_resolved = self._base_dir.resolve()
220
+ path_md = (self._base_dir / f"{clean_name}.md").resolve()
221
+ path_py = (self._base_dir / f"{clean_name}.py").resolve()
222
+
223
+ if not path_md.is_relative_to(base_resolved) or not path_py.is_relative_to(base_resolved):
224
+ raise ValueError(f"Path traversal detected in prompt name: '{clean_name}'")
225
+
226
+ with self._lock:
227
+ if self.is_cache_enabled() and clean_name in self._cache:
228
+ return self._cache[clean_name]
229
+
230
+ template_str: str | None = None
231
+ if path_md.is_file():
232
+ template_str = path_md.read_text(encoding="utf-8")
233
+ elif path_py.is_file():
234
+ template_str = self._load_from_python_file(path_py)
235
+ else:
236
+ raise FileNotFoundError(
237
+ f"Prompt '{clean_name}' not found. Searched {path_md} and {path_py}."
238
+ )
239
+
240
+ with self._lock:
241
+ if self.is_cache_enabled():
242
+ self._cache[clean_name] = template_str
243
+
244
+ return template_str
245
+
246
+ async def aget_template(self, name: str, /) -> str:
247
+ """Asynchronously load raw template string off the asyncio event loop."""
248
+ return await asyncio.to_thread(self.get_template, name)
249
+
250
+ def _load_from_python_file(self, path: Path) -> str:
251
+ """Load prompt template string from a Python script."""
252
+ digest = hashlib.sha256(str(path.resolve()).encode("utf-8")).hexdigest()[:12]
253
+ module_name = f"_swiftpy_prompt_{path.stem}_{digest}"
254
+ spec = importlib.util.spec_from_file_location(module_name, path)
255
+ if spec is None or spec.loader is None:
256
+ raise ImportError(f"Could not load Python prompt specification from {path}")
257
+
258
+ module = importlib.util.module_from_spec(spec)
259
+ spec.loader.exec_module(module)
260
+
261
+ for attr in ("PROMPT", "prompt", "TEMPLATE", "template"):
262
+ if hasattr(module, attr):
263
+ val = getattr(module, attr)
264
+ if isinstance(val, str):
265
+ return val
266
+ if callable(val):
267
+ res = val()
268
+ if isinstance(res, str):
269
+ return res
270
+
271
+ if module.__doc__ and module.__doc__.strip():
272
+ return module.__doc__.strip()
273
+
274
+ raise ValueError(
275
+ f"Python prompt file '{path}' does not define 'PROMPT', 'prompt', or 'TEMPLATE' (str or callable)."
276
+ )
277
+
278
+ def render(self, template: str, /, **variables: Any) -> str:
279
+ """
280
+ Interpolate {{ variable }} placeholders in a single pass.
281
+
282
+ Supports escaped \\{{ ... \\}} placeholders without corrupting substituted
283
+ variable values or preceding literal backslash pairs (e.g. C:\\\\{{ var }}).
284
+ Raises KeyError if any referenced placeholder is missing from variables.
285
+ """
286
+ def replacer(match: re.Match[str]) -> str:
287
+ pair = match.group("pair")
288
+ if pair is not None:
289
+ return "\\" * (len(pair) // 2)
290
+ escaped = match.group("escaped")
291
+ if escaped is not None:
292
+ return escaped
293
+ key = match.group("var")
294
+ if key not in variables:
295
+ raise KeyError(
296
+ f"Missing prompt variable: '{key}'. Provided variables: {list(variables.keys())}"
297
+ )
298
+ return str(variables[key])
299
+
300
+ return _TEMPLATE_TOKEN_PATTERN.sub(replacer, template)
301
+
302
+ def load(self, name: str, /, **variables: Any) -> str:
303
+ """
304
+ Load prompt by name, interpolate {{ variable }} placeholders, and return rendered prompt.
305
+ """
306
+ template = self.get_template(name)
307
+ if not variables:
308
+ remaining = _extract_variables(template)
309
+ if remaining:
310
+ missing = ", ".join(sorted(set(remaining)))
311
+ raise KeyError(
312
+ f"Missing required prompt variables: {missing} for prompt '{name}'"
313
+ )
314
+ return self.render(template, **variables)
315
+
316
+ async def aload(self, name: str, /, **variables: Any) -> str:
317
+ """
318
+ Asynchronously load prompt by name off the event loop, interpolate variables,
319
+ and return the rendered prompt.
320
+ """
321
+ template = await self.aget_template(name)
322
+ if not variables:
323
+ remaining = _extract_variables(template)
324
+ if remaining:
325
+ missing = ", ".join(sorted(set(remaining)))
326
+ raise KeyError(
327
+ f"Missing required prompt variables: {missing} for prompt '{name}'"
328
+ )
329
+ return self.render(template, **variables)
330
+
331
+
332
+ def load_prompt(name: str, /, **variables: Any) -> str:
333
+ """
334
+ Convenience function to load and interpolate a prompt using the default PromptLoader.
335
+ """
336
+ return PromptLoader.get_instance().load(name, **variables)
337
+
338
+
339
+ async def aload_prompt(name: str, /, **variables: Any) -> str:
340
+ """
341
+ Asynchronously load and interpolate a prompt using the default PromptLoader.
342
+ """
343
+ return await PromptLoader.get_instance().aload(name, **variables)
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.ai.providers.anthropic_provider import AnthropicProvider
4
+ from swiftpy.ai.providers.base import (
5
+ LLMRequest,
6
+ LLMResponse,
7
+ Message,
8
+ ProviderInterface,
9
+ TokenUsage,
10
+ )
11
+ from swiftpy.ai.providers.openai_provider import OpenAIProvider
12
+
13
+ __all__ = [
14
+ "AnthropicProvider",
15
+ "LLMRequest",
16
+ "LLMResponse",
17
+ "Message",
18
+ "OpenAIProvider",
19
+ "ProviderInterface",
20
+ "TokenUsage",
21
+ ]
22
+