mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-07-09 22:04:21 +00:00
First commit
This commit is contained in:
282
evals/offline/evaluator/persistence/oracle_store.py
Normal file
282
evals/offline/evaluator/persistence/oracle_store.py
Normal file
@@ -0,0 +1,282 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _json_dumps(value: Any) -> str:
|
||||
return json.dumps(value or {}, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def _json_loads(value: str | bytes | None, default: Any):
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8")
|
||||
try:
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
@dataclass
|
||||
class OracleSettings:
|
||||
user: str
|
||||
password: str
|
||||
dsn: str
|
||||
wallet_location: str | None = None
|
||||
wallet_password: str | None = None
|
||||
table_prefix: str = "AGENTFW"
|
||||
|
||||
|
||||
class OracleStore:
|
||||
"""Oracle Autonomous Database store following the Agent Framework pattern.
|
||||
|
||||
Uses direct oracledb.connect() with wallet arguments. Synchronous DB calls are
|
||||
exposed through asyncio.to_thread(), matching the main framework style.
|
||||
"""
|
||||
|
||||
def __init__(self, settings, auto_init_schema: bool = False):
|
||||
self.settings = settings
|
||||
self.cfg = OracleSettings(
|
||||
user=settings.ADB_USER or "",
|
||||
password=settings.ADB_PASSWORD or "",
|
||||
dsn=settings.ADB_DSN or "",
|
||||
wallet_location=getattr(settings, "ADB_WALLET_LOCATION", None),
|
||||
wallet_password=getattr(settings, "ADB_WALLET_PASSWORD", None),
|
||||
table_prefix=(getattr(settings, "ADB_TABLE_PREFIX", "AGENTFW") or "AGENTFW").upper().rstrip("_"),
|
||||
)
|
||||
if not self.cfg.user or not self.cfg.password or not self.cfg.dsn:
|
||||
raise RuntimeError("ADB_USER, ADB_PASSWORD and ADB_DSN are required")
|
||||
if auto_init_schema:
|
||||
self._init_schema()
|
||||
|
||||
@staticmethod
|
||||
def now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
def t(self, name: str) -> str:
|
||||
return f"{self.cfg.table_prefix}_{name}".upper()
|
||||
|
||||
@contextmanager
|
||||
def connect(self):
|
||||
import oracledb
|
||||
oracledb.defaults.fetch_lobs = False
|
||||
kwargs = {}
|
||||
if self.cfg.wallet_location:
|
||||
kwargs["config_dir"] = self.cfg.wallet_location
|
||||
kwargs["wallet_location"] = self.cfg.wallet_location
|
||||
if self.cfg.wallet_password:
|
||||
kwargs["wallet_password"] = self.cfg.wallet_password
|
||||
conn = oracledb.connect(user=self.cfg.user, password=self.cfg.password, dsn=self.cfg.dsn, **kwargs)
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
async def to_thread(self, func, *args, **kwargs):
|
||||
return await asyncio.to_thread(func, *args, **kwargs)
|
||||
|
||||
def _exec_ddl_ignore_exists(self, cur, ddl: str):
|
||||
try:
|
||||
cur.execute(ddl)
|
||||
except Exception as exc:
|
||||
msg = str(exc)
|
||||
if "ORA-00955" in msg or "ORA-01408" in msg or "ORA-02275" in msg:
|
||||
return
|
||||
raise
|
||||
|
||||
def _column_exists(self, cur, table_name: str, column_name: str) -> bool:
|
||||
cur.execute("""
|
||||
select count(*)
|
||||
from user_tab_columns
|
||||
where table_name = :table_name
|
||||
and column_name = :column_name
|
||||
""", {"table_name": self.t(table_name), "column_name": column_name.upper()})
|
||||
return int(cur.fetchone()[0] or 0) > 0
|
||||
|
||||
def _ensure_column(self, cur, table_name: str, column_name: str, ddl_type: str):
|
||||
if not self._column_exists(cur, table_name, column_name):
|
||||
cur.execute(f"alter table {self.t(table_name)} add {column_name.upper()} {ddl_type}")
|
||||
|
||||
def drop_schema(self):
|
||||
tables = [
|
||||
"EVALUATION_RESULT",
|
||||
"EVALUATION_FINDING",
|
||||
"EVALUATION_METRIC",
|
||||
"EVALUATION_ITEM",
|
||||
"EVALUATION_PROGRESS_EVENT",
|
||||
"EVALUATION_RUN",
|
||||
]
|
||||
with self.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
for table in tables:
|
||||
try:
|
||||
cur.execute(f"drop table {self.t(table)} cascade constraints purge")
|
||||
except Exception as exc:
|
||||
if "ORA-00942" in str(exc):
|
||||
continue
|
||||
raise
|
||||
|
||||
def _init_schema(self):
|
||||
with self.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
self._exec_ddl_ignore_exists(cur, f"""
|
||||
create table {self.t('EVALUATION_RUN')} (
|
||||
RUN_ID varchar2(64) primary key,
|
||||
AGENT_ID varchar2(128),
|
||||
SOURCE varchar2(64),
|
||||
PERIOD_START timestamp with time zone,
|
||||
PERIOD_END timestamp with time zone,
|
||||
STATUS varchar2(32) not null,
|
||||
TOTAL_ITEMS number default 0 not null,
|
||||
PROCESSED_ITEMS number default 0 not null,
|
||||
FAILED_ITEMS number default 0 not null,
|
||||
RETRY_COUNT number default 0 not null,
|
||||
ERROR_MESSAGE clob,
|
||||
LAST_HEARTBEAT_AT timestamp with time zone,
|
||||
CREATED_AT timestamp with time zone not null,
|
||||
UPDATED_AT timestamp with time zone not null
|
||||
)
|
||||
""")
|
||||
|
||||
self._exec_ddl_ignore_exists(cur, f"""
|
||||
create table {self.t('EVALUATION_PROGRESS_EVENT')} (
|
||||
ID number generated always as identity primary key,
|
||||
RUN_ID varchar2(64) not null,
|
||||
STAGE varchar2(128) not null,
|
||||
MESSAGE varchar2(1000),
|
||||
DETAILS_JSON clob check (DETAILS_JSON is json),
|
||||
CREATED_AT timestamp with time zone not null,
|
||||
constraint {self.t('FK_EVAL_PROGRESS_RUN')}
|
||||
foreign key (RUN_ID) references {self.t('EVALUATION_RUN')}(RUN_ID)
|
||||
)
|
||||
""")
|
||||
self._exec_ddl_ignore_exists(cur, f"create index {self.t('IX_EVAL_PROGRESS_RUN')} on {self.t('EVALUATION_PROGRESS_EVENT')}(RUN_ID, CREATED_AT)")
|
||||
|
||||
self._exec_ddl_ignore_exists(cur, f"""
|
||||
create table {self.t('EVALUATION_ITEM')} (
|
||||
ITEM_ID varchar2(64) primary key,
|
||||
RUN_ID varchar2(64) not null,
|
||||
TRACE_ID varchar2(256),
|
||||
SESSION_ID varchar2(256),
|
||||
MESSAGE_ID varchar2(256),
|
||||
AGENT_ID varchar2(128),
|
||||
CHANNEL varchar2(64),
|
||||
STATUS varchar2(32) not null,
|
||||
ATTEMPT_COUNT number default 0 not null,
|
||||
ERROR_MESSAGE clob,
|
||||
RAW_JSON clob check (RAW_JSON is json),
|
||||
CREATED_AT timestamp with time zone not null,
|
||||
UPDATED_AT timestamp with time zone not null,
|
||||
constraint {self.t('FK_EVAL_ITEM_RUN')}
|
||||
foreign key (RUN_ID) references {self.t('EVALUATION_RUN')}(RUN_ID)
|
||||
)
|
||||
""")
|
||||
self._exec_ddl_ignore_exists(cur, f"create index {self.t('IX_EVAL_ITEM_RUN')} on {self.t('EVALUATION_ITEM')}(RUN_ID, STATUS, CREATED_AT)")
|
||||
|
||||
self._exec_ddl_ignore_exists(cur, f"""
|
||||
create table {self.t('EVALUATION_RESULT')} (
|
||||
RESULT_ID varchar2(64) primary key,
|
||||
RUN_ID varchar2(64) not null,
|
||||
ITEM_ID varchar2(64),
|
||||
TRACE_ID varchar2(256),
|
||||
SESSION_ID varchar2(256),
|
||||
AGENT_ID varchar2(128),
|
||||
JUDGE_NAME varchar2(128) not null,
|
||||
JUDGE_TYPE varchar2(32),
|
||||
SCORE number,
|
||||
JUDGE_SCORE number,
|
||||
ACCURACY_SCORE number,
|
||||
ALUCINATION_SCORE number,
|
||||
HALLUCINATION_SCORE number,
|
||||
INFERRED_CSI_SCORE number,
|
||||
RESOLUTION number,
|
||||
CONVERSATION_PRECISION number,
|
||||
TOOL_USAGE_SCORE number,
|
||||
ROUTING_SCORE number,
|
||||
RATIONALE clob,
|
||||
REASONING clob,
|
||||
RESULT_JSON clob check (RESULT_JSON is json),
|
||||
CREATED_AT timestamp with time zone not null,
|
||||
constraint {self.t('FK_EVAL_RESULT_RUN')}
|
||||
foreign key (RUN_ID) references {self.t('EVALUATION_RUN')}(RUN_ID),
|
||||
constraint {self.t('FK_EVAL_RESULT_ITEM')}
|
||||
foreign key (ITEM_ID) references {self.t('EVALUATION_ITEM')}(ITEM_ID)
|
||||
)
|
||||
""")
|
||||
self._exec_ddl_ignore_exists(cur, f"create index {self.t('IX_EVAL_RESULT_RUN')} on {self.t('EVALUATION_RESULT')}(RUN_ID, ITEM_ID)")
|
||||
|
||||
self._exec_ddl_ignore_exists(cur, f"""
|
||||
create table {self.t('EVALUATION_METRIC')} (
|
||||
METRIC_ID varchar2(64) primary key,
|
||||
RUN_ID varchar2(64) not null,
|
||||
ITEM_ID varchar2(64),
|
||||
METRIC_NAME varchar2(128) not null,
|
||||
METRIC_VALUE number,
|
||||
DIMENSIONS_JSON clob check (DIMENSIONS_JSON is json),
|
||||
CREATED_AT timestamp with time zone not null,
|
||||
constraint {self.t('FK_EVAL_METRIC_RUN')}
|
||||
foreign key (RUN_ID) references {self.t('EVALUATION_RUN')}(RUN_ID)
|
||||
)
|
||||
""")
|
||||
|
||||
self._exec_ddl_ignore_exists(cur, f"""
|
||||
create table {self.t('EVALUATION_FINDING')} (
|
||||
FINDING_ID varchar2(64) primary key,
|
||||
RUN_ID varchar2(64) not null,
|
||||
ITEM_ID varchar2(64),
|
||||
SEVERITY varchar2(32),
|
||||
CATEGORY varchar2(128),
|
||||
TITLE varchar2(512),
|
||||
DESCRIPTION clob,
|
||||
EVIDENCE_JSON clob check (EVIDENCE_JSON is json),
|
||||
CREATED_AT timestamp with time zone not null,
|
||||
constraint {self.t('FK_EVAL_FINDING_RUN')}
|
||||
foreign key (RUN_ID) references {self.t('EVALUATION_RUN')}(RUN_ID)
|
||||
)
|
||||
""")
|
||||
|
||||
# Non-destructive compatibility for older generated schemas.
|
||||
for col, typ in [
|
||||
("RETRY_COUNT", "number default 0"),
|
||||
("ERROR_MESSAGE", "clob"),
|
||||
("LAST_HEARTBEAT_AT", "timestamp with time zone"),
|
||||
("UPDATED_AT", "timestamp with time zone"),
|
||||
]:
|
||||
self._ensure_column(cur, "EVALUATION_RUN", col, typ)
|
||||
for col, typ in [
|
||||
("ID", "number generated always as identity"),
|
||||
]:
|
||||
# Identity column cannot always be added cleanly; ignore if table is old without ID.
|
||||
try:
|
||||
self._ensure_column(cur, "EVALUATION_PROGRESS_EVENT", col, typ)
|
||||
except Exception:
|
||||
pass
|
||||
for col, typ in [
|
||||
("JUDGE_NAME", "varchar2(128) default 'unknown_judge' not null"),
|
||||
("JUDGE_TYPE", "varchar2(32)"),
|
||||
("SCORE", "number"),
|
||||
("JUDGE_SCORE", "number"),
|
||||
("ACCURACY_SCORE", "number"),
|
||||
("ALUCINATION_SCORE", "number"),
|
||||
("HALLUCINATION_SCORE", "number"),
|
||||
("INFERRED_CSI_SCORE", "number"),
|
||||
("RESOLUTION", "number"),
|
||||
("CONVERSATION_PRECISION", "number"),
|
||||
("TOOL_USAGE_SCORE", "number"),
|
||||
("ROUTING_SCORE", "number"),
|
||||
("RATIONALE", "clob"),
|
||||
("REASONING", "clob"),
|
||||
("RESULT_JSON", "clob"),
|
||||
]:
|
||||
self._ensure_column(cur, "EVALUATION_RESULT", col, typ)
|
||||
410
evals/offline/evaluator/persistence/repository.py
Normal file
410
evals/offline/evaluator/persistence/repository.py
Normal file
@@ -0,0 +1,410 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from evaluator.config.settings import settings
|
||||
from evaluator.core.models import ConversationRecord, ItemStatus, RunStatus, TraceJudgeResult, SessionJudgeResult
|
||||
from evaluator.persistence.oracle_store import OracleStore, _json_dumps, _json_loads
|
||||
|
||||
|
||||
class EvaluationRepository:
|
||||
def __init__(self, auto_init_schema: bool = False):
|
||||
self.store = OracleStore(settings, auto_init_schema=auto_init_schema)
|
||||
|
||||
def create_run(self, period_start: datetime, period_end: datetime, source: str, agent_id: str | None = None) -> str:
|
||||
run_id = str(uuid.uuid4())
|
||||
now = self.store.now()
|
||||
with self.store.connect() as conn:
|
||||
conn.cursor().execute(f"""
|
||||
insert into {self.store.t('EVALUATION_RUN')}
|
||||
(RUN_ID, AGENT_ID, PERIOD_START, PERIOD_END, SOURCE, STATUS, TOTAL_ITEMS,
|
||||
PROCESSED_ITEMS, FAILED_ITEMS, RETRY_COUNT, LAST_HEARTBEAT_AT, CREATED_AT, UPDATED_AT)
|
||||
values (:run_id, :agent_id, :period_start, :period_end, :source, :status,
|
||||
0, 0, 0, 0, :heartbeat_at, :created_at, :updated_at)
|
||||
""", {
|
||||
"run_id": run_id,
|
||||
"agent_id": agent_id,
|
||||
"period_start": period_start,
|
||||
"period_end": period_end,
|
||||
"source": source,
|
||||
"status": RunStatus.RUNNING.value,
|
||||
"heartbeat_at": now,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
})
|
||||
return run_id
|
||||
|
||||
async def acreate_run(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.create_run, *args, **kwargs)
|
||||
|
||||
def record_progress(self, run_id: str, stage: str, message: str = "", details: dict | None = None):
|
||||
with self.store.connect() as conn:
|
||||
conn.cursor().execute(f"""
|
||||
insert into {self.store.t('EVALUATION_PROGRESS_EVENT')}
|
||||
(RUN_ID, STAGE, MESSAGE, DETAILS_JSON, CREATED_AT)
|
||||
values (:run_id, :stage, :message, :details_json, :created_at)
|
||||
""", {
|
||||
"run_id": run_id,
|
||||
"stage": stage,
|
||||
"message": (message or "")[:1000],
|
||||
"details_json": _json_dumps(details or {}),
|
||||
"created_at": self.store.now(),
|
||||
})
|
||||
|
||||
async def arecord_progress(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.record_progress, *args, **kwargs)
|
||||
|
||||
def insert_items(self, run_id: str, records: list[ConversationRecord]) -> int:
|
||||
inserted = 0
|
||||
now = self.store.now()
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
for record in records:
|
||||
try:
|
||||
cur.execute(f"""
|
||||
insert into {self.store.t('EVALUATION_ITEM')}
|
||||
(ITEM_ID, RUN_ID, TRACE_ID, SESSION_ID, MESSAGE_ID, AGENT_ID, CHANNEL,
|
||||
STATUS, ATTEMPT_COUNT, RAW_JSON, CREATED_AT, UPDATED_AT)
|
||||
values (:item_id, :run_id, :trace_id, :session_id, :message_id, :agent_id,
|
||||
:channel, :status, 0, :raw_json, :created_at, :updated_at)
|
||||
""", {
|
||||
"item_id": str(uuid.uuid4()),
|
||||
"run_id": run_id,
|
||||
"trace_id": record.trace_id,
|
||||
"session_id": record.session_id,
|
||||
"message_id": record.message_id,
|
||||
"agent_id": record.agent_id,
|
||||
"channel": record.channel,
|
||||
"status": ItemStatus.PENDING.value,
|
||||
"raw_json": record.model_dump_json(),
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
})
|
||||
inserted += 1
|
||||
except Exception as exc:
|
||||
if "ORA-00001" not in str(exc):
|
||||
raise
|
||||
cur.execute(f"""
|
||||
update {self.store.t('EVALUATION_RUN')}
|
||||
set TOTAL_ITEMS = (
|
||||
select count(*) from {self.store.t('EVALUATION_ITEM')} where RUN_ID = :run_id
|
||||
),
|
||||
UPDATED_AT = :updated_at
|
||||
where RUN_ID = :run_id
|
||||
""", {"run_id": run_id, "updated_at": self.store.now()})
|
||||
return inserted
|
||||
|
||||
async def ainsert_items(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.insert_items, *args, **kwargs)
|
||||
|
||||
def fetch_next_items(self, run_id: str, batch_size: int) -> list[dict]:
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""
|
||||
select * from (
|
||||
select ITEM_ID, RUN_ID, TRACE_ID, SESSION_ID, MESSAGE_ID, AGENT_ID, CHANNEL,
|
||||
STATUS, ATTEMPT_COUNT, RAW_JSON
|
||||
from {self.store.t('EVALUATION_ITEM')}
|
||||
where RUN_ID = :run_id
|
||||
and STATUS in (:pending, :failed)
|
||||
and ATTEMPT_COUNT < :max_attempts
|
||||
order by CREATED_AT
|
||||
) where rownum <= :batch_size
|
||||
""", {
|
||||
"run_id": run_id,
|
||||
"pending": ItemStatus.PENDING.value,
|
||||
"failed": ItemStatus.FAILED.value,
|
||||
"max_attempts": settings.max_attempts,
|
||||
"batch_size": batch_size,
|
||||
})
|
||||
cols = [d[0].lower() for d in cur.description]
|
||||
return [dict(zip(cols, row)) for row in cur.fetchall()]
|
||||
|
||||
async def afetch_next_items(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.fetch_next_items, *args, **kwargs)
|
||||
|
||||
def mark_item_processing(self, item_id: str):
|
||||
with self.store.connect() as conn:
|
||||
conn.cursor().execute(f"""
|
||||
update {self.store.t('EVALUATION_ITEM')}
|
||||
set STATUS = :status,
|
||||
ATTEMPT_COUNT = ATTEMPT_COUNT + 1,
|
||||
UPDATED_AT = :updated_at
|
||||
where ITEM_ID = :item_id
|
||||
""", {
|
||||
"status": ItemStatus.PROCESSING.value,
|
||||
"updated_at": self.store.now(),
|
||||
"item_id": item_id,
|
||||
})
|
||||
|
||||
async def amark_item_processing(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.mark_item_processing, *args, **kwargs)
|
||||
|
||||
def mark_item_completed(self, run_id: str, item_id: str):
|
||||
now = self.store.now()
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""
|
||||
update {self.store.t('EVALUATION_ITEM')}
|
||||
set STATUS = :status,
|
||||
UPDATED_AT = :updated_at
|
||||
where ITEM_ID = :item_id
|
||||
""", {
|
||||
"status": ItemStatus.COMPLETED.value,
|
||||
"updated_at": now,
|
||||
"item_id": item_id,
|
||||
})
|
||||
self._refresh_run_counters(cur, run_id, now)
|
||||
|
||||
async def amark_item_completed(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.mark_item_completed, *args, **kwargs)
|
||||
|
||||
def mark_item_failed(self, run_id: str, item_id: str, error: str):
|
||||
now = self.store.now()
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""
|
||||
update {self.store.t('EVALUATION_ITEM')}
|
||||
set STATUS = :status,
|
||||
ERROR_MESSAGE = :error,
|
||||
UPDATED_AT = :updated_at
|
||||
where ITEM_ID = :item_id
|
||||
""", {
|
||||
"status": ItemStatus.FAILED.value,
|
||||
"error": (error or "")[:4000],
|
||||
"updated_at": now,
|
||||
"item_id": item_id,
|
||||
})
|
||||
self._refresh_run_counters(cur, run_id, now)
|
||||
|
||||
async def amark_item_failed(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.mark_item_failed, *args, **kwargs)
|
||||
|
||||
def _refresh_run_counters(self, cur, run_id: str, updated_at):
|
||||
cur.execute(f"""
|
||||
update {self.store.t('EVALUATION_RUN')}
|
||||
set PROCESSED_ITEMS = (
|
||||
select count(*) from {self.store.t('EVALUATION_ITEM')}
|
||||
where RUN_ID = :run_id and STATUS = :completed
|
||||
),
|
||||
FAILED_ITEMS = (
|
||||
select count(*) from {self.store.t('EVALUATION_ITEM')}
|
||||
where RUN_ID = :run_id and STATUS = :failed
|
||||
),
|
||||
UPDATED_AT = :updated_at
|
||||
where RUN_ID = :run_id
|
||||
""", {
|
||||
"run_id": run_id,
|
||||
"completed": ItemStatus.COMPLETED.value,
|
||||
"failed": ItemStatus.FAILED.value,
|
||||
"updated_at": updated_at,
|
||||
})
|
||||
|
||||
def save_trace_result(self, run_id: str, item_id: str, record: ConversationRecord, result: TraceJudgeResult):
|
||||
judge_name = getattr(result, "judge_name", None) or "trace_metrics"
|
||||
judge_type = (getattr(result, "judge_type", None) or "TRACE").upper()
|
||||
score = getattr(result, "judgeScore", None)
|
||||
accuracy = getattr(result, "accuracyScore", None)
|
||||
alucination = getattr(result, "alucinationScore", None)
|
||||
rationale = getattr(result, "rationale", None) or ""
|
||||
with self.store.connect() as conn:
|
||||
conn.cursor().execute(f"""
|
||||
insert into {self.store.t('EVALUATION_RESULT')}
|
||||
(RESULT_ID, RUN_ID, ITEM_ID, TRACE_ID, SESSION_ID, AGENT_ID, JUDGE_NAME,
|
||||
JUDGE_TYPE, SCORE, JUDGE_SCORE, ACCURACY_SCORE, ALUCINATION_SCORE,
|
||||
RATIONALE, RESULT_JSON, CREATED_AT)
|
||||
values (:result_id, :run_id, :item_id, :trace_id, :session_id, :agent_id,
|
||||
:judge_name, :judge_type, :score, :judge_score, :accuracy_score,
|
||||
:alucination_score, :rationale, :result_json, :created_at)
|
||||
""", {
|
||||
"result_id": str(uuid.uuid4()),
|
||||
"run_id": run_id,
|
||||
"item_id": item_id,
|
||||
"trace_id": record.trace_id,
|
||||
"session_id": record.session_id,
|
||||
"agent_id": record.agent_id,
|
||||
"judge_name": judge_name,
|
||||
"judge_type": judge_type,
|
||||
"score": score,
|
||||
"judge_score": score,
|
||||
"accuracy_score": accuracy,
|
||||
"alucination_score": alucination,
|
||||
"rationale": rationale,
|
||||
"result_json": result.model_dump_json(),
|
||||
"created_at": self.store.now(),
|
||||
})
|
||||
|
||||
async def asave_trace_result(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.save_trace_result, *args, **kwargs)
|
||||
|
||||
def save_session_result(self, run_id: str, session_id: str, agent_id: str | None, result: SessionJudgeResult):
|
||||
judge_name = getattr(result, "judge_name", None) or "session_metrics"
|
||||
judge_type = (getattr(result, "judge_type", None) or "SESSION").upper()
|
||||
rationale = getattr(result, "rationale", None) or ""
|
||||
with self.store.connect() as conn:
|
||||
conn.cursor().execute(f"""
|
||||
insert into {self.store.t('EVALUATION_RESULT')}
|
||||
(RESULT_ID, RUN_ID, SESSION_ID, AGENT_ID, JUDGE_NAME, JUDGE_TYPE,
|
||||
INFERRED_CSI_SCORE, RESOLUTION, CONVERSATION_PRECISION, RATIONALE,
|
||||
RESULT_JSON, CREATED_AT)
|
||||
values (:result_id, :run_id, :session_id, :agent_id, :judge_name, :judge_type,
|
||||
:csi, :resolution, :precision, :rationale, :result_json, :created_at)
|
||||
""", {
|
||||
"result_id": str(uuid.uuid4()),
|
||||
"run_id": run_id,
|
||||
"session_id": session_id,
|
||||
"agent_id": agent_id,
|
||||
"judge_name": judge_name,
|
||||
"judge_type": judge_type,
|
||||
"csi": getattr(result, "inferredCsiScore", None),
|
||||
"resolution": getattr(result, "resolution", None),
|
||||
"precision": getattr(result, "conversationPrecision", None),
|
||||
"rationale": rationale,
|
||||
"result_json": result.model_dump_json(),
|
||||
"created_at": self.store.now(),
|
||||
})
|
||||
|
||||
async def asave_session_result(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.save_session_result, *args, **kwargs)
|
||||
|
||||
def mark_run_status(self, run_id: str, status: RunStatus, error: str | None = None):
|
||||
with self.store.connect() as conn:
|
||||
conn.cursor().execute(f"""
|
||||
update {self.store.t('EVALUATION_RUN')}
|
||||
set STATUS = :status,
|
||||
ERROR_MESSAGE = :error,
|
||||
UPDATED_AT = :updated_at
|
||||
where RUN_ID = :run_id
|
||||
""", {
|
||||
"status": status.value,
|
||||
"error": error,
|
||||
"updated_at": self.store.now(),
|
||||
"run_id": run_id,
|
||||
})
|
||||
|
||||
async def amark_run_status(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.mark_run_status, *args, **kwargs)
|
||||
|
||||
def summarize_run(self, run_id: str) -> dict:
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""
|
||||
select
|
||||
(select count(*) from {self.store.t('EVALUATION_ITEM')} where RUN_ID = :run_id),
|
||||
(select count(*) from {self.store.t('EVALUATION_ITEM')} where RUN_ID = :run_id and STATUS = 'COMPLETED'),
|
||||
(select count(*) from {self.store.t('EVALUATION_ITEM')} where RUN_ID = :run_id and STATUS = 'FAILED'),
|
||||
(select count(*) from {self.store.t('EVALUATION_RESULT')} where RUN_ID = :run_id and JUDGE_TYPE = 'TRACE'),
|
||||
(select avg(JUDGE_SCORE) from {self.store.t('EVALUATION_RESULT')} where RUN_ID = :run_id and JUDGE_TYPE = 'TRACE')
|
||||
from dual
|
||||
""", {"run_id": run_id})
|
||||
r = cur.fetchone()
|
||||
return {
|
||||
"run_id": run_id,
|
||||
"total_items": int(r[0] or 0),
|
||||
"completed_items": int(r[1] or 0),
|
||||
"failed_items": int(r[2] or 0),
|
||||
"evaluations": int(r[3] or 0),
|
||||
"avg_score": float(r[4]) if r[4] is not None else None,
|
||||
}
|
||||
|
||||
async def asummarize_run(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.summarize_run, *args, **kwargs)
|
||||
|
||||
def get_run_progress(self, run_id: str, event_limit: int = 20) -> dict:
|
||||
summary = self.summarize_run(run_id)
|
||||
total = summary["total_items"] or 0
|
||||
done = summary["completed_items"] + summary["failed_items"]
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""
|
||||
select * from (
|
||||
select STAGE, MESSAGE, DETAILS_JSON, CREATED_AT
|
||||
from {self.store.t('EVALUATION_PROGRESS_EVENT')}
|
||||
where RUN_ID = :run_id
|
||||
order by CREATED_AT desc
|
||||
) where rownum <= :max_rows
|
||||
""", {"run_id": run_id, "max_rows": event_limit})
|
||||
events = [
|
||||
{
|
||||
"stage": s,
|
||||
"message": m,
|
||||
"details": _json_loads(d.read() if hasattr(d, "read") else d, {}),
|
||||
"created_at": str(c),
|
||||
}
|
||||
for s, m, d, c in cur.fetchall()
|
||||
]
|
||||
return {
|
||||
**summary,
|
||||
"done_items": done,
|
||||
"percent_complete": round((done / total) * 100, 2) if total else 0.0,
|
||||
"events": events,
|
||||
}
|
||||
|
||||
async def aget_run_progress(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.get_run_progress, *args, **kwargs)
|
||||
|
||||
def list_runs(self, limit: int = 20):
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""
|
||||
select * from (
|
||||
select RUN_ID, AGENT_ID, PERIOD_START, PERIOD_END, SOURCE, STATUS,
|
||||
TOTAL_ITEMS, PROCESSED_ITEMS, FAILED_ITEMS, CREATED_AT, UPDATED_AT
|
||||
from {self.store.t('EVALUATION_RUN')}
|
||||
order by CREATED_AT desc
|
||||
) where rownum <= :max_rows
|
||||
""", {"max_rows": limit})
|
||||
return [
|
||||
{
|
||||
"run_id": r[0],
|
||||
"agent_id": r[1],
|
||||
"period_start": str(r[2]),
|
||||
"period_end": str(r[3]),
|
||||
"source": r[4],
|
||||
"status": r[5],
|
||||
"total_items": int(r[6] or 0),
|
||||
"processed_items": int(r[7] or 0),
|
||||
"failed_items": int(r[8] or 0),
|
||||
"created_at": str(r[9]),
|
||||
"updated_at": str(r[10]),
|
||||
}
|
||||
for r in cur.fetchall()
|
||||
]
|
||||
|
||||
async def alist_runs(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.list_runs, *args, **kwargs)
|
||||
|
||||
def list_results(self, run_id: str, limit: int = 100) -> list[dict]:
|
||||
with self.store.connect() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(f"""
|
||||
select JUDGE_NAME, JUDGE_TYPE, SCORE, JUDGE_SCORE, ACCURACY_SCORE,
|
||||
ALUCINATION_SCORE, INFERRED_CSI_SCORE, RESOLUTION,
|
||||
CONVERSATION_PRECISION, RATIONALE, TRACE_ID, SESSION_ID, CREATED_AT
|
||||
from {self.store.t('EVALUATION_RESULT')}
|
||||
where RUN_ID = :run_id
|
||||
order by CREATED_AT desc
|
||||
""", {"run_id": run_id})
|
||||
return [
|
||||
{
|
||||
"judge_name": r[0],
|
||||
"judge_type": r[1],
|
||||
"score": r[2],
|
||||
"judge_score": r[3],
|
||||
"accuracy_score": r[4],
|
||||
"alucination_score": r[5],
|
||||
"inferred_csi_score": r[6],
|
||||
"resolution": r[7],
|
||||
"conversation_precision": r[8],
|
||||
"rationale": r[9],
|
||||
"trace_id": r[10],
|
||||
"session_id": r[11],
|
||||
"created_at": str(r[12]),
|
||||
}
|
||||
for r in cur.fetchall()[:limit]
|
||||
]
|
||||
|
||||
async def alist_results(self, *args, **kwargs):
|
||||
return await self.store.to_thread(self.list_results, *args, **kwargs)
|
||||
Reference in New Issue
Block a user