mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
目录按文档点到的表优先展开;模型提问不再算起草失败
这个数据源的目录渲染出来十万字符,被截到两万四,kpi_info 的 attribute01…54 全在被砍掉的 后半段,模型看不到列就只能如实列问题——界面却把它记成「起草失败」。 现在文本按「概览 → 全量表索引 → 全量列名 → 文档点到的表展开」排,预算不够先丢最后一段, 列名一个不少。模型一条指标都没写、但把缺什么说清楚时,草稿落在 questions_open,问题原样 留下来,不再报失败。
This commit is contained in:
@@ -439,11 +439,51 @@ def _source_tables(source: DataSource) -> dict[str, Any]:
|
||||
return tables if isinstance(tables, dict) else {}
|
||||
|
||||
|
||||
async def _schema_material(session: AsyncSession, source: DataSource) -> tuple[str, set[str]]:
|
||||
def _focus_tables(tables: list[str], text: str, limit: int = 8) -> list[str]:
|
||||
"""文档里点到的表优先展开:按它在原文里第一次出现的位置排,没点到的按目录顺序补几张。
|
||||
|
||||
目录动辄十万字符,全塞给模型会把文档和别的表挤掉。先给全量列名(见 _columns_digest),再把文档点到的
|
||||
那几张表展开注释和目录——写 SQL 需要的列名一个不少,细节只给用得上的。
|
||||
"""
|
||||
lowered = (text or "").lower()
|
||||
hit: list[tuple[int, str]] = []
|
||||
for table in tables:
|
||||
name = str(table).lower()
|
||||
short = name.split(".")[-1]
|
||||
at = lowered.find(name)
|
||||
if at < 0:
|
||||
at = lowered.find(short)
|
||||
if at >= 0:
|
||||
hit.append((at, table))
|
||||
hit.sort(key=lambda pair: pair[0])
|
||||
picked = [table for _, table in hit][:limit]
|
||||
for table in tables:
|
||||
if len(picked) >= min(limit, 5):
|
||||
break
|
||||
if table not in picked:
|
||||
picked.append(table)
|
||||
return picked
|
||||
|
||||
|
||||
def _columns_digest(source: DataSource, tables: list[str]) -> str:
|
||||
"""每张表的列名清单(只给名字):要写 SQL,先得知道有哪些列。注释和目录留给下面展开的那几张。"""
|
||||
from app.data import introspect
|
||||
|
||||
lines = ["各表的列(只列名字;下面展开的表另有类型、注释和数据目录):"]
|
||||
for table in tables:
|
||||
meta = introspect.find_table(source, table)
|
||||
columns = [str(c.get("name")) for c in (meta or {}).get("columns") or [] if c.get("name")]
|
||||
if columns:
|
||||
lines.append(f"{table}: {', '.join(columns)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _schema_material(session: AsyncSession, source: DataSource, focus_text: str = ""
|
||||
) -> tuple[str, set[str]]:
|
||||
"""这个数据源给模型看的表结构 + 数据目录文本,以及 SQL 允许出现的表名集合。
|
||||
|
||||
给模型的是「库概览 + 紧凑表名索引 + 前几十张表的字段/目录」(schema_view 的现成渲染),表名一律照目录原文,模型照着抄。
|
||||
allowed_tables 收全部探查到的表(含 schema 前缀写法):文本里被截掉详情的表也允许,因为索引里有名字。
|
||||
文本按「概览 → 全量表索引 → 全量列名 → 文档点到的表展开」的顺序排:预算不够时先丢的是最后那一段,
|
||||
列名不会丢。表名一律照目录原文,模型照着抄。
|
||||
"""
|
||||
from app.data import catalog, schema_view
|
||||
|
||||
@@ -456,9 +496,8 @@ async def _schema_material(session: AsyncSession, source: DataSource) -> tuple[s
|
||||
lines = [schema_view.overview_line(source, catalog_tables=sum(
|
||||
1 for entry in entries.values() if catalog.visible_notes(entry.notes)))]
|
||||
lines.append(schema_view.table_index(source, notes))
|
||||
for position, table in enumerate(tables):
|
||||
if position >= 60:
|
||||
break
|
||||
lines.append(_columns_digest(source, tables))
|
||||
for table in _focus_tables(tables, focus_text):
|
||||
lines.append(schema_view.table_detail(source, table, catalog_notes=notes))
|
||||
schema = cache.get("schema")
|
||||
allowed: set[str] = set()
|
||||
@@ -642,7 +681,8 @@ async def _extract_in_background(draft_id: str) -> None:
|
||||
draft.status = "failed"
|
||||
await session.commit()
|
||||
return
|
||||
tables_text, allowed_tables = await _schema_material(session, source)
|
||||
focus_text = "\n".join(str(c.get("content") or "") for c in chunks.values())
|
||||
tables_text, allowed_tables = await _schema_material(session, source, focus_text)
|
||||
if not allowed_tables:
|
||||
draft.error = "这个数据源还没有表结构:先探查表结构,再起草口径"
|
||||
draft.status = "failed"
|
||||
@@ -668,6 +708,14 @@ async def _extract_in_background(draft_id: str) -> None:
|
||||
error = _problems_text(problems) or "模型没有给出可用的取数 SQL 和指标"
|
||||
if result.get("diagnostics"):
|
||||
error = f"{error}({result['diagnostics']})"
|
||||
if plan["open_questions"]:
|
||||
# 模型把缺什么说清楚了(目录里没有这些列、业务上还没定):这是一份要补信息的草稿,不是失败
|
||||
draft.payload = {**base, "trial": {"run_id": "", "status": "", "error": ""}}
|
||||
draft.model_id = result["model_id"] or None
|
||||
draft.error = ""
|
||||
draft.status = "questions_open"
|
||||
await session.commit()
|
||||
return
|
||||
draft.payload = {**base, "trial": {"run_id": "", "status": "failed", "error": error}}
|
||||
draft.model_id = result["model_id"] or None
|
||||
draft.error = error
|
||||
|
||||
@@ -507,3 +507,28 @@ async def test_a_real_trial_run_feeds_the_trial_endpoint(client, documents, tmp_
|
||||
if source is not None:
|
||||
await session.delete(source)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def test_the_model_asking_questions_is_not_a_failure(client, documents, datasource, monkeypatch):
|
||||
"""模型说「目录里没有这些列、写不了 SQL」时,这是要补信息的草稿,不是失败:问题原样留下,不编指标。"""
|
||||
doc = await documents("dd-questions")
|
||||
source_id = await datasource()
|
||||
fake_model(monkeypatch, json.dumps({
|
||||
"entry_fields": [], "sql": "", "metrics": [],
|
||||
"open_questions": ["数据目录里没有 attribute01…54 的取值列,写不出时间类指标"],
|
||||
}, ensure_ascii=False))
|
||||
|
||||
body = await extract(client, doc, source_id)
|
||||
made = (await client.get(f"/api/ledger/drafts/{body['draft_id']}")).json()
|
||||
assert made["status"] == "questions_open" and made["error"] == ""
|
||||
assert made["open_questions"] == ["数据目录里没有 attribute01…54 的取值列,写不出时间类指标"]
|
||||
assert made["metrics"] == [] and made["workflow_id"] is None
|
||||
assert made["trial"]["run_id"] == ""
|
||||
|
||||
|
||||
def test_the_schema_text_expands_the_tables_the_document_names_first():
|
||||
"""目录十万字符、预算只有两万多:先展开文档点到的表,其余只靠列名清单认。"""
|
||||
tables = ["orders", "refunds", "stores", "kpi_info", "attr_info"]
|
||||
picked = api_ledger._focus_tables(tables, "指标取自 kpi_info,属性在 attr_info 里")
|
||||
assert picked[:2] == ["kpi_info", "attr_info"]
|
||||
assert set(picked) >= {"kpi_info", "attr_info"}
|
||||
|
||||
Reference in New Issue
Block a user