Spaces:
Runtime error
Runtime error
| """ | |
| PregoPal - MiniCPM-o API 客户端 | |
| 用法: | |
| from modal_deploy.client import MiniCPMClient | |
| client = MiniCPMClient(base_url="https://your-app.modal.run") | |
| response = client.chat("今天孕妇可以吃什么?") | |
| """ | |
| import json | |
| import logging | |
| from typing import Optional | |
| from dataclasses import dataclass, field | |
| import requests | |
| logger = logging.getLogger(__name__) | |
| class ChatMessage: | |
| role: str | |
| content: str | |
| class MultimodalContent: | |
| type: str # "text" or "image_url" | |
| text: Optional[str] = None | |
| image_url: Optional[dict] = None | |
| class MiniCPMClient: | |
| """封装 MiniCPM-o API 的客户端""" | |
| def __init__( | |
| self, | |
| base_url: str = "http://localhost:8080", | |
| timeout: int = 120, | |
| default_max_tokens: int = 1024, | |
| default_temperature: float = 0.7, | |
| ): | |
| self.base_url = base_url.rstrip("/") | |
| self.timeout = timeout | |
| self.default_max_tokens = default_max_tokens | |
| self.default_temperature = default_temperature | |
| def _post(self, path: str, body: dict) -> dict: | |
| url = f"{self.base_url}{path}" | |
| try: | |
| r = requests.post(url, json=body, timeout=self.timeout) | |
| r.raise_for_status() | |
| return r.json() | |
| except requests.exceptions.RequestException as e: | |
| logger.error(f"API request failed: {e}") | |
| raise | |
| def health(self) -> dict: | |
| """检查服务健康状态""" | |
| r = requests.get(f"{self.base_url}/health", timeout=10) | |
| return r.json() | |
| # ── 文本对话 ────────────────────────────────────────── | |
| def chat( | |
| self, | |
| messages: list[dict], | |
| system_prompt: Optional[str] = None, | |
| max_tokens: Optional[int] = None, | |
| temperature: Optional[float] = None, | |
| stream: bool = False, | |
| ) -> dict | str: | |
| """ | |
| OpenAI 兼容的聊天接口 | |
| Args: | |
| messages: 消息列表 [{"role": "user", "content": "..."}] | |
| system_prompt: 系统提示词 | |
| max_tokens: 最大生成长度 | |
| temperature: 温度参数 | |
| stream: 是否流式 | |
| Returns: | |
| dict 或流式字符串 | |
| """ | |
| msgs = [] | |
| if system_prompt: | |
| msgs.append({"role": "system", "content": system_prompt}) | |
| msgs.extend(messages) | |
| body = { | |
| "model": "MiniCPM-o-4_5", | |
| "messages": msgs, | |
| "max_tokens": max_tokens or self.default_max_tokens, | |
| "temperature": temperature or self.default_temperature, | |
| "stream": stream, | |
| } | |
| return self._post("/v1/chat/completions", body) | |
| def ask(self, prompt: str, system_prompt: Optional[str] = None) -> str: | |
| """简化调用:发送问题,返回文本回答""" | |
| result = self.chat( | |
| messages=[{"role": "user", "content": prompt}], | |
| system_prompt=system_prompt, | |
| ) | |
| try: | |
| return result["choices"][0]["message"]["content"] | |
| except (KeyError, IndexError, TypeError): | |
| logger.error(f"Unexpected response format: {result}") | |
| return str(result) | |
| # ── 多模态(图片理解) ─────────────────────────────── | |
| def chat_with_image( | |
| self, | |
| prompt: str, | |
| image_base64: str, | |
| image_format: str = "jpeg", | |
| max_tokens: Optional[int] = None, | |
| temperature: Optional[float] = None, | |
| ) -> dict: | |
| """ | |
| 发送图片 + 文字进行多模态对话 | |
| Args: | |
| prompt: 文字提示 | |
| image_base64: base64 编码的图片 | |
| image_format: 图片格式 (jpeg, png, webp) | |
| """ | |
| body = { | |
| "messages": [ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "text", "text": prompt}, | |
| { | |
| "type": "image_url", | |
| "image_url": { | |
| "url": f"data:image/{image_format};base64,{image_base64}" | |
| }, | |
| }, | |
| ], | |
| } | |
| ], | |
| "max_tokens": max_tokens or self.default_max_tokens, | |
| "temperature": temperature or self.default_temperature, | |
| } | |
| return self._post("/v1/chat/completions", body) | |
| def describe_image(self, image_base64: str, image_format: str = "jpeg") -> str: | |
| """简化调用:描述图片内容""" | |
| result = self.chat_with_image( | |
| prompt="请详细描述这张图片中的内容。", | |
| image_base64=image_base64, | |
| image_format=image_format, | |
| ) | |
| return result.get("response", str(result)) | |
| # ── 嵌入向量 ────────────────────────────────────────── | |
| def embed(self, texts: list[str]) -> list[list[float]]: | |
| """获取文本的嵌入向量""" | |
| body = { | |
| "model": "MiniCPM-o-4_5", | |
| "input": texts, | |
| } | |
| result = self._post("/v1/embeddings", body) | |
| return [item["embedding"] for item in result.get("data", [])] | |
| # ── 预设提示词 (针对孕期陪护场景) ──────────────────────── | |
| SYSTEM_PROMPTS = { | |
| "diet_advisor": """你是一位专业的孕期营养顾问。请根据以下原则提供建议: | |
| 1. 优先考虑食材的易得性和家庭制作能力 | |
| 2. 注意孕期各阶段的营养需求差异 | |
| 3. 避免生冷、辛辣、高汞鱼类等孕期禁忌食物 | |
| 4. 推荐富含叶酸、铁、钙、DHA 的食物 | |
| 5. 回答简洁实用,给出具体可操作的菜品建议""", | |
| "meal_planner": """你是一位家庭营养师,专门为孕妇设计日常菜单。要求: | |
| 1. 每天设计早中晚三餐 + 2次加餐 | |
| 2. 确保营养均衡,覆盖蛋白质、碳水、脂肪、维生素、矿物质 | |
| 3. 考虑中国家庭的烹饪习惯和常见食材 | |
| 4. 标注每餐的关键营养素 | |
| 5. 给出简要的制作步骤""", | |
| "food_analyzer": """分析用户提供的食物/菜品描述,提取以下信息: | |
| 1. 菜名 | |
| 2. 主要食材 | |
| 3. 营养成分评估(热量、蛋白质、脂肪、碳水、关键微量元素) | |
| 4. 孕期适宜度(推荐/可食用/慎食/忌食) | |
| 5. 建议(如有) | |
| 请用 JSON 格式回复。""", | |
| "nutrition_summary": """根据用户提供的饮食记录,总结营养摄入情况: | |
| 1. 各大类营养素摄入评估 | |
| 2. 与孕期推荐标准的对比 | |
| 3. 需要补充或调整的方向 | |
| 4. 具体的一周饮食改善建议 | |
| 5. 用结构化格式输出,便于前端可视化""", | |
| } | |