pixie-prompts 0.0.0__tar.gz → 0.1.1__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.
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/PKG-INFO +2 -1
- pixie_prompts-0.1.1/pixie/prompts/__init__.py +19 -0
- pixie_prompts-0.1.1/pixie/prompts/file_watcher.py +327 -0
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/pixie/prompts/graphql.py +17 -10
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/pixie/prompts/prompt.py +4 -4
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/pixie/prompts/server.py +16 -48
- pixie_prompts-0.1.1/pixie/prompts/storage.py +399 -0
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/pyproject.toml +4 -3
- pixie_prompts-0.0.0/pixie/prompts/__init__.py +0 -0
- pixie_prompts-0.0.0/pixie/prompts/storage.py +0 -228
- pixie_prompts-0.0.0/pixie/tests/__init__.py +0 -0
- pixie_prompts-0.0.0/pixie/tests/test_prompt.py +0 -1321
- pixie_prompts-0.0.0/pixie/tests/test_prompt_management.py +0 -117
- pixie_prompts-0.0.0/pixie/tests/test_prompt_storage.py +0 -1453
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/LICENSE +0 -0
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/README.md +0 -0
- {pixie_prompts-0.0.0 → pixie_prompts-0.1.1}/pixie/prompts/prompt_management.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixie-prompts
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Code-first, type-safe prompt management
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -23,6 +23,7 @@ Requires-Dist: jsonsubschema (>=0.0.7,<0.0.8)
|
|
|
23
23
|
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
24
24
|
Requires-Dist: strawberry-graphql (>=0.288.1) ; extra == "server"
|
|
25
25
|
Requires-Dist: uvicorn (>=0.40.0) ; extra == "server"
|
|
26
|
+
Requires-Dist: watchdog (>=6.0.0) ; extra == "server"
|
|
26
27
|
Project-URL: Changelog, https://github.com/yiouli/pixie-prompts/commits/main/
|
|
27
28
|
Project-URL: Documentation, https://yiouli.github.io/pixie-prompts/
|
|
28
29
|
Project-URL: Homepage, https://gopixie.ai
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from . import graphql
|
|
2
|
+
from .prompt import Prompt, Variables
|
|
3
|
+
from .prompt_management import (
|
|
4
|
+
create_prompt,
|
|
5
|
+
get_prompt,
|
|
6
|
+
list_prompts,
|
|
7
|
+
)
|
|
8
|
+
from .storage import initialize_prompt_storage, StorageBackedPrompt
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Prompt",
|
|
12
|
+
"Variables",
|
|
13
|
+
"StorageBackedPrompt",
|
|
14
|
+
"create_prompt",
|
|
15
|
+
"get_prompt",
|
|
16
|
+
"graphql",
|
|
17
|
+
"initialize_prompt_storage",
|
|
18
|
+
"list_prompts",
|
|
19
|
+
]
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import importlib.util
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from watchdog.events import FileSystemEventHandler, FileSystemEvent
|
|
9
|
+
from watchdog.observers import Observer
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from pixie.prompts.storage import initialize_prompt_storage
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_storage_observer: Observer | None = None # type: ignore
|
|
18
|
+
_storage_reload_task: asyncio.Task | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def discover_and_load_modules():
|
|
22
|
+
"""Discover and load all Python files that use pixie.prompts.create_prompt, or pixie.create_prompt.
|
|
23
|
+
|
|
24
|
+
This function recursively searches the current working directory for Python files
|
|
25
|
+
"""
|
|
26
|
+
cwd = Path.cwd()
|
|
27
|
+
# Recursively find all Python files
|
|
28
|
+
python_files = list(cwd.rglob("*.py"))
|
|
29
|
+
|
|
30
|
+
if not python_files:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
# Add current directory to Python path if not already there
|
|
34
|
+
if str(cwd) not in sys.path:
|
|
35
|
+
sys.path.insert(0, str(cwd))
|
|
36
|
+
|
|
37
|
+
loaded_count = 0
|
|
38
|
+
for py_file in python_files:
|
|
39
|
+
# Skip __init__.py, private files, and anything in site-packages/venv
|
|
40
|
+
if py_file.name.startswith("_") or any(
|
|
41
|
+
part in py_file.parts
|
|
42
|
+
for part in ["site-packages", ".venv", "venv", "__pycache__"]
|
|
43
|
+
):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Quick check if file imports register_application
|
|
47
|
+
content = py_file.read_text()
|
|
48
|
+
if not re.search(r"(?:^|\s)pixie(?=[\s.])", content):
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# Check for ignore directive
|
|
52
|
+
if any(
|
|
53
|
+
re.fullmatch(r"#\s*pixie\s*:\s*ignore", line.strip())
|
|
54
|
+
for line in content.splitlines()
|
|
55
|
+
):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Load the module with a unique name based on path
|
|
59
|
+
relative_path = py_file.relative_to(cwd)
|
|
60
|
+
module_name = str(relative_path.with_suffix("")).replace("/", ".")
|
|
61
|
+
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
|
62
|
+
if spec and spec.loader:
|
|
63
|
+
try:
|
|
64
|
+
module = importlib.util.module_from_spec(spec)
|
|
65
|
+
sys.modules[module_name] = module
|
|
66
|
+
spec.loader.exec_module(module)
|
|
67
|
+
loaded_count += 1
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error("Failed to load module %s: %s", module_name, e)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _reload_prompt_storage() -> None:
|
|
73
|
+
"""Reload prompts from disk and refresh any actualized prompts."""
|
|
74
|
+
from pixie.prompts import storage as storage_module
|
|
75
|
+
|
|
76
|
+
storage_instance = storage_module._storage_instance
|
|
77
|
+
if storage_instance is None:
|
|
78
|
+
logger.warning("Prompt storage is not initialized; skip reload.")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
storage_instance.load()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _debounced_reload(delay_seconds: float) -> None:
|
|
85
|
+
"""Reload storage after a debounce window to collapse bursty events."""
|
|
86
|
+
try:
|
|
87
|
+
await asyncio.sleep(delay_seconds)
|
|
88
|
+
logger.info("Detected prompt storage change; reloading prompts...")
|
|
89
|
+
_reload_prompt_storage()
|
|
90
|
+
except asyncio.CancelledError:
|
|
91
|
+
raise
|
|
92
|
+
except Exception:
|
|
93
|
+
logger.exception("Failed to reload prompts after storage change.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _StorageChangeHandler(FileSystemEventHandler):
|
|
97
|
+
"""Watchdog handler that schedules prompt reloads on any change."""
|
|
98
|
+
|
|
99
|
+
# Patterns to ignore
|
|
100
|
+
IGNORE_PATTERNS = {
|
|
101
|
+
".vscode",
|
|
102
|
+
".git",
|
|
103
|
+
"__pycache__",
|
|
104
|
+
".pytest_cache",
|
|
105
|
+
".mypy_cache",
|
|
106
|
+
".ruff_cache",
|
|
107
|
+
".ipynb_checkpoints",
|
|
108
|
+
"~", # Vim swap files
|
|
109
|
+
".swp",
|
|
110
|
+
".swo",
|
|
111
|
+
".swx",
|
|
112
|
+
".tmp",
|
|
113
|
+
".temp",
|
|
114
|
+
".DS_Store",
|
|
115
|
+
"thumbs.db",
|
|
116
|
+
".lock",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
ALLOWED_EVENT_TYPES = {"created", "modified", "deleted", "moved"}
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
loop: asyncio.AbstractEventLoop,
|
|
124
|
+
debounce_seconds: float,
|
|
125
|
+
watch_extensions: set[str] | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
self._loop = loop
|
|
128
|
+
self._debounce_seconds = debounce_seconds
|
|
129
|
+
# Default to common prompt file formats if not specified
|
|
130
|
+
self._watch_extensions = watch_extensions or {
|
|
131
|
+
".json",
|
|
132
|
+
".jinja",
|
|
133
|
+
}
|
|
134
|
+
self._last_seen: dict[Path, tuple[float | None, int | None]] = {}
|
|
135
|
+
|
|
136
|
+
def _should_ignore_event(self, event: FileSystemEvent) -> bool:
|
|
137
|
+
"""Check if event should be ignored."""
|
|
138
|
+
src = event.src_path
|
|
139
|
+
if not isinstance(src, str):
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
path = Path(src)
|
|
143
|
+
|
|
144
|
+
# Check primary path
|
|
145
|
+
if self._is_ignored_path(path):
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
# For move events, also check destination
|
|
149
|
+
if hasattr(event, "dest_path") and event.dest_path:
|
|
150
|
+
dest = event.dest_path
|
|
151
|
+
if not isinstance(dest, str):
|
|
152
|
+
return True
|
|
153
|
+
dest_path = Path(dest)
|
|
154
|
+
# Ignore if either source or dest should be ignored
|
|
155
|
+
if self._is_ignored_path(dest_path):
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def _is_ignored_path(self, path: Path) -> bool:
|
|
161
|
+
"""Check if a path should be ignored."""
|
|
162
|
+
path_str = str(path)
|
|
163
|
+
|
|
164
|
+
# Ignore if path contains any ignore pattern
|
|
165
|
+
if any(pattern in path_str for pattern in self.IGNORE_PATTERNS):
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
# Ignore temporary files (various editor patterns)
|
|
169
|
+
filename = path.name
|
|
170
|
+
|
|
171
|
+
# VSCode creates files like: .file.txt.12345~
|
|
172
|
+
if filename.startswith(".") and "~" in filename:
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
# Hidden files starting with .
|
|
176
|
+
if filename.startswith("."):
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# Backup files
|
|
180
|
+
if filename.startswith("~") or filename.endswith("~"):
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
# Temporary suffixes
|
|
184
|
+
if any(
|
|
185
|
+
filename.endswith(suffix)
|
|
186
|
+
for suffix in (".tmp", ".temp", ".swp", ".swo", ".swx", ".bak", ".lock")
|
|
187
|
+
):
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
# For file events (not directories), check extension
|
|
191
|
+
if path.is_file() or (not path.exists() and path.suffix):
|
|
192
|
+
if path.suffix not in self._watch_extensions:
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def on_any_event(self, event: FileSystemEvent) -> None:
|
|
198
|
+
# Filter out unwanted events
|
|
199
|
+
if event.event_type not in self.ALLOWED_EVENT_TYPES:
|
|
200
|
+
return
|
|
201
|
+
if self._should_ignore_event(event):
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Skip duplicate events where the file's mtime/size did not change
|
|
205
|
+
if not event.is_directory and event.event_type in {"created", "modified"}:
|
|
206
|
+
src = event.src_path
|
|
207
|
+
if isinstance(src, str):
|
|
208
|
+
path = Path(src)
|
|
209
|
+
if self._is_ignored_path(path):
|
|
210
|
+
return
|
|
211
|
+
if self._is_duplicate_event(path):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# Ignore pure directory events (unless it's a delete/move that could affect contents)
|
|
215
|
+
# We still want to know if a directory containing prompt files was deleted/moved
|
|
216
|
+
if event.is_directory and event.event_type not in ("deleted", "moved"):
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
logger.debug(
|
|
220
|
+
"Storage change detected: %s (%s)%s",
|
|
221
|
+
event.src_path,
|
|
222
|
+
event.event_type,
|
|
223
|
+
f" -> {event.dest_path}" if hasattr(event, "dest_path") else "",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Avoid blocking the watchdog thread; schedule reload on the event loop.
|
|
227
|
+
self._loop.call_soon_threadsafe(self._trigger_reload)
|
|
228
|
+
|
|
229
|
+
def _trigger_reload(self) -> None:
|
|
230
|
+
global _storage_reload_task
|
|
231
|
+
|
|
232
|
+
# Cancel existing reload task to reset the debounce timer
|
|
233
|
+
if _storage_reload_task is not None and not _storage_reload_task.done():
|
|
234
|
+
_storage_reload_task.cancel()
|
|
235
|
+
|
|
236
|
+
_storage_reload_task = self._loop.create_task(
|
|
237
|
+
_debounced_reload(self._debounce_seconds)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _is_duplicate_event(self, path: Path) -> bool:
|
|
241
|
+
"""Return True if the event is a repeat with unchanged mtime/size."""
|
|
242
|
+
try:
|
|
243
|
+
stat = path.stat()
|
|
244
|
+
fingerprint = (stat.st_mtime, stat.st_size)
|
|
245
|
+
except FileNotFoundError:
|
|
246
|
+
self._last_seen.pop(path, None)
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
last = self._last_seen.get(path)
|
|
250
|
+
if last == fingerprint:
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
self._last_seen[path] = fingerprint
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def start_storage_watcher(
|
|
258
|
+
storage_directory: Path,
|
|
259
|
+
debounce_seconds: float,
|
|
260
|
+
watch_extensions: set[str] | None = None,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Start a watchdog observer on the prompt storage directory.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
storage_directory: Directory to watch for changes
|
|
266
|
+
debounce_seconds: Delay before triggering reload (collapses rapid events)
|
|
267
|
+
watch_extensions: Set of file extensions to watch (e.g., {'.yaml', '.json'})
|
|
268
|
+
"""
|
|
269
|
+
global _storage_observer
|
|
270
|
+
|
|
271
|
+
if _storage_observer is not None:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
loop = asyncio.get_running_loop()
|
|
275
|
+
storage_directory.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
|
|
277
|
+
handler = _StorageChangeHandler(loop, debounce_seconds, watch_extensions)
|
|
278
|
+
observer = Observer()
|
|
279
|
+
observer.schedule(handler, str(storage_directory), recursive=True)
|
|
280
|
+
observer.start()
|
|
281
|
+
|
|
282
|
+
_storage_observer = observer
|
|
283
|
+
|
|
284
|
+
extensions_str = ", ".join(sorted(handler._watch_extensions))
|
|
285
|
+
logger.info(
|
|
286
|
+
"Watching prompt storage at %s for %s files (debounce %.2fs)",
|
|
287
|
+
storage_directory,
|
|
288
|
+
extensions_str,
|
|
289
|
+
debounce_seconds,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def stop_storage_watcher() -> None:
|
|
294
|
+
"""Stop the watchdog observer if running."""
|
|
295
|
+
global _storage_observer, _storage_reload_task
|
|
296
|
+
|
|
297
|
+
if _storage_reload_task is not None:
|
|
298
|
+
_storage_reload_task.cancel()
|
|
299
|
+
try:
|
|
300
|
+
await _storage_reload_task
|
|
301
|
+
except asyncio.CancelledError:
|
|
302
|
+
pass
|
|
303
|
+
_storage_reload_task = None
|
|
304
|
+
|
|
305
|
+
if _storage_observer is not None:
|
|
306
|
+
_storage_observer.stop()
|
|
307
|
+
await asyncio.to_thread(_storage_observer.join, 5.0)
|
|
308
|
+
_storage_observer = None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def init_prompt_storage():
|
|
312
|
+
|
|
313
|
+
storage_directory = os.getenv("PIXIE_PROMPT_STORAGE_DIR", ".pixie/prompts")
|
|
314
|
+
initialize_prompt_storage(storage_directory)
|
|
315
|
+
|
|
316
|
+
@asynccontextmanager
|
|
317
|
+
async def lifespan(app: FastAPI):
|
|
318
|
+
try:
|
|
319
|
+
nonlocal storage_directory
|
|
320
|
+
storage_path = Path(storage_directory)
|
|
321
|
+
watch_interval = float(os.getenv("PIXIE_PROMPT_WATCH_INTERVAL", "1.0"))
|
|
322
|
+
await start_storage_watcher(storage_path, watch_interval)
|
|
323
|
+
yield
|
|
324
|
+
finally:
|
|
325
|
+
await stop_storage_watcher()
|
|
326
|
+
|
|
327
|
+
return lifespan
|
|
@@ -9,7 +9,6 @@ from strawberry.scalars import JSON
|
|
|
9
9
|
|
|
10
10
|
from pixie.prompts.prompt import variables_definition_to_schema
|
|
11
11
|
from pixie.prompts.prompt_management import get_prompt, list_prompts
|
|
12
|
-
from importlib.metadata import PackageNotFoundError, version
|
|
13
12
|
|
|
14
13
|
logger = logging.getLogger(__name__)
|
|
15
14
|
|
|
@@ -41,13 +40,22 @@ class TKeyValue:
|
|
|
41
40
|
value: str
|
|
42
41
|
|
|
43
42
|
|
|
43
|
+
@strawberry.type
|
|
44
|
+
class PromptVersion:
|
|
45
|
+
"""Prompt version information."""
|
|
46
|
+
|
|
47
|
+
version_id: str
|
|
48
|
+
content: str
|
|
49
|
+
created_at: float
|
|
50
|
+
|
|
51
|
+
|
|
44
52
|
@strawberry.type
|
|
45
53
|
class Prompt:
|
|
46
54
|
"""Full prompt information including versions."""
|
|
47
55
|
|
|
48
56
|
id: strawberry.ID
|
|
49
57
|
variables_schema: JSON
|
|
50
|
-
versions: list[
|
|
58
|
+
versions: list[PromptVersion]
|
|
51
59
|
default_version_id: str | None
|
|
52
60
|
"""default version id can only be None if versions is empty"""
|
|
53
61
|
description: Optional[str] = None
|
|
@@ -62,13 +70,7 @@ class Query:
|
|
|
62
70
|
async def health_check(self) -> str:
|
|
63
71
|
"""Health check endpoint."""
|
|
64
72
|
logger.debug("Health check endpoint called")
|
|
65
|
-
|
|
66
|
-
version_str = version("pixie-sdk")
|
|
67
|
-
logger.debug("Pixie SDK version: %s", version_str)
|
|
68
|
-
return version_str
|
|
69
|
-
except PackageNotFoundError as e:
|
|
70
|
-
logger.warning("Failed to get Pixie SDK version: %s", str(e))
|
|
71
|
-
return "0.0.0"
|
|
73
|
+
return "0.0.0"
|
|
72
74
|
|
|
73
75
|
@strawberry.field
|
|
74
76
|
def list_prompts(self) -> list[PromptMetadata]:
|
|
@@ -123,7 +125,12 @@ class Query:
|
|
|
123
125
|
module=prompt_with_registration.module,
|
|
124
126
|
)
|
|
125
127
|
versions_dict = prompt.get_versions()
|
|
126
|
-
versions = [
|
|
128
|
+
versions = [
|
|
129
|
+
PromptVersion(
|
|
130
|
+
version_id=k, content=v, created_at=prompt.get_version_creation_time(k)
|
|
131
|
+
)
|
|
132
|
+
for k, v in versions_dict.items()
|
|
133
|
+
]
|
|
127
134
|
default_version_id: str = prompt.get_default_version_id()
|
|
128
135
|
variables_schema = prompt.get_variables_schema()
|
|
129
136
|
return Prompt(
|
|
@@ -11,12 +11,12 @@ from jsonsubschema import isSubschema
|
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class
|
|
14
|
+
class Variables(BaseModel):
|
|
15
15
|
# TODO add validation to prevent fields using reserved names
|
|
16
16
|
pass
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
TPromptVar = TypeVar("TPromptVar", bound=
|
|
19
|
+
TPromptVar = TypeVar("TPromptVar", bound=Variables | None)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
_prompt_registry: dict[str, "BasePrompt"] = {}
|
|
@@ -36,7 +36,7 @@ class _CompiledPrompt:
|
|
|
36
36
|
value: str
|
|
37
37
|
prompt: "BasePrompt | OutdatedPrompt"
|
|
38
38
|
version_id: str
|
|
39
|
-
variables:
|
|
39
|
+
variables: Variables | None
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
_compiled_prompt_registry: dict[int, _CompiledPrompt] = {}
|
|
@@ -186,7 +186,7 @@ def variables_definition_to_schema(definition: type[TPromptVar]) -> dict[str, An
|
|
|
186
186
|
if definition is NoneType:
|
|
187
187
|
return EMPTY_VARIABLES_SCHEMA
|
|
188
188
|
|
|
189
|
-
return cast(type[
|
|
189
|
+
return cast(type[Variables], definition).model_json_schema()
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
class BasePrompt(BaseUntypedPrompt, Generic[TPromptVar]):
|
|
@@ -4,9 +4,6 @@ import argparse
|
|
|
4
4
|
import os
|
|
5
5
|
import colorlog
|
|
6
6
|
import logging
|
|
7
|
-
import sys
|
|
8
|
-
import importlib.util
|
|
9
|
-
from pathlib import Path
|
|
10
7
|
from urllib.parse import quote
|
|
11
8
|
|
|
12
9
|
import dotenv
|
|
@@ -15,7 +12,10 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
|
15
12
|
from strawberry.fastapi import GraphQLRouter
|
|
16
13
|
import uvicorn
|
|
17
14
|
|
|
18
|
-
from pixie.prompts.
|
|
15
|
+
from pixie.prompts.file_watcher import (
|
|
16
|
+
discover_and_load_modules,
|
|
17
|
+
init_prompt_storage,
|
|
18
|
+
)
|
|
19
19
|
from pixie.prompts.graphql import schema
|
|
20
20
|
|
|
21
21
|
|
|
@@ -25,42 +25,6 @@ logger = logging.getLogger(__name__)
|
|
|
25
25
|
_logging_mode: str = "default"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def discover_and_load_prompts():
|
|
29
|
-
"""Discover and load all Python files that use pixie.prompts.create_prompt, or pixie.create_prompt.
|
|
30
|
-
|
|
31
|
-
This function recursively searches the current working directory for Python files
|
|
32
|
-
"""
|
|
33
|
-
cwd = Path.cwd()
|
|
34
|
-
# Recursively find all Python files
|
|
35
|
-
python_files = list(cwd.rglob("*.py"))
|
|
36
|
-
|
|
37
|
-
if not python_files:
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
# Add current directory to Python path if not already there
|
|
41
|
-
if str(cwd) not in sys.path:
|
|
42
|
-
sys.path.insert(0, str(cwd))
|
|
43
|
-
|
|
44
|
-
loaded_count = 0
|
|
45
|
-
for py_file in python_files:
|
|
46
|
-
# Skip __init__.py, private files, and anything in site-packages/venv
|
|
47
|
-
if py_file.name.startswith("_") or any(
|
|
48
|
-
part in py_file.parts
|
|
49
|
-
for part in ["site-packages", ".venv", "venv", "__pycache__"]
|
|
50
|
-
):
|
|
51
|
-
continue
|
|
52
|
-
|
|
53
|
-
# Load the module with a unique name based on path
|
|
54
|
-
relative_path = py_file.relative_to(cwd)
|
|
55
|
-
module_name = str(relative_path.with_suffix("")).replace("/", ".")
|
|
56
|
-
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
|
57
|
-
if spec and spec.loader:
|
|
58
|
-
module = importlib.util.module_from_spec(spec)
|
|
59
|
-
sys.modules[module_name] = module
|
|
60
|
-
spec.loader.exec_module(module)
|
|
61
|
-
loaded_count += 1
|
|
62
|
-
|
|
63
|
-
|
|
64
28
|
def setup_logging(mode: str = "default"):
|
|
65
29
|
"""Configure logging for the entire application.
|
|
66
30
|
|
|
@@ -123,22 +87,26 @@ def create_app() -> FastAPI:
|
|
|
123
87
|
setup_logging(_logging_mode)
|
|
124
88
|
|
|
125
89
|
# Discover and load applications on every app creation (including reloads)
|
|
126
|
-
|
|
90
|
+
discover_and_load_modules()
|
|
127
91
|
|
|
128
92
|
dotenv.load_dotenv(os.getcwd() + "/.env")
|
|
129
|
-
|
|
130
|
-
initialize_prompt_storage(storage_directory)
|
|
93
|
+
lifespan = init_prompt_storage()
|
|
131
94
|
|
|
132
95
|
app = FastAPI(
|
|
133
|
-
title="Pixie
|
|
134
|
-
description="Server for
|
|
96
|
+
title="Pixie Prompts Dev Server",
|
|
97
|
+
description="Server for managing prompts",
|
|
135
98
|
version="0.1.0",
|
|
99
|
+
lifespan=lifespan,
|
|
136
100
|
)
|
|
137
|
-
|
|
101
|
+
# Matches:
|
|
102
|
+
# 1. http://localhost followed by an optional port (:8080, :3000, etc.)
|
|
103
|
+
# 2. http://127.0.0.1 followed by an optional port
|
|
104
|
+
# 3. https://yourdomain.com (the production domain)
|
|
105
|
+
origins_regex = r"http://(localhost|127\.0\.0\.1)(:\d+)?|https://gopixie\.ai"
|
|
138
106
|
# Add CORS middleware
|
|
139
107
|
app.add_middleware(
|
|
140
108
|
CORSMiddleware,
|
|
141
|
-
|
|
109
|
+
allow_origin_regex=origins_regex,
|
|
142
110
|
allow_credentials=True,
|
|
143
111
|
allow_methods=["*"], # Allows all methods
|
|
144
112
|
allow_headers=["*"], # Allows all headers
|
|
@@ -155,7 +123,7 @@ def create_app() -> FastAPI:
|
|
|
155
123
|
@app.get("/")
|
|
156
124
|
async def root():
|
|
157
125
|
return {
|
|
158
|
-
"message": "Pixie
|
|
126
|
+
"message": "Pixie Prompts Dev Server",
|
|
159
127
|
"graphiql": "/graphql",
|
|
160
128
|
"version": "0.1.0",
|
|
161
129
|
}
|