初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
"""资料导入候选抽取用例(异步后台任务,跨 services + ai_provider)。
触发端点在校验并置 job 为 ``extracting_*`` 后即返回;实际模型调用在后台任务中
进行(独立 session),完成后落候选并置 job 为 ``reviewing`` / ``parsed``。
仅适用单进程;多 worker 部署需替换为跨进程任务队列。
"""
import asyncio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.ai_provider import ProviderManager, ProviderManagerFactory
from app.core.logging import logger
from app.db.session import AsyncSessionLocal
from app.models.candidates import MemoryCandidate, ProfileCandidate
from app.models.imports import CharacterImportJob, ImportItem
from app.models.personality import PersonalityProposal
from app.modules.imports.extract import (
parse_memory_candidates,
parse_personality_proposals,
parse_profile_candidates,
)
from app.modules.imports.prompts import (
build_memory_extraction_messages,
build_personality_extraction_messages,
build_profile_extraction_messages,
)
from app.repositories import candidates as candidates_repo
from app.repositories import imports as imports_repo
from app.repositories.json_utils import dumps
# 后台抽取任务强引用:asyncio.create_task 仅保留弱引用,需强引用防 GC。
_running_extraction_tasks: set[asyncio.Task[None]] = set()
_provider_manager: ProviderManager | None = None
def _get_provider_manager() -> ProviderManager:
global _provider_manager
if _provider_manager is None:
_provider_manager = ProviderManagerFactory.create()
return _provider_manager
_PROFILE_MODEL_HINT = "personality-analysis"
_MEMORY_MODEL_HINT = "memory-extraction"
_PERSONALITY_MODEL_HINT = "personality-analysis"
async def drain_pending_extractions() -> None:
"""等待所有进行中的后台抽取任务完成(测试断言 / 优雅停机用)。"""
if _running_extraction_tasks:
await asyncio.gather(*_running_extraction_tasks, return_exceptions=True)
def start_profile_extraction(job_id: str) -> None:
task = asyncio.create_task(_run_profile_extraction(job_id))
_running_extraction_tasks.add(task)
task.add_done_callback(_running_extraction_tasks.discard)
def start_memory_extraction(job_id: str) -> None:
task = asyncio.create_task(_run_memory_extraction(job_id))
_running_extraction_tasks.add(task)
task.add_done_callback(_running_extraction_tasks.discard)
def start_personality_extraction(job_id: str) -> None:
task = asyncio.create_task(_run_personality_extraction(job_id))
_running_extraction_tasks.add(task)
task.add_done_callback(_running_extraction_tasks.discard)
async def _load_job_and_items(
session: AsyncSession, job_id: str
) -> tuple[CharacterImportJob | None, list[ImportItem]]:
job = await imports_repo.get_job(session, job_id)
if job is None:
return None, []
result = await session.execute(
select(ImportItem)
.where(ImportItem.import_job_id == job_id)
.order_by(ImportItem.sequence.asc())
)
return job, list(result.scalars().all())
async def _run_profile_extraction(job_id: str) -> None:
async with AsyncSessionLocal() as session:
try:
job, items = await _load_job_and_items(session, job_id)
if job is None:
return
system, user = build_profile_extraction_messages(_pairs(items))
raw = await _call_model(system, user, _PROFILE_MODEL_HINT, job.character_id)
candidates = [
ProfileCandidate(
character_id=job.character_id,
import_job_id=job.id,
field=draft.field_name,
value=draft.value,
confidence=draft.confidence,
reason=draft.reason,
status="pending",
evidence_ids_json=dumps(_evidence_ids(items, draft.evidence)),
)
for draft in parse_profile_candidates(raw)
]
await candidates_repo.add_profile_candidates(session, candidates)
job.status = "reviewing" if candidates else "parsed"
await session.commit()
except Exception as exc:
await _fail_job(session, job_id, exc)
async def _run_memory_extraction(job_id: str) -> None:
async with AsyncSessionLocal() as session:
try:
job, items = await _load_job_and_items(session, job_id)
if job is None:
return
system, user = build_memory_extraction_messages(_pairs(items))
raw = await _call_model(system, user, _MEMORY_MODEL_HINT, job.character_id)
candidates = [
MemoryCandidate(
character_id=job.character_id,
import_job_id=job.id,
type=draft.type,
title=draft.title,
content=draft.content,
confidence=0.6,
importance=draft.importance,
sensitivity=draft.sensitivity,
reason=draft.reason,
status="pending",
evidence_ids_json=dumps(_evidence_ids(items, draft.evidence)),
)
for draft in parse_memory_candidates(raw)
]
await candidates_repo.add_memory_candidates(session, candidates)
job.status = "reviewing" if candidates else "parsed"
await session.commit()
except Exception as exc:
await _fail_job(session, job_id, exc)
async def _run_personality_extraction(job_id: str) -> None:
async with AsyncSessionLocal() as session:
try:
job, items = await _load_job_and_items(session, job_id)
if job is None:
return
system, user = build_personality_extraction_messages(_pairs(items), "")
raw = await _call_model(system, user, _PERSONALITY_MODEL_HINT, job.character_id)
proposals = [
PersonalityProposal(
character_id=job.character_id,
fields_json=dumps({draft.field_name: draft.change}),
reason=draft.reason or "资料导入抽取的人格完善建议",
status="pending",
)
for draft in parse_personality_proposals(raw)
]
session.add_all(proposals)
await session.flush()
job.status = "reviewing" if proposals else "parsed"
await session.commit()
except Exception as exc:
await _fail_job(session, job_id, exc)
def _pairs(items: list[ImportItem]) -> list[tuple[str, str]]:
return [(item.role, item.content) for item in items]
def _evidence_ids(items: list[ImportItem], indices: list[int]) -> list[str]:
return [items[i].id for i in indices if 0 <= i < len(items)]
async def _call_model(system: str, user: str, model_hint: str, _character_id: str) -> str:
manager = _get_provider_manager()
messages = manager.to_message_list([system], [("user", user)])
response = await manager.chat(messages, model_hint=model_hint)
return response.content
async def _fail_job(session: AsyncSession, job_id: str, exc: Exception) -> None:
logger.error(f"导入候选抽取失败 job={job_id}: {exc}")
try:
job = await imports_repo.get_job(session, job_id)
if job is not None:
job.status = "failed"
job.error_code = "extraction_failed"
job.error_message = str(exc)[:500]
await session.commit()
except Exception: # noqa: BLE001 — 失败处理本身不能再抛
pass