feat: 后端基础设施 — LLM工厂/Embedding工厂/验证客户端/会话持久化
- 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 依赖声明
This commit is contained in:
@@ -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
|
||||||
+23
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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)}"}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user