feat: add ai platform migrations and provider flow

This commit is contained in:
2026-06-22 20:58:47 +08:00
parent 3f46b59f76
commit ccf35ca586
17 changed files with 673 additions and 12 deletions
@@ -0,0 +1,77 @@
# V2 AI Platform Migration and Provider Flow Closure
日期:2026-06-22
## 本轮目标
继续推进 `yuqei-ai-platform` 的真实运行能力,把上一轮“配置、Prompt、知识、审计可落库”推进到:
- 有 Alembic migration。
- 有 OpenAI-compatible Provider Adapter。
- 法律问答按“知识检索 -> Prompt 渲染 -> Provider 调用 -> 回退 -> 审计”运行。
- 默认仍可在无 API Key 的情况下稳定工作。
## 已完成
1. Alembic 迁移底座
- 新增 `alembic.ini`
- 新增 `migrations/env.py`
- 新增首个迁移 `20260622_0001_create_ai_platform_tables.py`
- 覆盖表:
- `ai_provider_configs`
- `prompt_templates`
- `knowledge_documents`
- `ai_run_audits`
2. Provider Adapter
- 新增 `providers.py`
- 支持 `DisabledProviderAdapter`
- 支持 `OpenAICompatibleProviderAdapter`,接口按 `/chat/completions` 兼容格式调用。
- 支持 `AI_PLATFORM_PROVIDER_CALL_MODE`
- 支持 `AI_PLATFORM_PROVIDER_API_KEY``AI_PLATFORM_PROVIDER_BASE_URL``AI_PLATFORM_PROVIDER_REQUEST_TIMEOUT_SECONDS`
- Provider 调用失败时可按 `AI_PLATFORM_PROVIDER_FALLBACK_ENABLED` 回退。
3. Prompt 渲染
- 新增 `prompts.py`
- 法律问答模板支持:
- `{question}`
- `{matter_context}`
- `{citations}`
- 后台模板未写占位符时,会自动附加标准问题/背景/依据块。
- 模板占位符写错时,会回退到内置默认模板,避免问答接口 500。
4. 法律问答主链路
- 先检索知识文档。
- 将检索结果转换为引用依据。
- 渲染 Prompt。
- 如果 provider 成功,使用 provider 回答。
- 如果 provider 被禁用或失败,默认回退到规则型可审计回答。
- 审计状态支持:
- `succeeded`
- `fallback`
- `failed`
5. 部署配置
- AI Platform Docker 镜像增加 `alembic``httpx`
- compose 本地环境显式设置:
- `AI_PLATFORM_REPOSITORY_BACKEND=sqlalchemy`
- `AI_PLATFORM_DATABASE_AUTO_CREATE=true`
- `AI_PLATFORM_PROVIDER_CALL_MODE=disabled`
- `AI_PLATFORM_PROVIDER_FALLBACK_ENABLED=true`
## 设计边界
- 合同系统仍不直接调用模型 provider。
- 合同系统仍只通过 SDK/OpenAPI 使用 AI 中台。
- 真实 provider 默认关闭,避免无密钥环境下影响开发、测试和演示。
- 当前 provider adapter 先支持 OpenAI-compatible 格式,DeepSeek、通义、私有模型可通过该格式优先接入。
## 下一轮建议
下一轮进入知识库检索增强:
1. 增加知识文档切分表。
2. 增加导入批次和索引状态。
3. 增加检索命中分数和来源定位。
4. 法律问答返回更结构化的“主要依据”列表。
5. 审计记录扩展 provider 错误、token、费用和命中文档 ID。
@@ -663,7 +663,7 @@ components:
type: integer
status:
type: string
enum: [pending, running, succeeded, failed, cancelled]
enum: [pending, running, succeeded, fallback, skipped, failed, cancelled]
created_at:
type: string
format: date-time
@@ -18,6 +18,22 @@ $env:AI_PLATFORM_DATABASE_URL="postgresql+psycopg://ai:ai@localhost:5432/yuqei_a
python -m uvicorn yuqei_ai_platform_api.main:app --app-dir src --reload --port 8101
```
Run database migrations from this service directory:
```powershell
$env:AI_PLATFORM_DATABASE_URL="postgresql+psycopg://ai:ai@localhost:5432/yuqei_ai"
python -m alembic upgrade head
```
Provider calls are disabled by default. To enable an OpenAI-compatible provider:
```powershell
$env:AI_PLATFORM_PROVIDER_CALL_MODE="enabled"
$env:AI_PLATFORM_PROVIDER_API_KEY="your-api-key"
$env:AI_PLATFORM_PROVIDER_BASE_URL="https://api.deepseek.com/v1"
$env:AI_PLATFORM_PROVIDER_FALLBACK_ENABLED="true"
```
Health endpoints:
- `GET /health`
@@ -0,0 +1,42 @@
[alembic]
script_location = migrations
prepend_sys_path = src
path_separator = os
sqlalchemy.url = postgresql+psycopg://ai:ai@localhost:5432/yuqei_ai
[post_write_hooks]
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
@@ -0,0 +1,54 @@
from __future__ import annotations
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from yuqei_ai_platform_api.config import get_settings
from yuqei_ai_platform_api.models import Base
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def get_database_url() -> str:
return get_settings().database_url
def run_migrations_offline() -> None:
context.configure(
url=get_database_url(),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
configuration = config.get_section(config.config_ini_section, {})
configuration["sqlalchemy.url"] = get_database_url()
connectable = engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -0,0 +1,89 @@
"""create ai platform tables
Revision ID: 20260622_0001
Revises:
Create Date: 2026-06-22
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260622_0001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"ai_provider_configs",
sa.Column("id", sa.String(length=64), nullable=False),
sa.Column("provider", sa.String(length=64), nullable=False),
sa.Column("model", sa.String(length=128), nullable=False),
sa.Column("display_name", sa.String(length=128), nullable=False),
sa.Column("base_url", sa.String(length=512), nullable=True),
sa.Column("temperature", sa.Float(), nullable=False),
sa.Column("max_tokens", sa.Integer(), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False),
sa.Column("is_default", sa.Boolean(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_ai_provider_configs_provider", "ai_provider_configs", ["provider"])
op.create_table(
"prompt_templates",
sa.Column("id", sa.String(length=64), nullable=False),
sa.Column("name", sa.String(length=128), nullable=False),
sa.Column("version", sa.String(length=32), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_prompt_templates_name", "prompt_templates", ["name"])
op.create_table(
"knowledge_documents",
sa.Column("id", sa.String(length=64), nullable=False),
sa.Column("knowledge_base_id", sa.String(length=64), nullable=False),
sa.Column("title", sa.String(length=256), nullable=False),
sa.Column("source_type", sa.String(length=64), nullable=False),
sa.Column("reference", sa.String(length=256), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_knowledge_documents_knowledge_base_id", "knowledge_documents", ["knowledge_base_id"])
op.create_table(
"ai_run_audits",
sa.Column("id", sa.String(length=64), nullable=False),
sa.Column("operation", sa.String(length=64), nullable=False),
sa.Column("provider_config_id", sa.String(length=64), nullable=True),
sa.Column("prompt_template_id", sa.String(length=64), nullable=True),
sa.Column("question", sa.Text(), nullable=True),
sa.Column("answer", sa.Text(), nullable=True),
sa.Column("citations_count", sa.Integer(), nullable=False),
sa.Column("latency_ms", sa.Integer(), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_ai_run_audits_operation", "ai_run_audits", ["operation"])
op.create_index("ix_ai_run_audits_created_at", "ai_run_audits", ["created_at"])
def downgrade() -> None:
op.drop_index("ix_ai_run_audits_created_at", table_name="ai_run_audits")
op.drop_index("ix_ai_run_audits_operation", table_name="ai_run_audits")
op.drop_table("ai_run_audits")
op.drop_index("ix_knowledge_documents_knowledge_base_id", table_name="knowledge_documents")
op.drop_table("knowledge_documents")
op.drop_index("ix_prompt_templates_name", table_name="prompt_templates")
op.drop_table("prompt_templates")
op.drop_index("ix_ai_provider_configs_provider", table_name="ai_provider_configs")
op.drop_table("ai_provider_configs")
@@ -4,7 +4,9 @@ version = "0.1.0"
description = "Yuqei shared AI platform V2 API"
requires-python = ">=3.12"
dependencies = [
"alembic>=1.16",
"fastapi>=0.115",
"httpx>=0.28",
"pydantic>=2.10",
"pydantic-settings>=2.7",
"psycopg[binary]>=3.2",
@@ -10,10 +10,16 @@ class AiPlatformSettings(BaseSettings):
log_level: str = "INFO"
api_prefix: str = "/api/v1"
database_url: str = "postgresql+psycopg://ai:ai@localhost:5432/yuqei_ai"
database_auto_create: bool = True
database_auto_create: bool = False
redis_url: str = "redis://localhost:6379/1"
provider_config_source: Literal["database", "environment"] = "database"
repository_backend: Literal["memory", "sqlalchemy"] = "memory"
provider_call_mode: Literal["disabled", "enabled"] = "disabled"
provider_api_key: str | None = None
provider_api_keys: dict[str, str] = {}
provider_base_url: str | None = None
provider_request_timeout_seconds: float = 30.0
provider_fallback_enabled: bool = True
model_config = SettingsConfigDict(
env_file=".env",
@@ -9,6 +9,8 @@ from yuqei_ai_platform_api import __version__
from yuqei_ai_platform_api.config import AiPlatformSettings, get_settings
from yuqei_ai_platform_api.legal_qa import LegalQaRequest, LegalQaResponse, answer_legal_question
from yuqei_ai_platform_api.logging import configure_logging
from yuqei_ai_platform_api.prompts import render_legal_qa_prompt
from yuqei_ai_platform_api.providers import ProviderRequest, build_provider_adapter
from yuqei_ai_platform_api.repository import (
AiPlatformRepository,
AiRunAudit,
@@ -37,6 +39,7 @@ def create_app(
) -> FastAPI:
resolved_settings = settings or get_settings()
store = repository or get_repository(resolved_settings)
provider_adapter = build_provider_adapter(resolved_settings)
configure_logging(resolved_settings.log_level)
app = FastAPI(
@@ -124,12 +127,34 @@ def create_app(
def run_legal_qa(request: LegalQaRequest) -> LegalQaResponse:
started_at = perf_counter()
documents = _search_citation_documents(store, request)
response = answer_legal_question(
request,
retrieved_citations=[document_to_citation(document) for document in documents],
)
citations = [document_to_citation(document) for document in documents]
provider_config = store.get_default_provider_config()
prompt_template = store.get_prompt_template("legal_qa.default")
prompt = render_legal_qa_prompt(
request,
citations=citations,
prompt_template=prompt_template,
)
provider_response = provider_adapter.generate(
ProviderRequest(prompt=prompt, provider_config=provider_config)
)
fallback_response = answer_legal_question(
request,
retrieved_citations=citations,
)
if provider_response.status == "succeeded" and provider_response.answer:
response = fallback_response.model_copy(update={"answer": provider_response.answer})
audit_status = "succeeded"
elif resolved_settings.provider_fallback_enabled:
response = fallback_response
audit_status = "fallback"
else:
response = LegalQaResponse(
answer="AI 服务暂时不可用,请稍后重试。",
citations=citations,
confidence=0.0,
)
audit_status = "failed"
store.add_ai_run_audit(
AiRunAudit(
id=f"run-{uuid4().hex[:12]}",
@@ -140,7 +165,7 @@ def create_app(
answer=response.answer,
citations_count=len(response.citations),
latency_ms=max(0, int((perf_counter() - started_at) * 1000)),
status="succeeded",
status=audit_status,
)
)
return response
@@ -0,0 +1,46 @@
from __future__ import annotations
from yuqei_ai_platform_api.legal_qa import LegalCitation, LegalQaRequest
from yuqei_ai_platform_api.repository import PromptTemplate
def render_legal_qa_prompt(
request: LegalQaRequest,
*,
citations: list[LegalCitation],
prompt_template: PromptTemplate | None,
) -> str:
template = prompt_template.content if prompt_template else _default_legal_qa_template()
variables = {
"question": request.question.strip(),
"matter_context": (request.matter_context or "").strip() or "无补充背景",
"citations": _format_citations(citations),
}
if "{" not in template:
template = f"{template}\n\n{_default_legal_qa_template()}"
try:
return template.format(**variables)
except (KeyError, ValueError):
return _default_legal_qa_template().format(**variables)
def _format_citations(citations: list[LegalCitation]) -> str:
if not citations:
return "未命中明确依据,请在回答中说明需要进一步检索确认。"
lines: list[str] = []
for index, citation in enumerate(citations, start=1):
lines.append(
f"{index}. [{citation.source_type}] {citation.title} {citation.reference}: {citation.quote}"
)
return "\n".join(lines)
def _default_legal_qa_template() -> str:
return (
"你是企业合同法律助手。请基于下列检索依据回答用户问题,必须先给结论,再列主要依据。\n"
"用户问题:{question}\n"
"事项背景:{matter_context}\n"
"检索依据:\n{citations}\n"
"回答要求:语言简洁、不要编造法条;如果依据不足,要明确提示需要补充材料。"
)
@@ -0,0 +1,119 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Protocol
import httpx
from yuqei_ai_platform_api.config import AiPlatformSettings
from yuqei_ai_platform_api.repository import ProviderConfig
@dataclass(frozen=True)
class ProviderRequest:
prompt: str
provider_config: ProviderConfig | None
@dataclass(frozen=True)
class ProviderResponse:
answer: str
status: str = "succeeded"
error_message: str | None = None
class ModelProviderAdapter(Protocol):
def generate(self, request: ProviderRequest) -> ProviderResponse: ...
class DisabledProviderAdapter:
def generate(self, request: ProviderRequest) -> ProviderResponse:
return ProviderResponse(
answer="",
status="skipped",
error_message="Provider calls are disabled.",
)
class OpenAICompatibleProviderAdapter:
def __init__(
self,
settings: AiPlatformSettings,
*,
transport: httpx.BaseTransport | None = None,
) -> None:
self._settings = settings
self._transport = transport
def generate(self, request: ProviderRequest) -> ProviderResponse:
config = request.provider_config
if config is None:
return ProviderResponse(answer="", status="failed", error_message="No enabled provider config.")
api_key = self._resolve_api_key(config)
if not api_key:
return ProviderResponse(answer="", status="failed", error_message="Missing provider API key.")
base_url = (config.base_url or self._settings.provider_base_url or "").rstrip("/")
if not base_url:
return ProviderResponse(answer="", status="failed", error_message="Missing provider base URL.")
try:
with httpx.Client(
timeout=self._settings.provider_request_timeout_seconds,
transport=self._transport,
) as client:
response = client.post(
f"{base_url}/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": config.model,
"temperature": config.temperature,
"max_tokens": config.max_tokens,
"messages": [
{
"role": "system",
"content": "你是严谨的企业合同法律助手,回答必须基于给定依据。",
},
{"role": "user", "content": request.prompt},
],
},
)
response.raise_for_status()
except httpx.HTTPError as exc:
return ProviderResponse(answer="", status="failed", error_message=str(exc))
answer = _extract_answer(response.json())
if not answer:
return ProviderResponse(answer="", status="failed", error_message="Provider response has no answer.")
return ProviderResponse(answer=answer)
def _resolve_api_key(self, config: ProviderConfig) -> str | None:
if config.provider in self._settings.provider_api_keys:
return self._settings.provider_api_keys[config.provider]
return self._settings.provider_api_key
def build_provider_adapter(settings: AiPlatformSettings) -> ModelProviderAdapter:
if settings.provider_call_mode == "enabled":
return OpenAICompatibleProviderAdapter(settings)
return DisabledProviderAdapter()
def _extract_answer(payload: dict[str, Any]) -> str:
choices = payload.get("choices")
if not isinstance(choices, list) or not choices:
return ""
first = choices[0]
if not isinstance(first, dict):
return ""
message = first.get("message")
if isinstance(message, dict) and isinstance(message.get("content"), str):
return message["content"].strip()
text = first.get("text")
if isinstance(text, str):
return text.strip()
return ""
@@ -486,9 +486,15 @@ def _default_prompt_template() -> PromptTemplateCreate:
name="legal_qa.default",
version="v1",
content=(
"\u8bf7\u5148\u68c0\u7d22\u6cd5\u89c4\u3001\u4f01\u4e1a\u5236\u5ea6\u548c"
"\u5408\u540c\u6750\u6599\uff0c\u518d\u7528\u7b80\u6d01\u7ed3\u8bba\u56de\u7b54\u3002"
"\u56de\u7b54\u5fc5\u987b\u5217\u660e\u4e3b\u8981\u4f9d\u636e\u548c\u6761\u6b3e\u6765\u6e90\u3002"
"\u4f60\u662f\u4f01\u4e1a\u5408\u540c\u6cd5\u5f8b\u52a9\u624b\u3002"
"\u8bf7\u5148\u6839\u636e\u68c0\u7d22\u4f9d\u636e\u7ed9\u51fa\u7ed3\u8bba\uff0c"
"\u518d\u5217\u660e\u4e3b\u8981\u4f9d\u636e\u548c\u6761\u6b3e\u6765\u6e90\u3002\n"
"\u7528\u6237\u95ee\u9898\uff1a{question}\n"
"\u4e8b\u9879\u80cc\u666f\uff1a{matter_context}\n"
"\u68c0\u7d22\u4f9d\u636e\uff1a\n{citations}\n"
"\u56de\u7b54\u8981\u6c42\uff1a\u8bed\u8a00\u7b80\u6d01\uff0c"
"\u4e0d\u8981\u7f16\u9020\u6cd5\u6761\uff1b\u5982\u679c\u4f9d\u636e\u4e0d\u8db3\uff0c"
"\u660e\u786e\u8bf4\u660e\u9700\u8981\u8865\u5145\u6750\u6599\u3002"
),
enabled=True,
)
@@ -123,7 +123,7 @@ def test_legal_qa_uses_knowledge_and_records_audit() -> None:
assert audits[0]["operation"] == "legal_qa"
assert audits[0]["citations_count"] >= 1
assert audits[0]["prompt_template_id"] is not None
assert audits[0]["status"] == "succeeded"
assert audits[0]["status"] == "fallback"
def test_sqlalchemy_repository_persists_platform_data(tmp_path) -> None:
@@ -0,0 +1,150 @@
import httpx
from fastapi.testclient import TestClient
from yuqei_ai_platform_api.config import AiPlatformSettings
from yuqei_ai_platform_api.legal_qa import LegalCitation, LegalQaRequest
from yuqei_ai_platform_api.main import create_app
from yuqei_ai_platform_api.prompts import render_legal_qa_prompt
from yuqei_ai_platform_api.providers import OpenAICompatibleProviderAdapter, ProviderRequest
from yuqei_ai_platform_api.repository import InMemoryAiPlatformRepository, ProviderConfigCreate
def test_legal_qa_uses_provider_answer_when_enabled(monkeypatch) -> None:
repository = InMemoryAiPlatformRepository()
repository.seed_defaults()
repository.upsert_provider_config(
ProviderConfigCreate(
provider="openai-compatible",
model="legal-pro",
display_name="Legal Pro",
base_url="https://ai.example.test/v1",
enabled=True,
is_default=True,
)
)
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/v1/chat/completions"
assert request.headers["Authorization"] == "Bearer test-key"
payload = request.read().decode("utf-8")
assert "试用期如何约定" in payload
return httpx.Response(
200,
json={
"choices": [
{
"message": {
"content": "模型回答:依据检索材料,应先核对合同期限和试用期上限。"
}
}
]
},
)
def adapter_factory(settings: AiPlatformSettings) -> OpenAICompatibleProviderAdapter:
return OpenAICompatibleProviderAdapter(settings, transport=httpx.MockTransport(handler))
monkeypatch.setattr("yuqei_ai_platform_api.main.build_provider_adapter", adapter_factory)
app = create_app(
AiPlatformSettings(
environment="test",
provider_call_mode="enabled",
provider_api_key="test-key",
),
repository=repository,
)
client = TestClient(app)
response = client.post("/api/v1/legal/qa", json={"question": "试用期如何约定?"})
assert response.status_code == 200
payload = response.json()
assert payload["answer"].startswith("模型回答")
audit_response = client.get("/api/v1/audit/ai-runs")
assert audit_response.json()[0]["status"] == "succeeded"
def test_provider_failure_falls_back_to_rule_answer_and_audit_status() -> None:
repository = InMemoryAiPlatformRepository()
repository.seed_defaults()
repository.upsert_provider_config(
ProviderConfigCreate(
provider="openai-compatible",
model="legal-pro",
display_name="Legal Pro",
base_url="https://ai.example.test/v1",
enabled=True,
is_default=True,
)
)
app = create_app(
AiPlatformSettings(
environment="test",
provider_call_mode="enabled",
provider_api_key=None,
provider_fallback_enabled=True,
),
repository=repository,
)
client = TestClient(app)
response = client.post("/api/v1/legal/qa", json={"question": "试用期如何约定?"})
assert response.status_code == 200
assert response.json()["citations"]
assert client.get("/api/v1/audit/ai-runs").json()[0]["status"] == "fallback"
def test_openai_compatible_provider_extracts_chat_answer() -> None:
provider_config = InMemoryAiPlatformRepository()
created = provider_config.upsert_provider_config(
ProviderConfigCreate(
provider="deepseek",
model="deepseek-chat",
display_name="DeepSeek Chat",
base_url="https://ai.example.test/v1",
enabled=True,
is_default=True,
)
)
def handler(_request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"choices": [{"message": {"content": "OK"}}]})
adapter = OpenAICompatibleProviderAdapter(
AiPlatformSettings(
environment="test",
provider_call_mode="enabled",
provider_api_key="test-key",
),
transport=httpx.MockTransport(handler),
)
response = adapter.generate(ProviderRequest(prompt="hello", provider_config=created))
assert response.status == "succeeded"
assert response.answer == "OK"
def test_render_legal_qa_prompt_includes_question_context_and_citations() -> None:
prompt = render_legal_qa_prompt(
request=LegalQaRequest(
question="租赁合同押金如何约定?",
matter_context="房屋租赁",
),
citations=[
LegalCitation(
source_type="law",
title="中华人民共和国民法典",
reference="第七百零三条",
quote="租赁合同是出租人将租赁物交付承租人使用、收益,承租人支付租金的合同。",
)
],
prompt_template=None,
)
assert "租赁合同押金如何约定" in prompt
assert "房屋租赁" in prompt
assert "第七百零三条" in prompt
+3
View File
@@ -68,9 +68,12 @@ services:
environment:
AI_PLATFORM_ENVIRONMENT: local
AI_PLATFORM_DATABASE_URL: postgresql+psycopg://yuqei:yuqei-local@postgres:5432/yuqei_v2
AI_PLATFORM_DATABASE_AUTO_CREATE: "true"
AI_PLATFORM_REPOSITORY_BACKEND: sqlalchemy
AI_PLATFORM_REDIS_URL: redis://redis:6379/1
AI_PLATFORM_PROVIDER_CONFIG_SOURCE: database
AI_PLATFORM_PROVIDER_CALL_MODE: disabled
AI_PLATFORM_PROVIDER_FALLBACK_ENABLED: "true"
ports:
- "8101:8101"
depends_on:
@@ -7,7 +7,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app
COPY yuqei-ai-platform/services/ai-platform-api/pyproject.toml /tmp/ai-platform-api-pyproject.toml
RUN pip install --no-cache-dir fastapi pydantic pydantic-settings "psycopg[binary]" sqlalchemy uvicorn
RUN pip install --no-cache-dir alembic fastapi httpx pydantic pydantic-settings "psycopg[binary]" sqlalchemy uvicorn
COPY yuqei-ai-platform/services/ai-platform-api yuqei-ai-platform/services/ai-platform-api