pymite6941 commited on
Commit
89bab00
ยท
1 Parent(s): b3563c0

fix: resolve merge conflicts in main.py, remove bots.py

Browse files
Files changed (2) hide show
  1. backend/bots.py +0 -637
  2. backend/main.py +16 -77
backend/bots.py DELETED
@@ -1,637 +0,0 @@
1
- """
2
- DataFlow AI โ€” single-file backend.
3
- FastAPI SSE server + CrewAI 6-agent pipeline.
4
- """
5
-
6
- # โ”€โ”€ Stdlib โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
7
- import sys
8
- import io
9
- import queue
10
- import threading
11
- import tempfile
12
- import os
13
- import re as _re
14
- import time as _time
15
- import asyncio
16
- import json
17
- import json as _json
18
- from threading import Lock
19
- from typing import Optional, Literal, List as _List
20
-
21
- # โ”€โ”€ Third-party โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
22
- from dotenv import load_dotenv
23
- load_dotenv()
24
-
25
- from pydantic import BaseModel
26
-
27
- from crewai import Agent, Crew, Task, Process, LLM
28
- from crewai_tools import FileReadTool
29
-
30
- import litellm
31
- litellm.cache = None
32
- litellm.drop_params = True
33
-
34
- # โ”€โ”€ Groq cache_breakpoint patch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
35
- # Groq rejects messages that contain a 'cache_breakpoint' property.
36
- _real_completion = litellm.completion
37
-
38
- def _completion_no_cache_breakpoint(*args, **kwargs):
39
- kwargs["caching"] = False
40
- for msg in kwargs.get("messages", []):
41
- if isinstance(msg, dict):
42
- msg.pop("cache_breakpoint", None)
43
- if isinstance(msg.get("content"), list):
44
- for block in msg["content"]:
45
- if isinstance(block, dict):
46
- block.pop("cache_breakpoint", None)
47
- return _real_completion(*args, **kwargs)
48
-
49
- litellm.completion = _completion_no_cache_breakpoint
50
-
51
- # โ”€โ”€ Model rotation pools โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
52
- _FAST_MODELS = [
53
- "groq/llama-3.1-8b-instant",
54
- "groq/gemma2-9b-it",
55
- "groq/llama3-8b-8192",
56
- "openrouter/nvidia/nemotron-nano-9b-v2:free",
57
- "openrouter/minimax/minimax-m2.5:free",
58
- "openrouter/meta-llama/llama-3.1-8b-instruct:free",
59
- "openrouter/mistralai/mistral-7b-instruct:free",
60
- "openrouter/google/gemma-3-12b-it:free",
61
- "openrouter/qwen/qwen3-8b:free",
62
- "openrouter/meta-llama/llama-4-scout:free",
63
- "openrouter/microsoft/phi-3-mini-128k-instruct:free",
64
- ]
65
- _SMART_MODELS = [
66
- "groq/llama-3.3-70b-versatile",
67
- "groq/llama3-70b-8192",
68
- "openrouter/google/gemma-3-27b-it:free",
69
- "openrouter/qwen/qwen3-coder:free",
70
- "openrouter/meta-llama/llama-3.3-70b-instruct:free",
71
- "openrouter/deepseek/deepseek-chat-v3-0324:free",
72
- "openrouter/meta-llama/llama-4-maverick:free",
73
- "openrouter/mistralai/mistral-small-3.1-24b-instruct:free",
74
- "openrouter/google/gemma-3-12b-it:free",
75
- ]
76
-
77
- # โ”€โ”€ Cooldown state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
78
- _GROQ_MODELS_ALL: frozenset = frozenset(
79
- m for m in _FAST_MODELS + _SMART_MODELS if m.startswith("groq/")
80
- )
81
- _cooldown: dict[str, float] = {}
82
- _cooldown_lock = Lock()
83
- _crew_lock = Lock()
84
-
85
-
86
- def _set_cooldown(model: str, seconds: float) -> None:
87
- until = _time.monotonic() + seconds
88
- with _cooldown_lock:
89
- if model.startswith("groq/"):
90
- for m in _GROQ_MODELS_ALL:
91
- _cooldown[m] = max(_cooldown.get(m, 0.0), until)
92
- else:
93
- _cooldown[model] = max(_cooldown.get(model, 0.0), until)
94
-
95
-
96
- def _pick_model(pool: list[str]) -> tuple[str, int]:
97
- now = _time.monotonic()
98
- with _cooldown_lock:
99
- for idx, model in enumerate(pool):
100
- if _cooldown.get(model, 0.0) <= now:
101
- return model, idx
102
- best = min(range(len(pool)), key=lambda i: _cooldown.get(pool[i], 0.0))
103
- return pool[best], best
104
-
105
-
106
- def _wait_until_available() -> None:
107
- now = _time.monotonic()
108
- with _cooldown_lock:
109
- fast_waits = [max(0.0, _cooldown.get(m, 0.0) - now) for m in _FAST_MODELS]
110
- smart_waits = [max(0.0, _cooldown.get(m, 0.0) - now) for m in _SMART_MODELS]
111
- sleep_s = max(min(fast_waits), min(smart_waits))
112
- if sleep_s > 0:
113
- print(f"[WAIT] All providers cooling โ€” resuming in {sleep_s:.0f}s")
114
- _time.sleep(sleep_s)
115
-
116
-
117
- def _parse_retry_after(err_str: str) -> float:
118
- m = _re.search(r"retry_after_seconds['\"\s:]+(\d+(?:\.\d+)?)", err_str)
119
- if m:
120
- return float(m.group(1)) + 5
121
- m = _re.search(r"[Pp]lease try again in (\d+(?:\.\d+)?)s", err_str)
122
- if m:
123
- return float(m.group(1)) + 2
124
- return 35.0
125
-
126
-
127
- def _extract_json(text: str) -> str:
128
- text = _re.sub(r"^```(?:json)?\s*", "", text.strip(), flags=_re.MULTILINE)
129
- text = _re.sub(r"\s*```$", "", text.strip(), flags=_re.MULTILINE)
130
- text = text.strip()
131
- try:
132
- _json.loads(text)
133
- return text
134
- except _json.JSONDecodeError:
135
- pass
136
- start = text.find("{")
137
- if start != -1:
138
- depth = 0
139
- in_string = False
140
- escape_next = False
141
- for i, ch in enumerate(text[start:], start):
142
- if escape_next:
143
- escape_next = False
144
- continue
145
- if ch == "\\" and in_string:
146
- escape_next = True
147
- continue
148
- if ch == '"':
149
- in_string = not in_string
150
- continue
151
- if in_string:
152
- continue
153
- if ch == "{":
154
- depth += 1
155
- elif ch == "}":
156
- depth -= 1
157
- if depth == 0:
158
- candidate = text[start: i + 1]
159
- try:
160
- _json.loads(candidate)
161
- return candidate
162
- except _json.JSONDecodeError:
163
- break
164
- return text
165
-
166
-
167
- _OR_KEY = os.getenv("OPENROUTER_API_KEY", "")
168
- if _OR_KEY:
169
- os.environ.setdefault("OPENAI_API_KEY", _OR_KEY)
170
-
171
-
172
- def _api_key_for(model: str) -> str | None:
173
- if model.startswith("groq/"):
174
- return os.getenv("GROQ_API_KEY")
175
- return os.getenv("OPENROUTER_API_KEY")
176
-
177
-
178
- # โ”€โ”€ Output schemas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
179
-
180
- class DataPoint(BaseModel):
181
- label: str
182
- value: float
183
- category: Optional[str] = None
184
- x_value: Optional[float] = None # scatter: second axis
185
- value2: Optional[float] = None # radar: second series value
186
-
187
-
188
- class CodeBlock(BaseModel):
189
- language: str # python | sql | bash | r | javascript
190
- title: str
191
- code: str # keep under 400 chars
192
-
193
-
194
- class MetricItem(BaseModel):
195
- label: str
196
- value: str # formatted string, e.g. "1,234" or "98.5%"
197
- unit: Optional[str] = None
198
- trend: Optional[str] = None # up | down | neutral
199
- change: Optional[str] = None # e.g. "+12%"
200
- context: Optional[str] = None # e.g. "vs last week"
201
-
202
-
203
- class ComparisonRow(BaseModel):
204
- metric: str # e.g. "Avg Revenue"
205
- value_a: str # formatted value for entity A
206
- value_b: str # formatted value for entity B
207
- winner: Optional[Literal["a", "b", "tie"]] = None
208
-
209
-
210
- class FormattedOutput(BaseModel):
211
- """
212
- OUTPUT TYPE DECISION RULES โ€” pick the FIRST that matches:
213
- 1. "code" โ†’ answer is or includes runnable code/queries/scripts.
214
- Populate code_blocks (1-3 blocks).
215
- 2. "metrics" โ†’ answer is a set of KPIs or key numbers (3-8 items).
216
- Populate metrics list.
217
- 3. "comparison" โ†’ comparing two named entities side-by-side across metrics.
218
- Populate comparison_a_label, comparison_b_label, comparison_rows.
219
- 4. "heatmap" โ†’ data is a matrix (rows ร— columns) of numeric values โ€” e.g.
220
- a correlation matrix, time-of-day ร— day-of-week activity grid,
221
- or category ร— category frequency table.
222
- Populate heatmap_row_labels, heatmap_col_labels, heatmap_values.
223
- Max 10 rows ร— 10 cols.
224
- 5. "table" โ†’ ranked/multi-attribute list best shown as labelled rows+columns.
225
- Populate table_headers and table_rows (max 20 rows).
226
- 6. "chart" โ†’ 2+ numeric values that can be compared visually.
227
- Choose chart_type:
228
- bar โ†’ named categories
229
- line โ†’ sequential time periods
230
- pie โ†’ parts of a whole, 2-6 slices
231
- scatter โ†’ correlation (set x_value + value per DataPoint)
232
- funnel โ†’ sequential stages with drop-off (conversion pipelines)
233
- radar โ†’ multi-attribute profile comparison across dimensions;
234
- set value for series A; set value2 + radar_b_label
235
- if comparing two entities on the same axes.
236
- 7. "report" โ†’ qualitative or narrative findings.
237
-
238
- ALWAYS REQUIRED:
239
- - summary: 2-3 sentences directly answering the original question.
240
- - findings: 3-5 specific factual strings from the data.
241
- - recommendations: 2-3 actionable strings.
242
- - Set unused fields to null.
243
- """
244
- output_type: Literal["chart", "report", "code", "table", "metrics", "comparison", "heatmap"]
245
- # chart
246
- chart_type: Optional[Literal["bar", "line", "pie", "scatter", "funnel", "radar"]] = None
247
- chart_title: Optional[str] = None
248
- x_axis_label: Optional[str] = None
249
- y_axis_label: Optional[str] = None
250
- data_points: Optional[list[DataPoint]] = None
251
- radar_b_label: Optional[str] = None # label for value2 series in radar
252
- # code
253
- code_blocks: Optional[list[CodeBlock]] = None
254
- # table
255
- table_headers: Optional[list[str]] = None
256
- table_rows: Optional[list[list[str]]] = None
257
- # metrics
258
- metrics: Optional[list[MetricItem]] = None
259
- # comparison
260
- comparison_a_label: Optional[str] = None
261
- comparison_b_label: Optional[str] = None
262
- comparison_rows: Optional[list[ComparisonRow]] = None
263
- # heatmap
264
- heatmap_title: Optional[str] = None
265
- heatmap_row_labels: Optional[list[str]] = None
266
- heatmap_col_labels: Optional[list[str]] = None
267
- heatmap_values: Optional[list[list[float]]] = None # [row_idx][col_idx]
268
- # always
269
- summary: str
270
- findings: list[str]
271
- recommendations: list[str]
272
- quality_score: Optional[int] = None
273
- quality_verdict: Optional[str] = None
274
-
275
-
276
- # โ”€โ”€ Agent pipeline โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
277
-
278
- class Bots:
279
- def __init__(self, context: str):
280
- self.context = context
281
- self._ctx = context.replace("{", "{{").replace("}", "}}")
282
- self._fast_idx = 0
283
- self._smart_idx = 0
284
- self.file_read = FileReadTool()
285
-
286
- def _smart_llm(self, temperature: float) -> LLM:
287
- model = _SMART_MODELS[self._smart_idx % len(_SMART_MODELS)]
288
- return LLM(
289
- model=model,
290
- api_key=_api_key_for(model),
291
- max_tokens=1024,
292
- max_retries=0,
293
- timeout=120,
294
- temperature=temperature,
295
- )
296
-
297
- def _fast_llm(self, temperature: float, max_tokens: int = 1024) -> LLM:
298
- model = _FAST_MODELS[self._fast_idx % len(_FAST_MODELS)]
299
- return LLM(
300
- model=model,
301
- api_key=_api_key_for(model),
302
- max_tokens=max_tokens,
303
- max_retries=0,
304
- timeout=120,
305
- temperature=temperature,
306
- )
307
-
308
- def create_agents(self):
309
- self.context_agent = Agent(
310
- role="Analysis Directive Specialist",
311
- goal=(
312
- "Read the user's raw context and rewrite it as a precise, unambiguous "
313
- "analysis directive. Identify the core question, the most relevant columns "
314
- "or metrics, and the exact type of analysis needed."
315
- ),
316
- backstory=(
317
- "You translate vague requests into sharp, actionable instructions. "
318
- "You never perform analysis โ€” you only clarify the directive."
319
- ),
320
- tools=[],
321
- verbose=True,
322
- memory=False,
323
- llm=self._fast_llm(0.2),
324
- allow_delegation=False,
325
- cache=False,
326
- )
327
-
328
- self.data_cleaner = Agent(
329
- role="Data Quality Inspector",
330
- goal=(
331
- "Read every file provided and produce a concise data quality report: "
332
- "column names, row count, missing values, duplicate rows, data type issues. "
333
- "Keep under 200 words."
334
- ),
335
- backstory=(
336
- "You are a meticulous data auditor. You use FileReadTool once per file, "
337
- "then summarise its structure and flag obvious problems."
338
- ),
339
- tools=[self.file_read],
340
- verbose=True,
341
- memory=False,
342
- max_iter=5,
343
- llm=self._fast_llm(0.1, max_tokens=512),
344
- allow_delegation=False,
345
- cache=False,
346
- )
347
-
348
- self.prompt_engineer = Agent(
349
- role="Data Analysis Prompt Engineer",
350
- goal=(
351
- "Construct a precise, step-by-step analysis prompt for the data analyst. "
352
- "Specify exact columns, calculations, patterns to look for, and order of steps."
353
- ),
354
- backstory=(
355
- "You write technical prompts for data analysis pipelines. "
356
- "Vague instructions produce vague results โ€” you are never vague."
357
- ),
358
- tools=[],
359
- verbose=True,
360
- memory=False,
361
- llm=self._fast_llm(0.4),
362
- allow_delegation=False,
363
- cache=False,
364
- )
365
-
366
- self.data_analyst = Agent(
367
- role="Senior Data Analyst",
368
- goal=(
369
- "Follow the analysis prompt exactly. Call FileReadTool ONCE per file path. "
370
- "Reason over the content to answer the prompt. Never speculate beyond the data."
371
- ),
372
- backstory=(
373
- "You are a rigorous analyst. You call FileReadTool exactly once per file โ€” "
374
- "re-reading wastes tokens. You back every finding with evidence."
375
- ),
376
- tools=[self.file_read],
377
- verbose=True,
378
- memory=False,
379
- max_iter=6,
380
- llm=self._smart_llm(0.1),
381
- allow_delegation=False,
382
- cache=False,
383
- )
384
-
385
- self.output_formatter = Agent(
386
- role="Structured Output Specialist",
387
- goal=(
388
- "Convert analyst findings into a strict FormattedOutput JSON object. "
389
- "Choose output_type by priority: code โ†’ metrics โ†’ comparison โ†’ heatmap โ†’ table โ†’ chart โ†’ report."
390
- ),
391
- backstory=(
392
- "You serialise analysis results into one of 7 output modes:\n"
393
- "โ€ข code โ€” runnable scripts, queries, or algorithms\n"
394
- "โ€ข metrics โ€” key numbers / KPIs (3-8 items)\n"
395
- "โ€ข comparison โ€” two named entities compared across metrics\n"
396
- "โ€ข heatmap โ€” matrix of values (correlation, frequency, activity)\n"
397
- "โ€ข table โ€” ranked/multi-attribute list (max 20 rows)\n"
398
- "โ€ข chart โ€” bar, line, pie, scatter, funnel, or radar\n"
399
- "โ€ข report โ€” qualitative or narrative findings\n\n"
400
- "CHART TYPE SELECTION:\n"
401
- " funnel โ†’ sequential conversion stages with drop-off\n"
402
- " radar โ†’ multi-attribute profile (use value2+radar_b_label for dual series)\n"
403
- " scatter โ†’ correlation (x_value + value per point)\n"
404
- " pie โ†’ 2-6 parts of a whole\n"
405
- " line โ†’ time series\n"
406
- " bar โ†’ named categories\n\n"
407
- "COMPARISON: comparison_a_label and comparison_b_label name the two entities. "
408
- "Each comparison_row has metric, value_a, value_b, and winner (a/b/tie).\n\n"
409
- "HEATMAP: heatmap_values is a 2D list [row][col] of floats. Max 10ร—10.\n\n"
410
- "Output ONLY the raw JSON object. No markdown fences, no preamble. "
411
- "summary=2-3 sentences, findings=3-5 strings, recommendations=2-3 strings."
412
- ),
413
- tools=[],
414
- verbose=True,
415
- memory=False,
416
- llm=self._smart_llm(0.1),
417
- allow_delegation=False,
418
- cache=False,
419
- )
420
-
421
- self.qa_critic = Agent(
422
- role="Analysis Quality Critic",
423
- goal=(
424
- "Rate how well the analysis answered the original question. "
425
- 'Output ONLY: {"score": <int 1-10>, "verdict": "<1-2 sentences>"}'
426
- ),
427
- backstory=(
428
- "You review analyses for completeness, specificity, and evidence quality. "
429
- "Score 10 = every aspect answered with data. Score <5 = question not answered. "
430
- "Output ONLY the raw JSON โ€” no markdown, no preamble."
431
- ),
432
- tools=[],
433
- verbose=True,
434
- memory=False,
435
- llm=self._fast_llm(0.2, max_tokens=512),
436
- allow_delegation=False,
437
- cache=False,
438
- )
439
-
440
- def create_tasks(self):
441
- self.interpret_task = Task(
442
- description=(
443
- f"The user wants: {self._ctx}\n\n"
444
- "Rewrite this into a structured analysis directive:\n"
445
- "1. The single core question to answer\n"
446
- "2. Relevant columns/metrics\n"
447
- "3. Analysis type (trend, comparison, anomaly, summary, correlation)\n"
448
- "4. Any constraints (date range, thresholds, focus areas)\n\n"
449
- "Write 3-5 plain sentences addressed directly to a data analyst."
450
- ),
451
- expected_output=(
452
- "3-5 plain sentences. No headers, no bullets. "
453
- "Direct instruction specifying: the question, relevant columns, analysis type, constraints."
454
- ),
455
- agent=self.context_agent,
456
- )
457
-
458
- self.clean_task = Task(
459
- description=(
460
- "Dataset path(s):\n{data}\n\n"
461
- "If {data} is not '(no file)', use FileReadTool to read each path once.\n"
462
- "Report per file: type/size, column names, row count, missing values, "
463
- "obvious issues, 2 sample records.\n"
464
- "If no file: report 'No file provided โ€” analysis uses context only.'\n"
465
- "Keep under 200 words."
466
- ),
467
- expected_output="Concise data quality report under 200 words. Plain prose or bullets. No JSON.",
468
- context=[self.interpret_task],
469
- agent=self.data_cleaner,
470
- )
471
-
472
- self.prompt_task = Task(
473
- description=(
474
- f"Original request: {self._ctx}\n\n"
475
- "Using the directive and data quality report, write a step-by-step analysis prompt:\n"
476
- "1. Exact columns to load\n"
477
- "2. Calculations/aggregations to run\n"
478
- "3. Patterns, outliers, or trends to look for\n"
479
- "4. Order of approach\n"
480
- "5. What a complete answer looks like"
481
- ),
482
- expected_output=(
483
- "Numbered step-by-step prompt. Each step specific and actionable. "
484
- "References exact column names where possible."
485
- ),
486
- context=[self.interpret_task, self.clean_task],
487
- agent=self.prompt_engineer,
488
- )
489
-
490
- self.analyze_task = Task(
491
- description=(
492
- "Dataset path(s):\n{data}\n\n"
493
- "If file paths are provided (one per line), read each with FileReadTool exactly once. "
494
- "If multiple files, analyze together. "
495
- "If no file, answer from the analysis prompt using reasoning.\n\n"
496
- "Follow every step in the prompt. Read each file only once. Report only what the data shows."
497
- ),
498
- expected_output=(
499
- "Thorough analysis containing:\n"
500
- "1. Data source summary\n"
501
- "2. Key statistics (averages, ranges, counts, outliers)\n"
502
- "3. 3-5 concrete findings that answer the prompt\n"
503
- "4. 2-3 actionable recommendations"
504
- ),
505
- context=[self.prompt_task],
506
- agent=self.data_analyst,
507
- )
508
-
509
- self.format_task = Task(
510
- description=(
511
- f"Original request: {self._ctx}\n\n"
512
- "Convert the analyst's findings into a FormattedOutput JSON object.\n\n"
513
- "OUTPUT TYPE PRIORITY (pick first that fits):\n"
514
- " 'code' โ†’ answer is or includes runnable code/queries/scripts\n"
515
- " 'metrics' โ†’ answer is a set of KPIs or key numbers (3-8 items)\n"
516
- " 'comparison' โ†’ comparing two named entities across multiple metrics;\n"
517
- " set comparison_a_label, comparison_b_label, comparison_rows\n"
518
- " (each row: metric, value_a, value_b, winner='a'/'b'/'tie')\n"
519
- " 'heatmap' โ†’ data is a matrix (rows ร— cols) of numeric values;\n"
520
- " set heatmap_row_labels, heatmap_col_labels,\n"
521
- " heatmap_values (2D float list), heatmap_title. Max 10ร—10.\n"
522
- " 'table' โ†’ ranked/multi-attribute list, max 20 rows\n"
523
- " 'chart' โ†’ visual comparison of 2+ values; chart_type options:\n"
524
- " bar, line, pie, scatter, funnel, radar\n"
525
- " For funnel: stages in order, value = count/rate at each stage\n"
526
- " For radar: label=axis, value=series A; optionally value2=series B\n"
527
- " and set radar_b_label for B's name\n"
528
- " 'report' โ†’ qualitative/narrative findings\n\n"
529
- "ALWAYS: summary (2-3 sentences), findings (3-5 strings), recommendations (2-3 strings).\n"
530
- "Return ONLY the raw JSON object. No markdown, no commentary."
531
- ),
532
- expected_output=(
533
- "Single raw JSON object matching FormattedOutput. "
534
- "No markdown fences. Parseable by json.loads() without modification."
535
- ),
536
- context=[self.analyze_task],
537
- agent=self.output_formatter,
538
- output_pydantic=FormattedOutput,
539
- )
540
-
541
- self.qa_task = Task(
542
- description=(
543
- f"Original request: {self._ctx}\n\n"
544
- "Review the completed analysis. Score 1-10 based on:\n"
545
- "- Did it directly and specifically answer the original question?\n"
546
- "- Are findings backed by concrete numbers from the data?\n"
547
- "- Are recommendations actionable and relevant?\n"
548
- "- Is anything important missing, vague, or invented?\n\n"
549
- "Return ONLY: "
550
- '{"score": <int 1-10>, "verdict": "<1-2 sentences: what was done well and what gap remains>"}'
551
- ),
552
- expected_output='Raw JSON only: {"score": <int>, "verdict": "<string>"}. No markdown.',
553
- context=[self.analyze_task, self.format_task],
554
- agent=self.qa_critic,
555
- )
556
-
557
- def create_crew(self, data) -> str:
558
- with _crew_lock:
559
- return self._run_pipeline(data)
560
-
561
- def _run_pipeline(self, data) -> str:
562
- max_attempts = (len(_FAST_MODELS) + len(_SMART_MODELS)) * 2
563
-
564
- for attempt in range(max_attempts):
565
- _wait_until_available()
566
- fast_model, self._fast_idx = _pick_model(_FAST_MODELS)
567
- smart_model, self._smart_idx = _pick_model(_SMART_MODELS)
568
-
569
- self.create_agents()
570
- self.create_tasks()
571
-
572
- crew = Crew(
573
- agents=[
574
- self.context_agent, self.data_cleaner, self.prompt_engineer,
575
- self.data_analyst, self.output_formatter, self.qa_critic,
576
- ],
577
- tasks=[
578
- self.interpret_task, self.clean_task, self.prompt_task,
579
- self.analyze_task, self.format_task, self.qa_task,
580
- ],
581
- process=Process.sequential,
582
- verbose=True,
583
- memory=False,
584
- )
585
- try:
586
- result = crew.kickoff(inputs={"data": data})
587
- except Exception as e:
588
- err_str = str(e)
589
- is_404 = "404" in err_str
590
- is_rate_limit = any(x in err_str for x in ("429", "RateLimitError", "rate_limit_exceeded"))
591
- is_bad_request = any(x in err_str for x in ("BadRequestError", "invalid_request_error"))
592
- is_server_err = any(c in err_str for c in ("402", "401", "503", "529"))
593
- is_rotatable = is_404 or is_rate_limit or is_bad_request or is_server_err
594
-
595
- if is_rotatable and attempt < max_attempts - 1:
596
- if is_rate_limit:
597
- cooldown_s = _parse_retry_after(err_str)
598
- _set_cooldown(fast_model, cooldown_s)
599
- _set_cooldown(smart_model, cooldown_s)
600
- print(f"[RATE-LIMIT] fast={fast_model} smart={smart_model} โ†’ {cooldown_s:.0f}s cooldown")
601
- elif is_404 or is_bad_request:
602
- _set_cooldown(fast_model, 600)
603
- _set_cooldown(smart_model, 600)
604
- print(f"[ROTATE] fast={fast_model} smart={smart_model} โ†’ unavailable, 10-min cooldown")
605
- else:
606
- _set_cooldown(fast_model, 60)
607
- _set_cooldown(smart_model, 60)
608
- print(f"[SERVER-ERR] fast={fast_model} smart={smart_model} โ†’ 60s cooldown")
609
- continue
610
- raise
611
-
612
- fmt_task_out = crew.tasks[4].output if len(crew.tasks) > 4 else None
613
- formatted = None
614
- if fmt_task_out:
615
- if getattr(fmt_task_out, "pydantic", None):
616
- formatted = fmt_task_out.pydantic
617
- else:
618
- try:
619
- formatted = FormattedOutput(
620
- **_json.loads(_extract_json(fmt_task_out.raw or ""))
621
- )
622
- except Exception:
623
- pass
624
-
625
- if formatted:
626
- qa_raw = result.raw if hasattr(result, "raw") else str(result)
627
- try:
628
- qa = _json.loads(_extract_json(qa_raw))
629
- formatted.quality_score = int(qa.get("score", 0)) or None
630
- formatted.quality_verdict = qa.get("verdict")
631
- except Exception:
632
- pass
633
- return formatted.model_dump_json()
634
-
635
- raw = result.raw if hasattr(result, "raw") else str(result)
636
- return _extract_json(raw)
637
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/main.py CHANGED
@@ -1,6 +1,4 @@
1
- <<<<<<< HEAD
2
- =======
3
- """
4
  DataFlow AI โ€” single-file backend.
5
  FastAPI SSE server + CrewAI 6-agent pipeline.
6
  """
@@ -23,16 +21,11 @@ from threading import Lock
23
  from typing import Optional, Literal, List as _List
24
 
25
  # โ”€โ”€ Third-party โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
26
- >>>>>>> 384b2c8 (feat: add GitHub Models support, remove Groq dependency, add backend .gitignore)
27
  from dotenv import load_dotenv
28
  load_dotenv()
29
 
30
- import os
31
- from fastapi import FastAPI
32
  from fastapi.middleware.cors import CORSMiddleware
33
- <<<<<<< HEAD
34
- from routes import watch, analysis, user, charts, gps, integrations
35
- =======
36
  from fastapi.responses import StreamingResponse, JSONResponse
37
  from pydantic import BaseModel
38
 
@@ -44,7 +37,6 @@ litellm.cache = None
44
  litellm.drop_params = True
45
 
46
  # โ”€โ”€ Groq cache_breakpoint patch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
47
- # Groq rejects messages that contain a 'cache_breakpoint' property.
48
  _real_completion = litellm.completion
49
 
50
  def _completion_no_cache_breakpoint(*args, **kwargs):
@@ -198,91 +190,51 @@ class DataPoint(BaseModel):
198
  label: str
199
  value: float
200
  category: Optional[str] = None
201
- x_value: Optional[float] = None # scatter: second axis
202
- value2: Optional[float] = None # radar: second series value
203
 
204
 
205
  class CodeBlock(BaseModel):
206
- language: str # python | sql | bash | r | javascript
207
  title: str
208
- code: str # keep under 400 chars
209
 
210
 
211
  class MetricItem(BaseModel):
212
  label: str
213
- value: str # formatted string, e.g. "1,234" or "98.5%"
214
  unit: Optional[str] = None
215
- trend: Optional[str] = None # up | down | neutral
216
- change: Optional[str] = None # e.g. "+12%"
217
- context: Optional[str] = None # e.g. "vs last week"
218
 
219
 
220
  class ComparisonRow(BaseModel):
221
- metric: str # e.g. "Avg Revenue"
222
- value_a: str # formatted value for entity A
223
- value_b: str # formatted value for entity B
224
  winner: Optional[Literal["a", "b", "tie"]] = None
225
 
226
 
227
  class FormattedOutput(BaseModel):
228
- """
229
- OUTPUT TYPE DECISION RULES โ€” pick the FIRST that matches:
230
- 1. "code" โ†’ answer is or includes runnable code/queries/scripts.
231
- Populate code_blocks (1-3 blocks).
232
- 2. "metrics" โ†’ answer is a set of KPIs or key numbers (3-8 items).
233
- Populate metrics list.
234
- 3. "comparison" โ†’ comparing two named entities side-by-side across metrics.
235
- Populate comparison_a_label, comparison_b_label, comparison_rows.
236
- 4. "heatmap" โ†’ data is a matrix (rows ร— columns) of numeric values โ€” e.g.
237
- a correlation matrix, time-of-day ร— day-of-week activity grid,
238
- or category ร— category frequency table.
239
- Populate heatmap_row_labels, heatmap_col_labels, heatmap_values.
240
- Max 10 rows ร— 10 cols.
241
- 5. "table" โ†’ ranked/multi-attribute list best shown as labelled rows+columns.
242
- Populate table_headers and table_rows (max 20 rows).
243
- 6. "chart" โ†’ 2+ numeric values that can be compared visually.
244
- Choose chart_type:
245
- bar โ†’ named categories
246
- line โ†’ sequential time periods
247
- pie โ†’ parts of a whole, 2-6 slices
248
- scatter โ†’ correlation (set x_value + value per DataPoint)
249
- funnel โ†’ sequential stages with drop-off (conversion pipelines)
250
- radar โ†’ multi-attribute profile comparison across dimensions;
251
- set value for series A; set value2 + radar_b_label
252
- if comparing two entities on the same axes.
253
- 7. "report" โ†’ qualitative or narrative findings.
254
-
255
- ALWAYS REQUIRED:
256
- - summary: 2-3 sentences directly answering the original question.
257
- - findings: 3-5 specific factual strings from the data.
258
- - recommendations: 2-3 actionable strings.
259
- - Set unused fields to null.
260
- """
261
  output_type: Literal["chart", "report", "code", "table", "metrics", "comparison", "heatmap"]
262
- # chart
263
  chart_type: Optional[Literal["bar", "line", "pie", "scatter", "funnel", "radar"]] = None
264
  chart_title: Optional[str] = None
265
  x_axis_label: Optional[str] = None
266
  y_axis_label: Optional[str] = None
267
  data_points: Optional[list[DataPoint]] = None
268
- radar_b_label: Optional[str] = None # label for value2 series in radar
269
- # code
270
  code_blocks: Optional[list[CodeBlock]] = None
271
- # table
272
  table_headers: Optional[list[str]] = None
273
  table_rows: Optional[list[list[str]]] = None
274
- # metrics
275
  metrics: Optional[list[MetricItem]] = None
276
- # comparison
277
  comparison_a_label: Optional[str] = None
278
  comparison_b_label: Optional[str] = None
279
  comparison_rows: Optional[list[ComparisonRow]] = None
280
- # heatmap
281
  heatmap_title: Optional[str] = None
282
  heatmap_row_labels: Optional[list[str]] = None
283
  heatmap_col_labels: Optional[list[str]] = None
284
- heatmap_values: Optional[list[list[float]]] = None # [row_idx][col_idx]
285
- # always
286
  summary: str
287
  findings: list[str]
288
  recommendations: list[str]
@@ -653,9 +605,8 @@ class Bots:
653
 
654
 
655
  # โ”€โ”€ FastAPI app โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
656
- >>>>>>> 384b2c8 (feat: add GitHub Models support, remove Groq dependency, add backend .gitignore)
657
 
658
- app = FastAPI(title="Fitness AI Agents")
659
 
660
  _ALLOWED_ORIGINS = [o.strip() for o in os.getenv(
661
  "ALLOWED_ORIGINS",
@@ -670,14 +621,6 @@ app.add_middleware(
670
  allow_headers=["*"],
671
  )
672
 
673
- <<<<<<< HEAD
674
- app.include_router(watch.router, prefix="/watch", tags=["Watch"])
675
- app.include_router(analysis.router, prefix="/analyze", tags=["Analysis"])
676
- app.include_router(user.router, prefix="/user", tags=["User"])
677
- app.include_router(charts.router, prefix="/charts", tags=["Charts"])
678
- app.include_router(gps.router, prefix="/routes", tags=["GPS"])
679
- app.include_router(integrations.router, prefix="/integrations", tags=["Integrations"])
680
- =======
681
  ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
682
  BOX_CHARS = re.compile(r"[โ•ญโ•ฎโ•ฐโ•ฏโ”‚โ•žโ•กโ•ขโ•Ÿโ•”โ•—โ•šโ•โ•ฌโ•โ”€โ”ผโ”คโ”œโ”ฌโ”ดโ”Œโ””โ”โ”˜โ• โ•ฃโ•ฆโ•งโ•จโ•ฉโ•ชโ•ซ]")
683
 
@@ -709,13 +652,10 @@ class LineCapture(io.TextIOBase):
709
  self._q.put(self._buf.strip())
710
  self._buf = ""
711
 
712
- >>>>>>> 384b2c8 (feat: add GitHub Models support, remove Groq dependency, add backend .gitignore)
713
 
714
  @app.get("/health")
715
  async def health():
716
  return {"status": "ok"}
717
- <<<<<<< HEAD
718
- =======
719
 
720
 
721
  @app.post("/analyze")
@@ -850,4 +790,3 @@ async def analyze(
850
  media_type="text/event-stream",
851
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
852
  )
853
- >>>>>>> 384b2c8 (feat: add GitHub Models support, remove Groq dependency, add backend .gitignore)
 
1
+ ๏ปฟ"""
 
 
2
  DataFlow AI โ€” single-file backend.
3
  FastAPI SSE server + CrewAI 6-agent pipeline.
4
  """
 
21
  from typing import Optional, Literal, List as _List
22
 
23
  # โ”€โ”€ Third-party โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
24
  from dotenv import load_dotenv
25
  load_dotenv()
26
 
27
+ from fastapi import FastAPI, UploadFile, File, Form
 
28
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
29
  from fastapi.responses import StreamingResponse, JSONResponse
30
  from pydantic import BaseModel
31
 
 
37
  litellm.drop_params = True
38
 
39
  # โ”€โ”€ Groq cache_breakpoint patch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
40
  _real_completion = litellm.completion
41
 
42
  def _completion_no_cache_breakpoint(*args, **kwargs):
 
190
  label: str
191
  value: float
192
  category: Optional[str] = None
193
+ x_value: Optional[float] = None
194
+ value2: Optional[float] = None
195
 
196
 
197
  class CodeBlock(BaseModel):
198
+ language: str
199
  title: str
200
+ code: str
201
 
202
 
203
  class MetricItem(BaseModel):
204
  label: str
205
+ value: str
206
  unit: Optional[str] = None
207
+ trend: Optional[str] = None
208
+ change: Optional[str] = None
209
+ context: Optional[str] = None
210
 
211
 
212
  class ComparisonRow(BaseModel):
213
+ metric: str
214
+ value_a: str
215
+ value_b: str
216
  winner: Optional[Literal["a", "b", "tie"]] = None
217
 
218
 
219
  class FormattedOutput(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  output_type: Literal["chart", "report", "code", "table", "metrics", "comparison", "heatmap"]
 
221
  chart_type: Optional[Literal["bar", "line", "pie", "scatter", "funnel", "radar"]] = None
222
  chart_title: Optional[str] = None
223
  x_axis_label: Optional[str] = None
224
  y_axis_label: Optional[str] = None
225
  data_points: Optional[list[DataPoint]] = None
226
+ radar_b_label: Optional[str] = None
 
227
  code_blocks: Optional[list[CodeBlock]] = None
 
228
  table_headers: Optional[list[str]] = None
229
  table_rows: Optional[list[list[str]]] = None
 
230
  metrics: Optional[list[MetricItem]] = None
 
231
  comparison_a_label: Optional[str] = None
232
  comparison_b_label: Optional[str] = None
233
  comparison_rows: Optional[list[ComparisonRow]] = None
 
234
  heatmap_title: Optional[str] = None
235
  heatmap_row_labels: Optional[list[str]] = None
236
  heatmap_col_labels: Optional[list[str]] = None
237
+ heatmap_values: Optional[list[list[float]]] = None
 
238
  summary: str
239
  findings: list[str]
240
  recommendations: list[str]
 
605
 
606
 
607
  # โ”€โ”€ FastAPI app โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
608
 
609
+ app = FastAPI(title="DataFlow AI")
610
 
611
  _ALLOWED_ORIGINS = [o.strip() for o in os.getenv(
612
  "ALLOWED_ORIGINS",
 
621
  allow_headers=["*"],
622
  )
623
 
 
 
 
 
 
 
 
 
624
  ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
625
  BOX_CHARS = re.compile(r"[โ•ญโ•ฎโ•ฐโ•ฏโ”‚โ•žโ•กโ•ขโ•Ÿโ•”โ•—โ•šโ•โ•ฌโ•โ”€โ”ผโ”คโ”œโ”ฌโ”ดโ”Œโ””โ”โ”˜โ• โ•ฃโ•ฆโ•งโ•จโ•ฉโ•ชโ•ซ]")
626
 
 
652
  self._q.put(self._buf.strip())
653
  self._buf = ""
654
 
 
655
 
656
  @app.get("/health")
657
  async def health():
658
  return {"status": "ok"}
 
 
659
 
660
 
661
  @app.post("/analyze")
 
790
  media_type="text/event-stream",
791
  headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
792
  )