| """ |
| Cloud inference connector for Pocket Dungeon Master. |
| |
| Streams RPG narrative from the Modal-hosted vLLM backend (OpenAI-compatible). |
| Configured via environment variables (set as HF Space secrets): |
| MODAL_BACKEND_URL e.g. https://<workspace>--pocket-dm-backend-serve.modal.run |
| MODAL_API_KEY the VLLM_API_KEY chosen when deploying the Modal secret |
| """ |
| import os |
| from openai import OpenAI |
|
|
| from engine import load_system_prompt, log_trace |
|
|
| MODAL_BACKEND_URL = os.environ.get( |
| "MODAL_BACKEND_URL", "https://your-unique-modal-subdomain.modal.run" |
| ) |
| MODAL_API_KEY = os.environ.get("MODAL_API_KEY", "EMPTY") |
| MODEL_NAME = os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-3B-Instruct") |
|
|
| _client = OpenAI( |
| base_url=MODAL_BACKEND_URL.rstrip("/") + "/v1", |
| api_key=MODAL_API_KEY or "EMPTY", |
| timeout=120.0, |
| ) |
|
|
|
|
| def call_modal_generator(item_id, history_messages): |
| """Yields the cumulative narrative text as it streams from the backend.""" |
| messages = [{"role": "system", "content": load_system_prompt(item_id)}] |
| for msg in history_messages: |
| role = getattr(msg, "role", None) or (msg.get("role") if isinstance(msg, dict) else None) |
| content = getattr(msg, "content", None) or (msg.get("content") if isinstance(msg, dict) else None) |
| if content: |
| messages.append({"role": role, "content": content}) |
|
|
| full_reply_text = "" |
| try: |
| stream = _client.chat.completions.create( |
| model=MODEL_NAME, |
| messages=messages, |
| max_tokens=220, |
| temperature=0.85, |
| stream=True, |
| ) |
| for chunk in stream: |
| delta = chunk.choices[0].delta.content or "" |
| if delta: |
| full_reply_text += delta |
| yield full_reply_text |
| except Exception as e: |
| yield f"⚠️ The artifact's voice falters — the cloud core is unreachable ({e})." |
| return |
|
|
| |
| try: |
| last_user = next( |
| (m["content"] for m in reversed(messages) if m["role"] == "user"), "" |
| ) |
| log_trace({"item": item_id, "user": last_user, "model": full_reply_text}) |
| except Exception: |
| pass |
|
|
|
|
| def get_combat_keywords(): |
| return ['rage', 'boil', 'heat', 'kill', 'attack', 'shatter', 'fight', |
| 'force', 'mutate', 'scream', 'wield', 'blade', 'potion', 'key'] |
|
|