From 21a5fdf930e280610b16e8213921e8970cb0b97a Mon Sep 17 00:00:00 2001 From: panda <1415243231@qq.com> Date: Thu, 14 May 2026 23:20:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E7=AB=AF=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E8=AE=BE=E6=96=BD=20=E2=80=94=20LLM=E5=B7=A5=E5=8E=82/Embeddin?= =?UTF-8?q?g=E5=B7=A5=E5=8E=82/=E9=AA=8C=E8=AF=81=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF/=E4=BC=9A=E8=AF=9D=E6=8C=81=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend/llm.py: 支持 OpenAI 兼容 API 与 Ollama 本地模型切换 - backend/embeddings.py: 支持云端与本地嵌入模型(sentence-transformers) - backend/validation.py: FastAPI 验证服务 HTTP 客户端 - backend/session.py: JSON 文件会话管理(创建/加载/保存/列表/删除) - .env.example: 完整环境变量模板 - requirements.txt: 所有 Python 依赖声明 --- .env.example | 38 ++++++++++++++ .gitignore | 23 +++++++++ backend/__init__.py | 0 backend/embeddings.py | 26 ++++++++++ backend/llm.py | 28 +++++++++++ backend/session.py | 112 ++++++++++++++++++++++++++++++++++++++++++ backend/validation.py | 26 ++++++++++ requirements.txt | 27 ++++++++++ 8 files changed, 280 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 backend/__init__.py create mode 100644 backend/embeddings.py create mode 100644 backend/llm.py create mode 100644 backend/session.py create mode 100644 backend/validation.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..35668fb --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# 大语言模型后端:cloud 或 local +LLM_BACKEND=cloud + +# 云端配置(OpenAI 兼容) +OPENAI_API_KEY=sk-xxxx +OPENAI_BASE_URL=https://api.openai.com/v1 +LLM_MODEL=gpt-4o + +# 本地大语言模型(Ollama) +LOCAL_LLM_MODEL=qwen2.5-coder:7b + +# 嵌入模型后端:local 或 cloud +EMBED_BACKEND=local +LOCAL_EMBED_MODEL=Qwen/Qwen3-Embedding-0.6B + +# 验证服务地址 +VALIDATION_SERVICE_URL=http://localhost:8001/validate + +# Chroma 持久化目录 +CHROMA_PERSIST_DIR=./db/chroma + +# 最大自动修正尝试次数 +MAX_RETRY=3 + +# 上下文压缩阈值(token 数) +CONTEXT_MAX_TOKENS=6000 + +# 保留最近 N 轮完整对话 +CONTEXT_KEEP_RECENT=4 + +# 会话持久化目录 +SESSIONS_DIR=./sessions + +# 状态快照保留数量(用于撤销操作) +HISTORY_MAX_SNAPSHOTS=10 + +# 意图识别模型(默认使用主 LLM 模型) +# INTENT_MODEL=gpt-4o-mini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fda7982 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# 环境配置(含密钥) +.env + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +.venv/ + +# 数据库 +db/chroma/ +sessions/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# 系统文件 +Thumbs.db +.DS_Store diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/embeddings.py b/backend/embeddings.py new file mode 100644 index 0000000..0c7ee93 --- /dev/null +++ b/backend/embeddings.py @@ -0,0 +1,26 @@ +"""嵌入模型工厂:支持本地 sentence-transformers 和云端 API。""" + +import os +from dotenv import load_dotenv + +load_dotenv() + + +def get_embeddings(): + backend = os.getenv("EMBED_BACKEND", "local") + if backend == "cloud": + from langchain_openai import OpenAIEmbeddings + + return OpenAIEmbeddings( + model=os.getenv("EMBED_CLOUD_MODEL", "text-embedding-3-small"), + api_key=os.getenv("OPENAI_API_KEY"), + base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), + ) + else: + try: + from langchain_huggingface import HuggingFaceEmbeddings + except ImportError: + from langchain_community.embeddings import HuggingFaceEmbeddings + + model = os.getenv("LOCAL_EMBED_MODEL", "Qwen/Qwen3-Embedding-0.6B") + return HuggingFaceEmbeddings(model_name=model) diff --git a/backend/llm.py b/backend/llm.py new file mode 100644 index 0000000..a9584ec --- /dev/null +++ b/backend/llm.py @@ -0,0 +1,28 @@ +"""大语言模型工厂:支持 OpenAI 兼容的云端 API 和本地 Ollama。""" + +import os +from dotenv import load_dotenv + +load_dotenv() + + +def get_llm(): + backend = os.getenv("LLM_BACKEND", "cloud") + if backend == "local": + from langchain_ollama import ChatOllama + + model = os.getenv("LOCAL_LLM_MODEL", "qwen2.5-coder:7b") + return ChatOllama(model=model, temperature=0.1) + else: + from langchain_openai import ChatOpenAI + + return ChatOpenAI( + model=os.getenv("LLM_MODEL", "gpt-4o"), + api_key=os.getenv("OPENAI_API_KEY"), + base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), + temperature=0.1, + ) + + +def get_llm_for_correction(): + return get_llm() diff --git a/backend/session.py b/backend/session.py new file mode 100644 index 0000000..8a41d58 --- /dev/null +++ b/backend/session.py @@ -0,0 +1,112 @@ +"""多会话持久化管理模块。 + +每个会话对应一个独立的 JSON 文件存储在 ./sessions/ 目录下。 +""" + +import json +import os +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +from dotenv import load_dotenv + +load_dotenv() + +SESSIONS_DIR = Path(os.getenv("SESSIONS_DIR", "./sessions")) + + +def _ensure_dir(): + SESSIONS_DIR.mkdir(parents=True, exist_ok=True) + + +def _session_path(session_id: str) -> Path: + return SESSIONS_DIR / f"{session_id}.json" + + +def generate_session_id() -> str: + return uuid.uuid4().hex[:12] + + +def create_session(name: str = "", agent_state: Optional[dict] = None) -> dict: + """创建新会话,返回会话元数据。""" + _ensure_dir() + sid = generate_session_id() + now = _now_iso() + data = { + "session_id": sid, + "session_name": name or f"新建报表 {now[:10]}", + "created_at": now, + "updated_at": now, + "agent_state": agent_state or {}, + } + with open(_session_path(sid), "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + return data + + +def load_session(session_id: str) -> Optional[dict]: + """按 ID 加载会话数据。未找到则返回 None。""" + _ensure_dir() + fp = _session_path(session_id) + if not fp.exists(): + return None + with open(fp, "r", encoding="utf-8") as f: + return json.load(f) + + +def save_session(session_id: str, agent_state: dict, session_name: str = ""): + """将会话状态保存(更新)至磁盘。""" + _ensure_dir() + fp = _session_path(session_id) + data = {} + if fp.exists(): + with open(fp, "r", encoding="utf-8") as f: + data = json.load(f) + + data["session_id"] = session_id + if session_name: + data["session_name"] = session_name + if not data.get("session_name"): + data["session_name"] = f"报表 {data.get('created_at', _now_iso())[:10]}" + data["updated_at"] = _now_iso() + if not data.get("created_at"): + data["created_at"] = data["updated_at"] + data["agent_state"] = agent_state + + with open(fp, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + +def list_all_sessions() -> list[dict]: + """列出所有历史会话(仅摘要,不含完整 agent_state)。""" + _ensure_dir() + sessions = [] + for fp in sorted(SESSIONS_DIR.glob("*.json"), key=os.path.getmtime, reverse=True): + try: + with open(fp, "r", encoding="utf-8") as f: + data = json.load(f) + sessions.append({ + "session_id": data.get("session_id", fp.stem), + "session_name": data.get("session_name", fp.stem), + "created_at": data.get("created_at", ""), + "updated_at": data.get("updated_at", ""), + }) + except (json.JSONDecodeError, KeyError): + continue + return sessions + + +def delete_session(session_id: str) -> bool: + """按 ID 删除会话文件。""" + _ensure_dir() + fp = _session_path(session_id) + if fp.exists(): + fp.unlink() + return True + return False + + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() diff --git a/backend/validation.py b/backend/validation.py new file mode 100644 index 0000000..df7647d --- /dev/null +++ b/backend/validation.py @@ -0,0 +1,26 @@ +"""FastAPI 验证服务的客户端。""" + +import os + +import httpx +from dotenv import load_dotenv + +load_dotenv() + +VALIDATION_URL = os.getenv("VALIDATION_SERVICE_URL", "http://localhost:8001/validate") + + +def validate_jrxml(jrxml_text: str) -> dict: + """将 JRXML 发送到验证服务并返回 {valid: bool, error: str}。""" + try: + with httpx.Client(timeout=30.0) as client: + resp = client.post(VALIDATION_URL, json={"jrxml": jrxml_text}) + resp.raise_for_status() + return resp.json() + except httpx.ConnectError: + return { + "valid": False, + "error": f"无法连接到验证服务 ({VALIDATION_URL})。是否正在运行?", + } + except Exception as e: + return {"valid": False, "error": f"验证请求失败: {str(e)}"} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..adb3b65 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +# 核心依赖 +streamlit>=1.28.0 +langgraph>=0.2.0 +langchain>=0.3.0 +langchain-openai>=0.2.0 +langchain-ollama>=0.2.0 +langchain-community>=0.3.0 + +# 向量数据库 +chromadb>=0.5.0 + +# 验证服务 +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +lxml>=5.3.0 + +# 嵌入模型(本地) +sentence-transformers>=3.0.0 + +# 工具类 +python-dotenv>=1.0.0 +httpx>=0.27.0 +tiktoken>=0.7.0 + +# 测试 +pytest>=8.0.0 +pytest-asyncio>=0.24.0