pixie-prompts 0.0.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.
@@ -0,0 +1,263 @@
1
+ """FastAPI server for SDK."""
2
+
3
+ import argparse
4
+ import os
5
+ import colorlog
6
+ import logging
7
+ import sys
8
+ import importlib.util
9
+ from pathlib import Path
10
+ from urllib.parse import quote
11
+
12
+ import dotenv
13
+ from fastapi import FastAPI
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from strawberry.fastapi import GraphQLRouter
16
+ import uvicorn
17
+
18
+ from pixie.prompts.storage import initialize_prompt_storage
19
+ from pixie.prompts.graphql import schema
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Global logging mode
25
+ _logging_mode: str = "default"
26
+
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
+ def setup_logging(mode: str = "default"):
65
+ """Configure logging for the entire application.
66
+
67
+ Sets up colored logging with consistent formatting for all loggers.
68
+
69
+ Args:
70
+ mode: Logging mode - "default", "verbose", or "debug"
71
+ - default: INFO for server events, WARNING+ for all modules
72
+ - verbose: INFO+ for all modules
73
+ - debug: DEBUG+ for all modules
74
+ """
75
+ global _logging_mode
76
+ _logging_mode = mode
77
+
78
+ # Determine log level based on mode
79
+ if mode == "debug":
80
+ level = logging.DEBUG
81
+ elif mode == "verbose":
82
+ level = logging.INFO
83
+ else: # default
84
+ level = logging.INFO
85
+
86
+ colorlog.basicConfig(
87
+ level=level,
88
+ format="[%(log_color)s%(levelname)-8s%(reset)s][%(asctime)s]\t%(message)s",
89
+ datefmt="%H:%M:%S",
90
+ log_colors={
91
+ "DEBUG": "cyan",
92
+ "INFO": "green",
93
+ "WARNING": "yellow",
94
+ "ERROR": "red",
95
+ "CRITICAL": "red,bg_white",
96
+ },
97
+ force=True,
98
+ )
99
+
100
+ # Configure uvicorn loggers to use the same format
101
+ for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error"]:
102
+ uvicorn_logger = logging.getLogger(logger_name)
103
+ uvicorn_logger.handlers = []
104
+ uvicorn_logger.propagate = True
105
+
106
+ # In default mode, set most loggers to WARNING+ except specific modules
107
+ if mode == "default":
108
+ # Set root logger to WARNING
109
+ logging.getLogger().setLevel(logging.WARNING)
110
+ # Allow INFO for pixie modules
111
+ logging.getLogger("pixie").setLevel(logging.INFO)
112
+ # Suppress uvicorn access logs in default mode
113
+ logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
114
+
115
+
116
+ def create_app() -> FastAPI:
117
+ """Create and configure the FastAPI application.
118
+
119
+ Returns:
120
+ Configured FastAPI application instance with GraphQL router.
121
+ """
122
+ # Setup logging first (use global logging mode)
123
+ setup_logging(_logging_mode)
124
+
125
+ # Discover and load applications on every app creation (including reloads)
126
+ discover_and_load_prompts()
127
+
128
+ dotenv.load_dotenv(os.getcwd() + "/.env")
129
+ storage_directory = os.getenv("PIXIE_PROMPT_STORAGE_DIR", ".pixie/prompts")
130
+ initialize_prompt_storage(storage_directory)
131
+
132
+ app = FastAPI(
133
+ title="Pixie SDK Server",
134
+ description="Server for running AI applications and agents",
135
+ version="0.1.0",
136
+ )
137
+
138
+ # Add CORS middleware
139
+ app.add_middleware(
140
+ CORSMiddleware,
141
+ allow_origins=["*"], # Allows all origins
142
+ allow_credentials=True,
143
+ allow_methods=["*"], # Allows all methods
144
+ allow_headers=["*"], # Allows all headers
145
+ )
146
+
147
+ # Add GraphQL router with GraphiQL enabled
148
+ graphql_app = GraphQLRouter(
149
+ schema,
150
+ graphiql=True,
151
+ )
152
+
153
+ app.include_router(graphql_app, prefix="/graphql")
154
+
155
+ @app.get("/")
156
+ async def root():
157
+ return {
158
+ "message": "Pixie SDK Server",
159
+ "graphiql": "/graphql",
160
+ "version": "0.1.0",
161
+ }
162
+
163
+ return app
164
+
165
+
166
+ def start_server(
167
+ host: str = "0.0.0.0",
168
+ port: int = 8000,
169
+ reload: bool = False,
170
+ log_mode: str = "default",
171
+ ) -> None:
172
+ """Start the SDK server.
173
+
174
+ Args:
175
+ host: Host to bind to
176
+ port: Port to bind to
177
+ reload: Enable auto-reload for development
178
+ log_mode: Logging mode - "default", "verbose", or "debug"
179
+ storage_directory: Directory to store prompt definitions
180
+ """
181
+ global _logging_mode
182
+ _logging_mode = log_mode
183
+
184
+ # Setup logging (will be called again in create_app for reload scenarios)
185
+ setup_logging(log_mode)
186
+
187
+ # Determine server URL
188
+ server_url = f"http://{host}:{port}"
189
+ if host == "0.0.0.0":
190
+ server_url = f"http://127.0.0.1:{port}"
191
+
192
+ # Log server start info
193
+ logger.info("Starting Pixie SDK Server")
194
+ logger.info("Server: %s", server_url)
195
+ logger.info("GraphQL: %s/graphql", server_url)
196
+
197
+ # Display gopixie.ai web link
198
+ encoded_url = quote(f"{server_url}/graphql", safe="")
199
+ pixie_web_url = f"https://gopixie.ai?url={encoded_url}"
200
+ logger.info("")
201
+ logger.info("=" * 60)
202
+ logger.info("")
203
+ logger.info("🎨 Open Pixie Web UI:")
204
+ logger.info("")
205
+ logger.info(" %s", pixie_web_url)
206
+ logger.info("")
207
+ logger.info("=" * 60)
208
+ logger.info("")
209
+
210
+ uvicorn.run(
211
+ "pixie.prompts.server:create_app",
212
+ host=host,
213
+ port=port,
214
+ loop="asyncio",
215
+ reload=reload,
216
+ factory=True,
217
+ log_config=None,
218
+ )
219
+
220
+
221
+ def main():
222
+ """Start the Pixie server.
223
+
224
+ Loads environment variables and starts the server with auto-reload enabled.
225
+ Supports --verbose and --debug flags for enhanced logging.
226
+ """
227
+ parser = argparse.ArgumentParser(description="Pixie Prompts development server")
228
+ parser.add_argument(
229
+ "--verbose",
230
+ "-v",
231
+ action="store_true",
232
+ help="Enable verbose logging (INFO+ for all modules)",
233
+ )
234
+ parser.add_argument(
235
+ "--debug",
236
+ "-d",
237
+ action="store_true",
238
+ help="Enable debug logging (DEBUG+ for all modules)",
239
+ )
240
+ parser.add_argument(
241
+ "--port",
242
+ "-p",
243
+ type=int,
244
+ default=None,
245
+ help="Port to run the server on (overrides PIXIE_SDK_PORT env var)",
246
+ )
247
+ args = parser.parse_args()
248
+
249
+ # Determine logging mode
250
+ log_mode = "default"
251
+ if args.debug:
252
+ log_mode = "debug"
253
+ elif args.verbose:
254
+ log_mode = "verbose"
255
+
256
+ dotenv.load_dotenv(os.getcwd() + "/.env")
257
+ port = args.port or int(os.getenv("PIXIE_SDK_PORT", "8000"))
258
+
259
+ start_server(port=port, reload=True, log_mode=log_mode)
260
+
261
+
262
+ if __name__ == "__main__":
263
+ main()
@@ -0,0 +1,228 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from types import NoneType
5
+ from typing import Any, Dict, Protocol, Self, TypedDict
6
+
7
+ from jsonsubschema import isSubschema
8
+
9
+ from .prompt import (
10
+ BasePrompt,
11
+ BaseUntypedPrompt,
12
+ Prompt,
13
+ TPromptVar,
14
+ variables_definition_to_schema,
15
+ )
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class PromptStorage(Protocol):
22
+
23
+ def load(self) -> None: ...
24
+
25
+ def exists(self, prompt_id: str) -> bool: ...
26
+
27
+ def save(self, prompt: BaseUntypedPrompt) -> None: ...
28
+
29
+ def get(self, prompt_id: str) -> BaseUntypedPrompt: ...
30
+
31
+
32
+ class _BasePromptJson(TypedDict):
33
+ versions: Dict[str, str]
34
+ defaultVersionId: str
35
+ variablesSchema: Dict[str, Any]
36
+
37
+
38
+ class _FilePromptStorage(PromptStorage):
39
+
40
+ def __init__(self, directory: str) -> None:
41
+ self._directory = directory
42
+ self._prompts: Dict[str, BaseUntypedPrompt] = {}
43
+ self.load()
44
+
45
+ def load(self) -> None:
46
+ """prompts that are in storage"""
47
+ if not os.path.exists(self._directory):
48
+ os.makedirs(self._directory)
49
+ for filename in os.listdir(self._directory):
50
+ if filename.endswith(".json"):
51
+ prompt_id = filename[:-5] # remove .json
52
+ filepath = os.path.join(self._directory, filename)
53
+ with open(filepath, "r") as f:
54
+ data: _BasePromptJson = json.load(f)
55
+ versions = data["versions"]
56
+ default_version_id = data["defaultVersionId"]
57
+ variables_schema = data["variablesSchema"]
58
+ prompt = BaseUntypedPrompt(
59
+ id=prompt_id,
60
+ versions=versions,
61
+ default_version_id=default_version_id,
62
+ variables_schema=variables_schema,
63
+ )
64
+ self._prompts[prompt_id] = prompt
65
+
66
+ def exists(self, prompt_id: str) -> bool:
67
+ return prompt_id in self._prompts
68
+
69
+ def save(self, prompt: BaseUntypedPrompt) -> bool:
70
+ prompt_id = prompt.id
71
+ original = self._prompts.get(prompt_id)
72
+ new_schema = prompt.get_variables_schema()
73
+ if original:
74
+ original_schema = original.get_variables_schema()
75
+ if not isSubschema(original_schema, new_schema):
76
+ raise TypeError(
77
+ "Original schema must be a subschema of the new schema."
78
+ )
79
+ data: _BasePromptJson = {
80
+ "versions": prompt.get_versions(),
81
+ "defaultVersionId": prompt.get_default_version_id(),
82
+ "variablesSchema": prompt.get_variables_schema(),
83
+ }
84
+ filepath = os.path.join(self._directory, f"{prompt_id}.json")
85
+ with open(filepath, "w") as f:
86
+ json.dump(data, f, indent=2)
87
+ try:
88
+ BasePrompt.update_prompt_registry(prompt)
89
+ except KeyError:
90
+ # Prompt not in type prompt registry yet, meaning there's no usage in code
91
+ # thus this untyped prompt would just be stored but not used in code
92
+ pass
93
+ self._prompts[prompt_id] = prompt
94
+ return original is None
95
+
96
+ def get(self, prompt_id: str) -> BaseUntypedPrompt:
97
+ return self._prompts[prompt_id]
98
+
99
+
100
+ _storage_instance: PromptStorage | None = None
101
+
102
+
103
+ # TODO allow other storage types later
104
+ def initialize_prompt_storage(directory: str) -> None:
105
+ global _storage_instance
106
+ if _storage_instance is not None:
107
+ raise RuntimeError("Prompt storage has already been initialized.")
108
+ _storage_instance = _FilePromptStorage(directory)
109
+ logger.info(f"Initialized prompt storage at directory: {directory}")
110
+
111
+
112
+ class StorageBackedPrompt(Prompt[TPromptVar]):
113
+
114
+ def __init__(
115
+ self,
116
+ id: str,
117
+ *,
118
+ variables_definition: type[TPromptVar] = NoneType,
119
+ ) -> None:
120
+ self._id = id
121
+ self._variables_definition = variables_definition
122
+ self._prompt: BasePrompt[TPromptVar] | None = None
123
+
124
+ @property
125
+ def id(self) -> str:
126
+ return self._id
127
+
128
+ @property
129
+ def variables_definition(self) -> type[TPromptVar]:
130
+ return self._variables_definition
131
+
132
+ def get_variables_schema(self) -> dict[str, Any]:
133
+ return variables_definition_to_schema(self._variables_definition)
134
+
135
+ def _get_prompt(self) -> BasePrompt[TPromptVar]:
136
+ if _storage_instance is None:
137
+ raise RuntimeError("Prompt storage has not been initialized.")
138
+ if self._prompt is None:
139
+ untyped_prompt = _storage_instance.get(self.id)
140
+ self._prompt = BasePrompt.from_untyped(
141
+ untyped_prompt,
142
+ variables_definition=self.variables_definition,
143
+ )
144
+ schema_from_storage = untyped_prompt.get_variables_schema()
145
+ schema_from_definition = self.get_variables_schema()
146
+ if not isSubschema(schema_from_definition, schema_from_storage):
147
+ raise TypeError(
148
+ "Schema from definition is not a subschema of the schema from storage."
149
+ )
150
+ return self._prompt
151
+
152
+ def actualize(self) -> Self:
153
+ self._get_prompt()
154
+ return self
155
+
156
+ def exists_in_storage(self) -> bool:
157
+ if _storage_instance is None:
158
+ raise RuntimeError("Prompt storage has not been initialized.")
159
+ try:
160
+ self.actualize()
161
+ return True
162
+ except KeyError:
163
+ return False
164
+
165
+ def get_versions(self) -> dict[str, str]:
166
+ prompt = self._get_prompt()
167
+ return prompt.get_versions()
168
+
169
+ def get_version_count(self) -> int:
170
+ try:
171
+ prompt = self._get_prompt()
172
+ versions_dict = prompt.get_versions()
173
+ return len(versions_dict)
174
+ except KeyError:
175
+ return 0
176
+
177
+ def get_default_version_id(self) -> str:
178
+ prompt = self._get_prompt()
179
+ return prompt.get_default_version_id()
180
+
181
+ def compile(
182
+ self,
183
+ variables: TPromptVar = None,
184
+ *,
185
+ version_id: str | None = None,
186
+ ) -> str:
187
+ prompt = self._get_prompt()
188
+ return prompt.compile(variables=variables, version_id=version_id)
189
+
190
+ def append_version(
191
+ self,
192
+ version_id: str,
193
+ content: str,
194
+ set_as_default: bool = False,
195
+ ) -> BasePrompt[TPromptVar]:
196
+ if _storage_instance is None:
197
+ raise RuntimeError("Prompt storage has not been initialized.")
198
+ if self.exists_in_storage():
199
+ prompt = self._get_prompt()
200
+ prompt.append_version(
201
+ version_id=version_id,
202
+ content=content,
203
+ set_as_default=set_as_default,
204
+ )
205
+ _storage_instance.save(prompt)
206
+ return prompt
207
+ else:
208
+ # it should be safe to assume there's no actualized prompt for this id
209
+ # thus it should be same to create a new instance of BasePrompt
210
+ new_prompt = BasePrompt(
211
+ id=self.id,
212
+ versions={version_id: content},
213
+ variables_definition=self.variables_definition,
214
+ default_version_id=version_id,
215
+ )
216
+ _storage_instance.save(new_prompt)
217
+ return new_prompt
218
+
219
+ def update_default_version_id(
220
+ self,
221
+ version_id: str,
222
+ ) -> BasePrompt[TPromptVar]:
223
+ if _storage_instance is None:
224
+ raise RuntimeError("Prompt storage has not been initialized.")
225
+ prompt = self._get_prompt()
226
+ prompt.update_default_version_id(version_id)
227
+ _storage_instance.save(prompt)
228
+ return prompt
File without changes