| """LLM 流式调用:与本地 llama-server 的 OpenAI 兼容 /v1/chat/completions 通信。""" |
| from __future__ import annotations |
|
|
| import json |
| from typing import AsyncIterator, Dict, List |
|
|
| import httpx |
|
|
| from .config import ( |
| LLM_BASE_URL, |
| LLM_MAX_TOKENS, |
| LLM_MODEL, |
| LLM_TEMPERATURE, |
| LLM_TIMEOUT_S, |
| ) |
|
|
|
|
| async def stream_chat( |
| system: str, |
| user: str, |
| *, |
| max_tokens: int | None = None, |
| temperature: float | None = None, |
| ) -> AsyncIterator[str]: |
| """流式调用 LLM,逐 chunk yield 文本。 |
| |
| llama.cpp 的 OpenAI 兼容端点返回 SSE 格式: |
| data: {"choices":[{"delta":{"content":"..."}}]} |
| data: [DONE] |
| """ |
| payload: Dict = { |
| "model": LLM_MODEL, |
| "messages": [ |
| {"role": "system", "content": system}, |
| {"role": "user", "content": user}, |
| ], |
| "max_tokens": max_tokens or LLM_MAX_TOKENS, |
| "temperature": temperature if temperature is not None else LLM_TEMPERATURE, |
| "stream": True, |
| } |
| headers = {"Content-Type": "application/json", "Accept": "text/event-stream"} |
|
|
| timeout = httpx.Timeout( |
| connect=min(3.0, LLM_TIMEOUT_S), |
| read=min(8.0, LLM_TIMEOUT_S), |
| write=min(8.0, LLM_TIMEOUT_S), |
| pool=min(3.0, LLM_TIMEOUT_S), |
| ) |
| url = f"{LLM_BASE_URL.rstrip('/')}/v1/chat/completions" |
|
|
| async with httpx.AsyncClient(timeout=timeout) as client: |
| async with client.stream("POST", url, json=payload, headers=headers) as resp: |
| resp.raise_for_status() |
| async for raw in resp.aiter_lines(): |
| line = raw.strip() |
| if not line.startswith("data:"): |
| continue |
| data = line[5:].strip() |
| if data == "[DONE]": |
| break |
| try: |
| obj = json.loads(data) |
| except json.JSONDecodeError: |
| continue |
| try: |
| delta = obj["choices"][0].get("delta", {}) or {} |
| chunk = delta.get("content", "") |
| except (KeyError, IndexError, TypeError): |
| chunk = "" |
| if chunk: |
| yield chunk |
|
|
|
|
| async def stream_fate_reading( |
| question: str, |
| hex_info: dict, |
| changed_info: dict | None, |
| moving: List[int], |
| level: str = "中平", |
| reason: str = "", |
| ui: dict | None = None, |
| lang: str = "zh", |
| ) -> AsyncIterator[str]: |
| """组装 System+User Prompt,流式返回 LLM 文本。""" |
| from .constants import build_system_prompt, build_user_prompt |
| ui = ui or {"remedy": "自渡锦囊"} |
| system_prompt = build_system_prompt(level, ui, lang=lang) |
| user_prompt = build_user_prompt(question, hex_info, changed_info, moving, level, reason, lang=lang) |
| async for chunk in stream_chat(system_prompt, user_prompt): |
| yield chunk |
|
|