JasonGross commited on
Commit
5cf54dc
·
1 Parent(s): 99b4bd2
Files changed (7) hide show
  1. app/config.yaml +8 -0
  2. app/main.py +82 -15
  3. app/static/script.js +65 -6
  4. app/static/style.css +8 -0
  5. pyproject.toml +14 -5
  6. requirements.txt +3 -0
  7. uv.lock +117 -0
app/config.yaml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ llm:
2
+ model_name: "claude-3-5-sonnet-20240620"
3
+ system_prompt: |
4
+ You are an AI assistant that writes essays in the style of Paul Graham.
5
+ Focus on insights about startups, technology, programming, and contrarian thinking.
6
+ Be concise and clear.
7
+ max_tokens: 3500
8
+ prompt_template: "Write a Paul Graham essay about {short_description}"
app/main.py CHANGED
@@ -1,7 +1,7 @@
1
  import os
2
  import asyncio
3
  import logging
4
- from typing import Optional # Import Optional for type hinting
5
  from fastapi import FastAPI, Request, HTTPException, Form
6
  from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
7
  from fastapi.staticfiles import StaticFiles
@@ -12,6 +12,7 @@ from dotenv import load_dotenv
12
  import json
13
  from pydantic import BaseModel
14
  from datetime import datetime # Add datetime import
 
15
 
16
  # --- Import Anthropic ---
17
  from anthropic import AsyncAnthropic, APIError
@@ -63,6 +64,32 @@ except Exception as e:
63
  # --- Initialize FastAPI App ---
64
  app = FastAPI()
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  # --- Mount Static Files & Templates ---
67
  script_dir = os.path.dirname(__file__)
68
  static_dir = os.path.join(script_dir, "static")
@@ -383,30 +410,69 @@ async def ask_paul_graham(request: Request, prompt: str = Form(...)):
383
  )
384
  # ---------------------------
385
 
386
- # --- Determine Model Parameters --- #
387
- # (Define these based on your logic - fixed for now)
388
- model_name = "claude-3-5-sonnet-20240620"
389
- system_prompt = "You are an AI assistant that writes essays in the style of Paul Graham. Focus on insights about startups, technology, programming, and contrarian thinking. Be concise and clear."
390
- max_tokens = 3500 # GPT 2 token statistics on PG essays as of 2025-04-14
391
- # Mean: 3284.29, Median: 2052, Mode: 3292
392
- # Min: 104, Max: 17718, SD: 3086.28
393
- prompt_text = f"Write a Paul Graham essay about {short_description}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  messages = [
395
  {
396
  "role": "user",
397
- "content": prompt_text, # Use full description for the LLM
398
  }
399
  ]
400
- # --------------------------------- #
401
 
402
  try:
403
  # --- Get or Create Model Params ID --- #
 
404
  params_id = await get_or_create_model_params(
405
- model_name, system_prompt, max_tokens
406
  )
407
  # ------------------------------------- #
408
 
409
- # --- Find existing prompt based on short_description --- #
410
  existing_prompt_result = (
411
  supabase.table("prompts")
412
  .select("prompt_id, created_at")
@@ -485,10 +551,11 @@ async def ask_paul_graham(request: Request, prompt: str = Form(...)):
485
 
486
  # Stream the cached/latest response
487
  async def stream_latest_cached():
488
- chunk_size = 20
 
489
  for i in range(0, len(latest_response_text), chunk_size):
490
  chunk = latest_response_text[i : i + chunk_size]
491
- yield f"data: {json.dumps({'text': chunk})}\\n\\n"
492
  await asyncio.sleep(0.01)
493
  yield f"data: {json.dumps({'end': True})}\n\n"
494
 
 
1
  import os
2
  import asyncio
3
  import logging
4
+ from typing import Optional, Dict, Any # Import Dict and Any
5
  from fastapi import FastAPI, Request, HTTPException, Form
6
  from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
7
  from fastapi.staticfiles import StaticFiles
 
12
  import json
13
  from pydantic import BaseModel
14
  from datetime import datetime # Add datetime import
15
+ import yaml # Import YAML
16
 
17
  # --- Import Anthropic ---
18
  from anthropic import AsyncAnthropic, APIError
 
64
  # --- Initialize FastAPI App ---
65
  app = FastAPI()
66
 
67
+ # --- Load Configuration ---
68
+ config: Dict[str, Any] = {} # Global config dictionary
69
+ script_dir = os.path.dirname(__file__)
70
+ config_path = os.path.join(script_dir, "config.yaml")
71
+ try:
72
+ with open(config_path, "r") as f:
73
+ config = yaml.safe_load(f)
74
+ if not config or "llm" not in config:
75
+ raise ValueError("Invalid config structure: 'llm' section missing.")
76
+ logger.info(f"Successfully loaded configuration from {config_path}")
77
+ except FileNotFoundError:
78
+ logger.error(
79
+ f"Configuration file not found at {config_path}. LLM functionality may be limited."
80
+ )
81
+ # Optionally set default values or exit
82
+ config = {"llm": {}} # Ensure config['llm'] exists
83
+ except yaml.YAMLError as e:
84
+ logger.error(f"Error parsing configuration file {config_path}: {e}")
85
+ config = {"llm": {}} # Ensure config['llm'] exists
86
+ except Exception as e:
87
+ logger.exception(
88
+ f"An unexpected error occurred while loading configuration from {config_path}",
89
+ exc_info=e,
90
+ )
91
+ config = {"llm": {}} # Ensure config['llm'] exists
92
+
93
  # --- Mount Static Files & Templates ---
94
  script_dir = os.path.dirname(__file__)
95
  static_dir = os.path.join(script_dir, "static")
 
410
  )
411
  # ---------------------------
412
 
413
+ # --- Determine Model Parameters from Config --- #
414
+ llm_config = config.get("llm", {})
415
+ model_name = llm_config.get("model_name")
416
+ system_prompt = llm_config.get("system_prompt")
417
+ max_tokens_str = llm_config.get("max_tokens", "1000") # Default if missing
418
+ prompt_template = llm_config.get(
419
+ "prompt_template", "Write an essay about {short_description}"
420
+ ) # Default template
421
+
422
+ # Validate required parameters
423
+ if not model_name:
424
+ logger.error("LLM model_name is missing in the configuration.")
425
+ raise HTTPException(
426
+ status_code=500, detail="LLM configuration error: model_name missing."
427
+ )
428
+ if not system_prompt:
429
+ logger.warning(
430
+ "LLM system_prompt is missing in the configuration. Using empty system prompt."
431
+ )
432
+ system_prompt = ""
433
+
434
+ # Safely convert max_tokens to int
435
+ try:
436
+ max_tokens = int(max_tokens_str)
437
+ except (ValueError, TypeError):
438
+ logger.warning(
439
+ f"Invalid max_tokens value '{max_tokens_str}' in config. Using default 1000."
440
+ )
441
+ max_tokens = 1000
442
+
443
+ # Construct prompt_text using the template
444
+ try:
445
+ prompt_text = prompt_template.format(short_description=short_description)
446
+ except KeyError:
447
+ logger.warning(
448
+ f"Prompt template '{prompt_template}' is missing '{{short_description}}'. Using default template."
449
+ )
450
+ prompt_text = f"Write an essay about {short_description}"
451
+ except Exception as e:
452
+ logger.error(f"Error formatting prompt template: {e}. Using basic prompt.")
453
+ prompt_text = f"Write an essay about {short_description}"
454
+
455
+ logger.info(f"Using LLM Parameters: model='{model_name}', max_tokens={max_tokens}")
456
+ # logger.debug(f"System Prompt: '{system_prompt[:100]}...'") # Log truncated system prompt
457
+ # logger.debug(f"Generated Prompt Text: '{prompt_text[:100]}...'") # Log truncated final prompt
458
+
459
  messages = [
460
  {
461
  "role": "user",
462
+ "content": prompt_text, # Use the formatted prompt text
463
  }
464
  ]
465
+ # ------------------------------------------ #
466
 
467
  try:
468
  # --- Get or Create Model Params ID --- #
469
+ # Pass the actual values retrieved from config
470
  params_id = await get_or_create_model_params(
471
+ model_name=model_name, system_prompt=system_prompt, max_tokens=max_tokens
472
  )
473
  # ------------------------------------- #
474
 
475
+ # --- Find existing prompt based on prompt_text (generated from template) --- #
476
  existing_prompt_result = (
477
  supabase.table("prompts")
478
  .select("prompt_id, created_at")
 
551
 
552
  # Stream the cached/latest response
553
  async def stream_latest_cached():
554
+ # chunk_size = 20
555
+ chunk_size = len(latest_response_text)
556
  for i in range(0, len(latest_response_text), chunk_size):
557
  chunk = latest_response_text[i : i + chunk_size]
558
+ yield f"data: {json.dumps({'text': chunk})}\n\n"
559
  await asyncio.sleep(0.01)
560
  yield f"data: {json.dumps({'end': True})}\n\n"
561
 
app/static/script.js CHANGED
@@ -13,6 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
13
 
14
  let currentSort = { field: 'time', order: 'desc' }; // Default sort
15
  let eventSource = null; // To hold the EventSource connection
 
16
 
17
  // --- Character Counter ---
18
  promptInput.addEventListener('input', () => {
@@ -22,6 +23,7 @@ document.addEventListener('DOMContentLoaded', () => {
22
 
23
  // --- Fetch and Render Essays ---
24
  async function fetchEssays(sortBy = 'time', order = 'desc') {
 
25
  essaysList.innerHTML = '<li class="text-gray-400">Loading essays...</li>'; // Show loading state
26
  try {
27
  const response = await fetch(`/essays?sort_by=${sortBy}&order=${order}`);
@@ -29,20 +31,43 @@ document.addEventListener('DOMContentLoaded', () => {
29
  throw new Error(`HTTP error! status: ${response.status}`);
30
  }
31
  const essays = await response.json();
 
32
 
33
  essaysList.innerHTML = ''; // Clear list
34
 
 
 
 
 
 
35
  if (essays.length === 0) {
36
  essaysList.innerHTML = '<li class="text-gray-500">No essays found yet.</li>';
37
  } else {
38
- essays.forEach(essay => {
 
 
39
  const li = document.createElement('li');
40
- // Simple display: just the prompt text
41
- li.textContent = essay.prompt;
42
- // Optional: Add view count or date
43
- // li.textContent += ` (Views: ${essay.view_count}, Created: ${new Date(essay.created_at).toLocaleDateString()})`;
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  essaysList.appendChild(li);
 
45
  });
 
46
  }
47
  } catch (error) {
48
  console.error('Error fetching essays:', error);
@@ -69,6 +94,7 @@ document.addEventListener('DOMContentLoaded', () => {
69
  loadingIndicator.classList.remove('hidden');
70
  responseOutput.innerHTML = ''; // Clear previous output
71
  errorMessage.classList.add('hidden'); // Hide previous errors
 
72
 
73
  try {
74
  // Use EventSource for Server-Sent Events
@@ -114,7 +140,40 @@ document.addEventListener('DOMContentLoaded', () => {
114
  try {
115
  const data = JSON.parse(line.substring(5).trim());
116
  if (data.text) {
117
- responseOutput.innerHTML += data.text; // Append text chunk
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  } else if (data.error) {
119
  console.error("SSE Error:", data.error);
120
  errorMessage.textContent = `Error: ${data.error}`;
 
13
 
14
  let currentSort = { field: 'time', order: 'desc' }; // Default sort
15
  let eventSource = null; // To hold the EventSource connection
16
+ let isFirstChunk = false; // Flag to track the first text chunk
17
 
18
  // --- Character Counter ---
19
  promptInput.addEventListener('input', () => {
 
23
 
24
  // --- Fetch and Render Essays ---
25
  async function fetchEssays(sortBy = 'time', order = 'desc') {
26
+ console.log(`Fetching essays: sort=${sortBy}, order=${order}`); // Log fetch start
27
  essaysList.innerHTML = '<li class="text-gray-400">Loading essays...</li>'; // Show loading state
28
  try {
29
  const response = await fetch(`/essays?sort_by=${sortBy}&order=${order}`);
 
31
  throw new Error(`HTTP error! status: ${response.status}`);
32
  }
33
  const essays = await response.json();
34
+ console.log("Fetched essays data:", essays); // Log the raw fetched data
35
 
36
  essaysList.innerHTML = ''; // Clear list
37
 
38
+ if (!Array.isArray(essays)) {
39
+ console.error("Invalid data received from /essays endpoint. Expected an array.", essays);
40
+ throw new Error("Received invalid data from server.");
41
+ }
42
+
43
  if (essays.length === 0) {
44
  essaysList.innerHTML = '<li class="text-gray-500">No essays found yet.</li>';
45
  } else {
46
+ console.log("Rendering essays..."); // Log before starting loop
47
+ essays.forEach((essay, index) => {
48
+ console.log(`Rendering essay ${index + 1}:`, essay); // Log each essay object
49
  const li = document.createElement('li');
50
+ // Use essay.prompt, provide fallback if null/undefined
51
+ const promptText = essay.prompt ? essay.prompt : '[No prompt text]';
52
+ li.textContent = promptText;
53
+ // Optional: Add view count or date (add checks for null values)
54
+ // const createdAt = essay.created_at ? new Date(essay.created_at).toLocaleDateString() : 'N/A';
55
+ // const viewCount = essay.view_count !== undefined ? essay.view_count : 'N/A';
56
+ // li.textContent += ` (Views: ${viewCount}, Created: ${createdAt})`;
57
+
58
+ // Make the list item clickable to load the prompt
59
+ li.classList.add('cursor-pointer', 'hover:text-orange-700');
60
+ li.addEventListener('click', () => {
61
+ // Set the input value and simulate submission
62
+ promptInput.value = essay.prompt; // Use the original prompt text
63
+ promptInput.dispatchEvent(new Event('input')); // Update char count
64
+ promptForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
65
+ });
66
+
67
  essaysList.appendChild(li);
68
+ console.log(`Appended item ${index + 1} to the list.`); // Confirm appendChild worked
69
  });
70
+ console.log("Finished rendering essays."); // Log after loop completes
71
  }
72
  } catch (error) {
73
  console.error('Error fetching essays:', error);
 
94
  loadingIndicator.classList.remove('hidden');
95
  responseOutput.innerHTML = ''; // Clear previous output
96
  errorMessage.classList.add('hidden'); // Hide previous errors
97
+ isFirstChunk = true; // Reset flag for new request
98
 
99
  try {
100
  // Use EventSource for Server-Sent Events
 
140
  try {
141
  const data = JSON.parse(line.substring(5).trim());
142
  if (data.text) {
143
+ // Escape HTML to prevent injection
144
+ const escapedText = data.text
145
+ .replace(/&/g, '&amp;')
146
+ .replace(/</g, '&lt;')
147
+ .replace(/>/g, '&gt;')
148
+ .replace(/"/g, '&quot;')
149
+ .replace(/'/g, '&#039;');
150
+
151
+ let formattedOutput = "";
152
+ if (isFirstChunk) {
153
+ // Find the index of the first occurrence of one or more newlines
154
+ const firstParaBreakIndex = escapedText.search(/\n+/);
155
+
156
+ if (firstParaBreakIndex !== -1) {
157
+ // Extract title (before the first paragraph break)
158
+ const title = escapedText.substring(0, firstParaBreakIndex);
159
+ // Extract the rest of the text (including the newlines that caused the break)
160
+ const restOfText = escapedText.substring(firstParaBreakIndex);
161
+
162
+ // Format: Title span + replaced newlines in the rest
163
+ formattedOutput = `<span class="essay-title">${title}</span>${restOfText.replace(/\n+/g, '<br><br>')}`;
164
+ isFirstChunk = false; // Only process the first chunk this way
165
+ } else {
166
+ // If the first chunk has no newline sequence, treat the whole chunk as the title
167
+ const title = escapedText; // No need to replace newlines here if none exist
168
+ formattedOutput = `<span class="essay-title">${title}</span>`;
169
+ // Subsequent chunks will add <br> if needed
170
+ }
171
+ } else {
172
+ // For subsequent chunks, just escape and replace newlines
173
+ formattedOutput = escapedText.replace(/\n+/g, '<br><br>');
174
+ }
175
+ responseOutput.innerHTML += formattedOutput; // Append safely formatted text chunk
176
+
177
  } else if (data.error) {
178
  console.error("SSE Error:", data.error);
179
  errorMessage.textContent = `Error: ${data.error}`;
app/static/style.css CHANGED
@@ -2,6 +2,14 @@
2
 
3
  /* Add any custom styles here if needed, complementing Tailwind */
4
 
 
 
 
 
 
 
 
 
5
  /* Example: Style for the active sort button */
6
  .sort-button.active {
7
  font-weight: bold;
 
2
 
3
  /* Add any custom styles here if needed, complementing Tailwind */
4
 
5
+ /* Style for the essay title */
6
+ .essay-title {
7
+ color: #841814;
8
+ font-variant: small-caps;
9
+ font-weight: bold;
10
+ /* Optional: make it bolder */
11
+ }
12
+
13
  /* Example: Style for the active sort button */
14
  .sort-button.active {
15
  font-weight: bold;
pyproject.toml CHANGED
@@ -12,7 +12,7 @@ version = "0.1.0"
12
  description = "A web app to generate Paul Graham style essays using an LLM, with response storage."
13
  readme = "README.md" # Assumes you have or will create a README file
14
  requires-python = ">=3.9" # Based on the Python version used in Dockerfile/dev environment
15
- license = { text = "MIT" } # Or choose another license, e.g., "Apache-2.0"
16
  authors = [
17
  { name = "Jason Gross", email = "jasongross9@gmail.com" }, # Optional: Add your details
18
  ]
@@ -30,6 +30,8 @@ dependencies = [
30
  "anthropic>=0.49.0",
31
  "httpx>=0.28.1",
32
  "supabase>=2.15.0",
 
 
33
  ]
34
 
35
  # --- Optional: Project URLs ---
@@ -43,10 +45,17 @@ dependencies = [
43
  # ask-pg = "app.main:app" # Example, depends on how you structure/run
44
 
45
  # --- Optional: Development Dependencies ---
46
- # Example: Add linters, formatters, testing tools here
47
- # [project.optional-dependencies]
48
- # dev = [
 
49
  # "pytest",
50
  # "ruff",
51
  # "mypy",
52
- # ]
 
 
 
 
 
 
 
12
  description = "A web app to generate Paul Graham style essays using an LLM, with response storage."
13
  readme = "README.md" # Assumes you have or will create a README file
14
  requires-python = ">=3.9" # Based on the Python version used in Dockerfile/dev environment
15
+ license = "MIT" # Use SPDX identifier string
16
  authors = [
17
  { name = "Jason Gross", email = "jasongross9@gmail.com" }, # Optional: Add your details
18
  ]
 
30
  "anthropic>=0.49.0",
31
  "httpx>=0.28.1",
32
  "supabase>=2.15.0",
33
+ "pyyaml>=6.0.2",
34
+ "requests>=2.32.3",
35
  ]
36
 
37
  # --- Optional: Project URLs ---
 
45
  # ask-pg = "app.main:app" # Example, depends on how you structure/run
46
 
47
  # --- Optional: Development Dependencies ---
48
+ # Add linters, formatters, testing tools here
49
+ [project.optional-dependencies]
50
+ dev = [
51
+ "types-PyYAML", # Stubs for PyYAML
52
  # "pytest",
53
  # "ruff",
54
  # "mypy",
55
+ ]
56
+
57
+ # --- Setuptools Configuration ---
58
+ [tool.setuptools.packages.find]
59
+ where = ["app"] # Look for packages in the app directory
60
+ # exclude = [] # Optional: exclude specific modules/packages
61
+ # include = ["app*"] # Optional: Be more specific if needed
requirements.txt CHANGED
@@ -6,3 +6,6 @@ httpx>=0.24.0
6
  python-dotenv>=1.0.0
7
  jinja2>=3.0.0 # For templating
8
  anthropic>=0.20.0 # Added Anthropic client library
 
 
 
 
6
  python-dotenv>=1.0.0
7
  jinja2>=3.0.0 # For templating
8
  anthropic>=0.20.0 # Added Anthropic client library
9
+ psycopg2-binary
10
+ requests
11
+ PyYAML # Added for config file parsing
uv.lock CHANGED
@@ -173,10 +173,17 @@ dependencies = [
173
  { name = "jinja2" },
174
  { name = "python-dotenv" },
175
  { name = "python-multipart" },
 
 
176
  { name = "supabase" },
177
  { name = "uvicorn", extra = ["standard"] },
178
  ]
179
 
 
 
 
 
 
180
  [package.metadata]
181
  requires-dist = [
182
  { name = "anthropic", specifier = ">=0.49.0" },
@@ -185,7 +192,10 @@ requires-dist = [
185
  { name = "jinja2", specifier = ">=3.0.0" },
186
  { name = "python-dotenv", specifier = ">=1.0.0" },
187
  { name = "python-multipart", specifier = ">=0.0.20" },
 
 
188
  { name = "supabase", specifier = ">=2.15.0" },
 
189
  { name = "uvicorn", extras = ["standard"], specifier = ">=0.20.0" },
190
  ]
191
 
@@ -216,6 +226,80 @@ wheels = [
216
  { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
217
  ]
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  [[package]]
220
  name = "click"
221
  version = "8.1.8"
@@ -1177,6 +1261,21 @@ wheels = [
1177
  { url = "https://files.pythonhosted.org/packages/1d/b7/1b7651f353e14543c60cdfe40e3ea4dea412cfb2e93ab6384e72be813f05/realtime-2.4.2-py3-none-any.whl", hash = "sha256:0cc1b4a097acf9c0bd3a2f1998170de47744574c606617285113ddb3021e54ca", size = 22025 },
1178
  ]
1179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1180
  [[package]]
1181
  name = "six"
1182
  version = "1.17.0"
@@ -1299,6 +1398,15 @@ wheels = [
1299
  { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
1300
  ]
1301
 
 
 
 
 
 
 
 
 
 
1302
  [[package]]
1303
  name = "typing-extensions"
1304
  version = "4.13.2"
@@ -1320,6 +1428,15 @@ wheels = [
1320
  { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
1321
  ]
1322
 
 
 
 
 
 
 
 
 
 
1323
  [[package]]
1324
  name = "uvicorn"
1325
  version = "0.34.1"
 
173
  { name = "jinja2" },
174
  { name = "python-dotenv" },
175
  { name = "python-multipart" },
176
+ { name = "pyyaml" },
177
+ { name = "requests" },
178
  { name = "supabase" },
179
  { name = "uvicorn", extra = ["standard"] },
180
  ]
181
 
182
+ [package.optional-dependencies]
183
+ dev = [
184
+ { name = "types-pyyaml" },
185
+ ]
186
+
187
  [package.metadata]
188
  requires-dist = [
189
  { name = "anthropic", specifier = ">=0.49.0" },
 
192
  { name = "jinja2", specifier = ">=3.0.0" },
193
  { name = "python-dotenv", specifier = ">=1.0.0" },
194
  { name = "python-multipart", specifier = ">=0.0.20" },
195
+ { name = "pyyaml", specifier = ">=6.0.2" },
196
+ { name = "requests", specifier = ">=2.32.3" },
197
  { name = "supabase", specifier = ">=2.15.0" },
198
+ { name = "types-pyyaml", marker = "extra == 'dev'" },
199
  { name = "uvicorn", extras = ["standard"], specifier = ">=0.20.0" },
200
  ]
201
 
 
226
  { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
227
  ]
228
 
229
+ [[package]]
230
+ name = "charset-normalizer"
231
+ version = "3.4.1"
232
+ source = { registry = "https://pypi.org/simple" }
233
+ sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
234
+ wheels = [
235
+ { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 },
236
+ { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 },
237
+ { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 },
238
+ { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 },
239
+ { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 },
240
+ { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 },
241
+ { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 },
242
+ { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 },
243
+ { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 },
244
+ { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 },
245
+ { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 },
246
+ { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 },
247
+ { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 },
248
+ { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
249
+ { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
250
+ { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
251
+ { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
252
+ { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
253
+ { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
254
+ { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
255
+ { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
256
+ { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
257
+ { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
258
+ { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
259
+ { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
260
+ { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
261
+ { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
262
+ { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
263
+ { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
264
+ { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
265
+ { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
266
+ { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
267
+ { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
268
+ { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
269
+ { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
270
+ { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
271
+ { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
272
+ { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
273
+ { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
274
+ { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
275
+ { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
276
+ { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
277
+ { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
278
+ { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
279
+ { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
280
+ { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
281
+ { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
282
+ { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
283
+ { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
284
+ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
285
+ { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
286
+ { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
287
+ { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 },
288
+ { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 },
289
+ { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 },
290
+ { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 },
291
+ { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 },
292
+ { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 },
293
+ { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 },
294
+ { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 },
295
+ { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 },
296
+ { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 },
297
+ { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 },
298
+ { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 },
299
+ { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 },
300
+ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
301
+ ]
302
+
303
  [[package]]
304
  name = "click"
305
  version = "8.1.8"
 
1261
  { url = "https://files.pythonhosted.org/packages/1d/b7/1b7651f353e14543c60cdfe40e3ea4dea412cfb2e93ab6384e72be813f05/realtime-2.4.2-py3-none-any.whl", hash = "sha256:0cc1b4a097acf9c0bd3a2f1998170de47744574c606617285113ddb3021e54ca", size = 22025 },
1262
  ]
1263
 
1264
+ [[package]]
1265
+ name = "requests"
1266
+ version = "2.32.3"
1267
+ source = { registry = "https://pypi.org/simple" }
1268
+ dependencies = [
1269
+ { name = "certifi" },
1270
+ { name = "charset-normalizer" },
1271
+ { name = "idna" },
1272
+ { name = "urllib3" },
1273
+ ]
1274
+ sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
1275
+ wheels = [
1276
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
1277
+ ]
1278
+
1279
  [[package]]
1280
  name = "six"
1281
  version = "1.17.0"
 
1398
  { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
1399
  ]
1400
 
1401
+ [[package]]
1402
+ name = "types-pyyaml"
1403
+ version = "6.0.12.20250402"
1404
+ source = { registry = "https://pypi.org/simple" }
1405
+ sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282 }
1406
+ wheels = [
1407
+ { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 },
1408
+ ]
1409
+
1410
  [[package]]
1411
  name = "typing-extensions"
1412
  version = "4.13.2"
 
1428
  { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
1429
  ]
1430
 
1431
+ [[package]]
1432
+ name = "urllib3"
1433
+ version = "2.4.0"
1434
+ source = { registry = "https://pypi.org/simple" }
1435
+ sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
1436
+ wheels = [
1437
+ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
1438
+ ]
1439
+
1440
  [[package]]
1441
  name = "uvicorn"
1442
  version = "0.34.1"