Files
PlantAI-rag-new/bioflow_service/app/context.py
T

214 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""app/context.py —— 生信服务共享实例容器(db/events/kernel/catalog/runner/semaphore/planner)。
- 生产:app 启动时 init_context() 加载真实依赖(SQLite、stage_image_map.json、
vstore FlowStore 本地实例(存在时)、真 docker 执行器)。
- 测试:build_context() 可注入 fake db/executor/flow_store(mock docker/LLM/vstore)。
- vstore 不存在/不可用:FlowStore 为 None,planner 自动降级 llm_only(FR-BIO-010 降级)。
- 并发信号量(BIOFLOW_CONCURRENCY,FR-BIO-031)每 context 一个。
"""
from __future__ import annotations
import asyncio
import logging
from dataclasses import dataclass, field
from typing import Any
from .config import get_settings
from .db import Database, db as default_db
from .services.bioflow.catalog import StageCatalog, build_catalog
from .services.bioflow.events import EventBus
from .services.bioflow.kernel import Kernel, KernelConfig, load_kernel
from .services.bioflow.planner import Planner
from .services.bioflow.runner import Runner
logger = logging.getLogger(__name__)
@dataclass
class Context:
db: Any = None
events: EventBus | None = None
kernel: Kernel | None = None
catalog: StageCatalog | None = None
runner: Runner | None = None
semaphore: asyncio.Semaphore | None = None
planner: Planner | None = None
flow_store: Any = None
executor: Any = None
records: dict[str, Any] = field(default_factory=dict) # job_id → JobRecord(内存)
_tasks: list[asyncio.Task] = field(default_factory=list)
def spawn(self, coro: Any) -> asyncio.Task:
task = asyncio.create_task(coro)
self._tasks.append(task)
task.add_done_callback(lambda t: self._tasks.remove(t) if t in self._tasks else None)
return task
async def close(self) -> None:
for task in self._tasks:
task.cancel()
if self._tasks:
await asyncio.gather(*self._tasks, return_exceptions=True)
self._tasks.clear()
# 全局 context(测试可整体替换)
context: Context = Context()
def init_events(jobs_dir: str) -> EventBus:
from pathlib import Path
return EventBus(Path(jobs_dir) / "events")
def init_flow_store(flow_docs_dir: str) -> Any | None:
"""vstore FlowStore 本地实例(存在则加载;失败/缺失返回 None → planner 降级)。"""
try:
import sys
from pathlib import Path
repo_root = Path(__file__).resolve().parent.parent.parent
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from vstore.flow import FlowStore # P6 A4 交付;缺失则降级
return FlowStore(flow_docs_dir)
except Exception as exc: # noqa: BLE001 - 缺失/损坏降级 llm_only,不阻断服务
logger.warning("context: vstore FlowStore 不可用(planner 降级 llm_only): %s", exc)
return None
def build_context(
*,
db: Database | None = None,
events: EventBus | None = None,
kernel: Kernel | None = None,
catalog: StageCatalog | None = None,
executor: Any = None,
semaphore: asyncio.Semaphore | None = None,
flow_store: Any = None,
runner_kwargs: dict[str, Any] | None = None,
) -> Context:
"""构造 context(生产与测试共用;各依赖可注入)。
executor=None 时用 kernel.run_stage(真 docker)。
"""
settings = get_settings()
database = db or default_db
bus = events or init_events(settings.jobs_dir_abs)
kern = kernel or load_kernel()
cat = catalog or build_catalog(kern.map)
sem = semaphore or asyncio.Semaphore(settings.concurrency)
exec_fn = executor or kern.run_stage
runner = Runner(
database,
bus,
exec_fn,
cat,
sem,
**(runner_kwargs or {}),
)
planner = Planner(
cat,
flow_store,
llm_plan_timeout=settings.llm_plan_timeout,
plan_candidates=settings.plan_candidates,
plan_max_rounds=settings.plan_max_rounds,
archive_finder=_make_archive_finder(database),
)
return Context(
db=database,
events=bus,
kernel=kern,
catalog=cat,
runner=runner,
semaphore=sem,
planner=planner,
flow_store=flow_store,
executor=exec_fn,
)
def _make_archive_finder(database: Database):
"""planner 复用归档查找回调(async;store 查询)。"""
from .services.bioflow import store
async def find(name: str) -> dict[str, Any] | None:
try:
return await store.find_archive_by_name(database, name)
except Exception: # noqa: BLE001
return None
return find
async def init_context(db_path: str | None = None) -> Context:
"""应用启动:初始化全局 context(幂等;db_path 供测试替换)。"""
global context
from .db import init_db
database = context.db
if db_path is not None:
database = await init_db(db_path)
elif context.db is None:
database = await init_db()
context = build_context(db=database)
return context
async def shutdown_context() -> None:
await context.close()
# ---- JobRecord 注册与恢复(路由层操作任务的内存记录)----
def register_record(record: Any) -> None:
context.records[record.id] = record
def _record_from_job(job: dict[str, Any], stages: list[dict[str, Any]]) -> Any:
"""DB 行 → JobRecord(无运行中内存记录时恢复,跨重启安全)。"""
from .services.bioflow.runner import JobRecord, params_dict
record = JobRecord(
id=job["id"],
session_id=job["session_id"],
title=job["title"],
status=job["status"],
stage_chain=list(job["stage_chain"] or []),
params=params_dict(job.get("params")),
auto_continue=bool(job.get("auto_continue")),
archive_id=job.get("archive_id"),
)
for s in stages:
record._stage_states[s["stage"]] = {
"status": s["status"],
"params": params_dict(s.get("params")),
"attempt": int(s.get("attempt") or 0),
"manifest_path": s.get("manifest_path"),
"log_tail": s.get("log_tail") or "",
"error": s.get("error"),
"started_at": s.get("started_at"),
"finished_at": s.get("finished_at"),
}
return record
async def load_record(job_id: str) -> Any | None:
"""取任务 JobRecord:内存优先,否则 DB 恢复(并注册回内存)。"""
if job_id in context.records:
return context.records[job_id]
if context.db is None:
return None
from .services.bioflow import store
job = await store.get_job(context.db, job_id)
if job is None:
return None
stages = await store.list_stages(context.db, job_id)
record = _record_from_job(job, stages)
context.records[job_id] = record
return record