Spaces:
Running
Running
Download local_parser.py from igardner/StockAnalysis: direct link, hf CLI and curl.
- Browser
- Download file 5.56 kB
-
https://huggingface.co/spaces/igardner/StockAnalysis/resolve/main/local_parser.py
- Command line
-
hf download hf://spaces/igardner/StockAnalysis/local_parser.py
-
curl -L -o local_parser.py https://huggingface.co/spaces/igardner/StockAnalysis/resolve/main/local_parser.py
5.56 kB
| """Optional local JSON parser for non-JSON upstream responses. | |
| The parser talks to any OpenAI-compatible chat-completions endpoint, for | |
| example Ollama at ``http://127.0.0.1:11434/v1``. It is opt-in: set | |
| ``LOCAL_PARSER_URL`` and ``LOCAL_PARSER_MODEL`` and non-JSON external results | |
| are sent to the local model and converted to JSON. | |
| The parser is a converter, not a second analyst. Its prompt tells the model to | |
| preserve the source facts and never invent prices, targets, or recommendations. | |
| If parsing fails, the app keeps the raw response and returns a clear error. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from typing import Any | |
| import requests | |
| DEFAULT_MODEL = "llama3.1" | |
| DEFAULT_TIMEOUT_SECONDS = 90.0 | |
| MAX_INPUT_CHARS = 12000 | |
| MAX_OUTPUT_TOKENS = 1600 | |
| SYSTEM_PROMPT = ( | |
| "You convert raw stock-analysis output into strict JSON. " | |
| "Return only one JSON object. Do not use markdown or code fences. " | |
| "Preserve the source facts exactly. Do not invent prices, targets, " | |
| "ratings, or recommendations. If a field is missing, leave it out." | |
| ) | |
| _settings: dict[str, Any] = { | |
| "url": "", | |
| "model": DEFAULT_MODEL, | |
| "timeout": DEFAULT_TIMEOUT_SECONDS, | |
| } | |
| class LocalParserError(RuntimeError): | |
| """Raised when the local model cannot produce valid JSON.""" | |
| def configure( | |
| *, | |
| url: str | None = None, | |
| model: str | None = None, | |
| timeout: float | None = None, | |
| ) -> dict[str, Any]: | |
| """Apply settings from ``app.py``. All values are optional.""" | |
| if url is not None: | |
| _settings["url"] = str(url).strip() | |
| if model is not None: | |
| _settings["model"] = str(model).strip() | |
| if timeout is not None: | |
| _settings["timeout"] = float(timeout) | |
| return settings() | |
| def settings() -> dict[str, Any]: | |
| """Return a copy of the current settings, without any credentials.""" | |
| return { | |
| "configured": is_configured(), | |
| "model": _resolved_model(), | |
| "timeout": float(_settings["timeout"]), | |
| } | |
| def is_configured() -> bool: | |
| return bool(_resolved_url()) and bool(_resolved_model()) | |
| def model_name() -> str: | |
| return _resolved_model() | |
| def _resolved_url() -> str: | |
| return str(_settings["url"] or os.getenv("LOCAL_PARSER_URL", "")).strip() | |
| def _resolved_model() -> str: | |
| return str(_settings["model"] or os.getenv("LOCAL_PARSER_MODEL", DEFAULT_MODEL)).strip() | |
| def _chat_url(base: str) -> str: | |
| url = base.rstrip("/") | |
| if url.endswith("/chat/completions"): | |
| return url | |
| if url.endswith("/v1"): | |
| return f"{url}/chat/completions" | |
| return f"{url}/v1/chat/completions" | |
| def _extract_json(text: str) -> Any: | |
| candidate = text.strip() | |
| if candidate.startswith("```"): | |
| lines = candidate.splitlines() | |
| if lines and lines[0].startswith("```"): | |
| lines = lines[1:] | |
| if lines and lines[-1].strip().startswith("```"): | |
| lines = lines[:-1] | |
| candidate = "\n".join(lines).strip() | |
| try: | |
| return json.loads(candidate) | |
| except ValueError: | |
| pass | |
| starts = [index for index in (candidate.find("{"), candidate.find("[")) if index != -1] | |
| if not starts: | |
| raise ValueError("no JSON value found") | |
| start = min(starts) | |
| ends = [candidate.rfind(char) for char in ("}", "]")] | |
| ends = [index for index in ends if index != -1] | |
| if not ends: | |
| raise ValueError("no JSON value end found") | |
| end = max(ends) | |
| if end <= start: | |
| raise ValueError("invalid JSON span") | |
| return json.loads(candidate[start : end + 1]) | |
| def parse_json(raw_text: str, ticker: str = "") -> Any: | |
| """Convert raw upstream text into JSON using the configured local model.""" | |
| if not is_configured(): | |
| raise LocalParserError("The local parser is not configured.") | |
| if not raw_text.strip(): | |
| raise LocalParserError("The upstream response was empty.") | |
| model = _resolved_model() | |
| user_content = ( | |
| f"Ticker: {ticker or 'unknown'}\n\n" | |
| "Raw analysis output:\n" | |
| f"{raw_text[:MAX_INPUT_CHARS]}" | |
| ) | |
| payload = { | |
| "model": model, | |
| "messages": [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_content}, | |
| ], | |
| "temperature": 0, | |
| "max_tokens": MAX_OUTPUT_TOKENS, | |
| "stream": False, | |
| } | |
| try: | |
| response = requests.post( | |
| _chat_url(_resolved_url()), | |
| json=payload, | |
| timeout=float(_settings["timeout"]), | |
| ) | |
| except requests.RequestException as exc: | |
| raise LocalParserError( | |
| f"Local parser request failed ({type(exc).__name__})." | |
| ) from exc | |
| if not response.ok: | |
| detail = (response.text or "").strip()[:200] | |
| raise LocalParserError( | |
| f"Local parser returned HTTP {response.status_code}: {detail}" | |
| ) | |
| try: | |
| data = response.json() | |
| except ValueError as exc: | |
| raise LocalParserError("Local parser returned a non-JSON response.") from exc | |
| try: | |
| content = data["choices"][0]["message"]["content"] | |
| except (KeyError, IndexError, TypeError) as exc: | |
| raise LocalParserError( | |
| "Local parser response did not contain message content." | |
| ) from exc | |
| if not isinstance(content, str) or not content.strip(): | |
| raise LocalParserError("Local parser returned empty content.") | |
| try: | |
| return _extract_json(content) | |
| except ValueError as exc: | |
| raise LocalParserError("Local parser output was not valid JSON.") from exc | |