mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 03:51:58 +00:00
助手按需查看:点名的一个都没认出来时(常见的是拿数据源说明里的中文词当表名),同一次查看按需求在那几个来源里挑,不让模型为拿真名再看一次,没认出来的照样写「没找到」;候选库一张表都没探查到时不再说「按需求没挑出来」,照实说尚未探查结构、不交空目录,查看算没完成;点名不在这一轮的数据源不补空目录(真实模型验收发现)
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
1. need 没写:回「look 要写 need」,不算次数;次数用完:回「按已查看的结构完成」,不再查看。
|
||||
2. targets 按类型分组(旧写法 tables:[{source, table}] 读进来换成 kind=table)。写了 name 的点名直取,不调挑选模型;
|
||||
只写了 kind(和 source)的、或者整个没写 targets 的,按 need 在那一类里挑(没写时挑这个入口的第一类)。
|
||||
3. 挑选失败或挑出来是空的:交回那一类的目录(status=index),写明原因,请模型点名再看。
|
||||
3. 挑选失败或挑出来是空的:交回那一类的目录(status=index),写明原因,请模型点名再看。点名的一个都没认出来时,
|
||||
按 need 在那几个来源里挑(同只写类别),没认出来的照样写「没找到」;候选里一个能查看的都没有(库还没探查表
|
||||
结构)时没有目录可交,status=failed,照实说原因。
|
||||
4. 这一批对象这一轮都已经看过:回「已在上面」,不重新排一遍。
|
||||
5. 各类给详情,合计受 LOOK_RESULT_CHARS 约束;记一条记录(id、by、need、status、objects、用时)。
|
||||
|
||||
@@ -36,6 +38,7 @@ INDEX_PROMPT = ("没能按需求挑出{what}:{reason}。下面是候选的{wha
|
||||
EMPTY_PROMPT = ("按这个需求没有挑出相关的{what}。这一步确实要用到{what}的话,从下面的目录里点名再 look;"
|
||||
"不需要就接着写:\n{index}")
|
||||
FAILED_PROMPT = "这一次查看出错了:{reason}。按已有的信息接着写"
|
||||
UNAVAILABLE_PROMPT = "没有可查看的{what}:{reason}。按已有的信息接着写,不要猜{what}的名字和字段"
|
||||
#: 按需求挑出来是空的:写进 look 事件的 reason,界面上照原话显示
|
||||
EMPTY_REASON = "按这个需求没有挑出相关的{what}"
|
||||
UNKNOWN_KIND_PROMPT = "这一轮不能查看这一类对象:{kinds}(能查看的:{allowed})"
|
||||
@@ -184,21 +187,33 @@ class LookService:
|
||||
keys: set[str] = set()
|
||||
indexed: list[str] = []
|
||||
empty: list[str] = []
|
||||
unavailable: list[str] = []
|
||||
budget = LOOK_RESULT_CHARS
|
||||
for kind_name in dict.fromkeys([*named, *picks]):
|
||||
kind = self.kinds[kind_name]
|
||||
found, missing = await kind.resolve(named.get(kind_name, [])) if kind_name in named else ([], [])
|
||||
fallback = bool(missing) and not found and kind_name not in picks
|
||||
if fallback:
|
||||
# 点名的一个都没认出来(常见的是拿说明里的中文词当表名):按 need 在那几个来源里挑,和只写类别时一样,
|
||||
# 不让模型为了拿到真名再 look 一次。没认出来的照样写进「没找到」,不拿挑出来的冒充点名的
|
||||
picks[kind_name] = list(dict.fromkeys(str(t["source"]) for t in missing if t.get("source"))) or None
|
||||
if kind_name in picks:
|
||||
within = picks[kind_name] or sources
|
||||
chosen, reason = await kind.pick(need, within)
|
||||
if reason is not None or not chosen:
|
||||
index = await kind.index(within)
|
||||
indexed.append(INDEX_PROMPT.format(what=kind.label, reason=reason, index=index) if reason
|
||||
else EMPTY_PROMPT.format(what=kind.label, index=index))
|
||||
if reason is not None:
|
||||
if not index and reason is not None:
|
||||
# 候选里一个能查看的都没有(库还没探查表结构):没有目录可交,照实说原因,算查看没完成
|
||||
unavailable.append(UNAVAILABLE_PROMPT.format(what=kind.label, reason=reason))
|
||||
record["reason"] = reason
|
||||
else:
|
||||
empty.append(kind.label)
|
||||
elif index or not fallback:
|
||||
# 点名的来源本身不在这一轮里(目录是空的):「没找到数据源」已经说清楚了,不再补一段空目录
|
||||
indexed.append(INDEX_PROMPT.format(what=kind.label, reason=reason, index=index) if reason
|
||||
else EMPTY_PROMPT.format(what=kind.label, index=index))
|
||||
if reason is not None:
|
||||
record["reason"] = reason
|
||||
else:
|
||||
empty.append(kind.label)
|
||||
found += [o for o in chosen if kind.key(o) not in {kind.key(f) for f in found}]
|
||||
if not found and not missing:
|
||||
continue
|
||||
@@ -210,8 +225,10 @@ class LookService:
|
||||
if unknown:
|
||||
parts.append(UNKNOWN_KIND_PROMPT.format(kinds="、".join(dict.fromkeys(unknown)),
|
||||
allowed="、".join(self.kinds)))
|
||||
parts += indexed
|
||||
if indexed and not keys:
|
||||
parts += indexed + unavailable
|
||||
if unavailable and not indexed and not keys:
|
||||
record["status"] = "failed"
|
||||
elif indexed and not keys:
|
||||
# 一个对象都没给出详情、交回的是目录:挑选失败(有原因)或按需求没挑出来(返回空)
|
||||
record["status"] = "index"
|
||||
record.setdefault("reason", EMPTY_REASON.format(what="、".join(empty)))
|
||||
|
||||
@@ -236,6 +236,12 @@ class TableKind:
|
||||
|
||||
async def pick(self, need: str, sources: list[str] | None) -> tuple[list[TableRef], str | None]:
|
||||
candidates, _ = self._candidates(sources)
|
||||
if not any(self.plans[i].tables for i in candidates):
|
||||
# 候选库一张表都没探查到:不是「按需求没挑出来」,照实说为什么是空的(和概览行同一句)
|
||||
bare = [self.plans[i] for i in candidates] if sources else self.plans
|
||||
if bare:
|
||||
return [], ";".join(f"数据源「{p.name}」{introspect.why_empty(p.source.schema_cache or {})}"
|
||||
for p in bare)
|
||||
pool = [TableRef(i, key, "picked") for i in candidates for key in self.plans[i].tables]
|
||||
if len(pool) <= PICK_LIMIT:
|
||||
return pool, None
|
||||
|
||||
@@ -458,3 +458,49 @@ async def test_the_old_tables_field_still_looks(scenic_source):
|
||||
result = await looks.look({"op": "look", "need": "门店",
|
||||
"tables": [{"source": scenic_source.name, "table": "stores"}]}, LookBudget())
|
||||
assert result.record["objects"][0]["named"] == ["stores"] and "store_code TEXT" in result.text
|
||||
|
||||
|
||||
async def test_names_that_match_nothing_fall_back_to_picking_by_need(monkeypatch, small_source):
|
||||
"""模型拿说明里的中文词点名(「订单」「客户」)一个都没认出来:同一次查看里按需求挑,不让它为了拿到真名再看一次。
|
||||
小库不调挑选模型、全部给出;没认出来的照样写进「没找到」,不拿挑出来的冒充点名的。"""
|
||||
fake = FakePicker(error=AssertionError("小库不该调挑选模型"))
|
||||
_use_picker(monkeypatch, fake)
|
||||
looks = await _service([small_source])
|
||||
result = await looks.look({"op": "look", "need": "每位客户的订单金额", "targets": [
|
||||
{"kind": "table", "source": small_source.name, "name": "订单"},
|
||||
{"kind": "table", "source": small_source.name, "name": "客户"}]}, LookBudget())
|
||||
record = result.record
|
||||
assert record["status"] == "ok" and not fake.calls
|
||||
(block,) = record["objects"]
|
||||
assert block["named"] == [] and sorted(block["picked"]) == ["customers", "orders"]
|
||||
assert block["missing"] == ["订单", "客户"]
|
||||
assert "customer_id" in result.text and f"没找到:{small_source.name} 的 订单" in result.text
|
||||
|
||||
|
||||
async def test_names_in_an_unknown_source_do_not_hand_back_an_empty_index(small_source):
|
||||
looks = await _service([small_source])
|
||||
result = await looks.look({"op": "look", "need": "退款", "targets": [
|
||||
{"kind": "table", "source": "没有这个库", "name": "refunds"}]}, LookBudget())
|
||||
assert "没找到数据源「没有这个库」" in result.text
|
||||
assert "目录" not in result.text and result.record["status"] == "ok"
|
||||
|
||||
|
||||
async def test_a_source_never_introspected_says_so_instead_of_an_empty_pick():
|
||||
"""库接上了但还没探查表结构:不是「按需求没挑出来」,照实说为什么是空的,查看算没完成(不交空目录)。"""
|
||||
async with SessionLocal() as session:
|
||||
row = DataSource(name=f"look_bare_{uuid.uuid4().hex[:8]}", kind="sqlite", database="/nonexistent.db",
|
||||
description="门店、订单、客户")
|
||||
session.add(row)
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
session.expunge(row)
|
||||
try:
|
||||
looks = await _service([row])
|
||||
for line in ({"op": "look", "need": "各地区销售额"},
|
||||
{"op": "look", "need": "各地区销售额", "targets": [{"kind": "table", "name": "订单"}]}):
|
||||
result = await looks.look(line, LookBudget())
|
||||
assert result.record["status"] == "failed", line
|
||||
assert result.record["reason"] == f"数据源「{row.name}」尚未探查结构"
|
||||
assert "尚未探查结构" in result.text and "目录" not in result.text
|
||||
finally:
|
||||
await drop_source(row.id)
|
||||
|
||||
Reference in New Issue
Block a user