tabagent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tabagent/__init__.py +57 -0
- tabagent/adapters/__init__.py +68 -0
- tabagent/adapters/common.py +138 -0
- tabagent/adapters/cuga.py +623 -0
- tabagent/adapters/detect.py +182 -0
- tabagent/adapters/live.py +111 -0
- tabagent/adapters/logfiles.py +728 -0
- tabagent/adapters/openai_messages.py +672 -0
- tabagent/adapters/tau2.py +195 -0
- tabagent/agent.py +219 -0
- tabagent/api.py +1016 -0
- tabagent/artifact.py +262 -0
- tabagent/auto.py +260 -0
- tabagent/baselines.py +62 -0
- tabagent/cards/__init__.py +1 -0
- tabagent/cards/appworld_subagent_routing.yaml +77 -0
- tabagent/cards/llmrouterbench_model_routing.yaml +113 -0
- tabagent/cards/mind2web_element.yaml +88 -0
- tabagent/cards/mind2web_operation.yaml +55 -0
- tabagent/cards/skillsbench_skill_gating.yaml +57 -0
- tabagent/cards/skillsbench_skill_gating_enriched.yaml +81 -0
- tabagent/cards/skillsbench_skill_select.yaml +61 -0
- tabagent/cards/skillsbench_skill_select_noprior.yaml +53 -0
- tabagent/cards/tau2_domain_shortlisting.yaml +85 -0
- tabagent/cards/tau2_tool_shortlisting.yaml +58 -0
- tabagent/cli.py +851 -0
- tabagent/datasets/__init__.py +138 -0
- tabagent/datasets/_core.py +171 -0
- tabagent/datasets/appworld.py +207 -0
- tabagent/datasets/folds/appworld_subagent_routing.json +590 -0
- tabagent/datasets/folds/llmrouterbench_tau2.json +283 -0
- tabagent/datasets/folds/mind2web_element_150.json +155 -0
- tabagent/datasets/folds/mind2web_groupkfold5.json +1014 -0
- tabagent/datasets/folds/mind2web_split.json +1014 -0
- tabagent/datasets/folds/skillsbench_skill_gating.json +89 -0
- tabagent/datasets/folds/skillsbench_skill_gating_enriched.json +92 -0
- tabagent/datasets/folds/skillsbench_skill_select.json +92 -0
- tabagent/datasets/folds/tau2_agents.json +599 -0
- tabagent/datasets/folds/tau2_airline.json +29 -0
- tabagent/datasets/folds/tau2_all_domains.json +144 -0
- tabagent/datasets/folds/tau2_retail.json +59 -0
- tabagent/datasets/folds/tau2_telecom.json +66 -0
- tabagent/datasets/llmrouterbench.py +249 -0
- tabagent/datasets/manifests/llmrouterbench_tau2_dense.json +102 -0
- tabagent/datasets/manifests/mind2web_train.json +71 -0
- tabagent/datasets/manifests/skillsbench_official_selected.json +328 -0
- tabagent/datasets/manifests/tau2_official_final.json +185 -0
- tabagent/datasets/manifests/tau2_sft_final.json +17 -0
- tabagent/datasets/mind2web.py +342 -0
- tabagent/datasets/skillsbench.py +207 -0
- tabagent/datasets/tau2.py +361 -0
- tabagent/defaults.py +23 -0
- tabagent/drift.py +701 -0
- tabagent/evaluation/__init__.py +36 -0
- tabagent/evaluation/cv.py +220 -0
- tabagent/evaluation/discovery.py +320 -0
- tabagent/evaluation/metrics.py +266 -0
- tabagent/evaluation/stats.py +116 -0
- tabagent/hub.py +44 -0
- tabagent/integrations/__init__.py +1 -0
- tabagent/integrations/cuga.py +186 -0
- tabagent/integrations/openai_tools.py +232 -0
- tabagent/prometheus.py +196 -0
- tabagent/py.typed +0 -0
- tabagent/report.py +237 -0
- tabagent/scanner.py +1113 -0
- tabagent/serve.py +873 -0
- tabagent/sklearn.py +353 -0
- tabagent/tabhead.py +427 -0
- tabagent/table.py +178 -0
- tabagent/tabschema/__init__.py +22 -0
- tabagent/tabschema/agents.py +1304 -0
- tabagent/tabschema/audit.py +408 -0
- tabagent/tabschema/card.py +392 -0
- tabagent/tabschema/checks.py +307 -0
- tabagent/tabschema/compile.py +1811 -0
- tabagent/tabschema/estimate.py +326 -0
- tabagent/tabschema/features.py +114 -0
- tabagent/tabschema/fitted.py +474 -0
- tabagent/tabschema/judges.py +203 -0
- tabagent/tabschema/llm.py +908 -0
- tabagent/tabschema/orchestrator.py +1823 -0
- tabagent/tabschema/prompts/__init__.py +80 -0
- tabagent/tabschema/prompts/code_extractor.txt +93 -0
- tabagent/tabschema/prompts/code_validator.txt +39 -0
- tabagent/tabschema/prompts/feature_aggregator.txt +37 -0
- tabagent/tabschema/prompts/feature_analyzer.txt +35 -0
- tabagent/tabschema/prompts/judge_generality.txt +51 -0
- tabagent/tabschema/prompts/judge_impact.txt +52 -0
- tabagent/tabschema/prompts/judge_relevance.txt +49 -0
- tabagent/tabschema/prompts/meta_judge.txt +65 -0
- tabagent/tabschema/prompts/output_auditor.txt +34 -0
- tabagent/tabschema/prompts/tabagent_analyzer_candidates.txt +6 -0
- tabagent/tabschema/prompts/tabagent_auditor_candidates.txt +5 -0
- tabagent/tabschema/prompts/tabagent_candidate_note.txt +3 -0
- tabagent/tabschema/prompts/tabagent_extractor.txt +3 -0
- tabagent/tabschema/prompts/tabagent_extractor_candidates.txt +9 -0
- tabagent/tabschema/prompts/tabagent_validator_candidates.txt +9 -0
- tabagent/tabschema/runlog.py +626 -0
- tabagent/tabschema/runstore.py +267 -0
- tabagent/tabschema/sampling.py +143 -0
- tabagent/tabschema/sandbox.py +623 -0
- tabagent/tabschema/trajectory.py +989 -0
- tabagent/tabschema/usefulness.py +193 -0
- tabagent/tabschema/verify.py +1303 -0
- tabagent/tabschema/warm.py +1073 -0
- tabagent/tabsynth.py +344 -0
- tabagent-0.1.0.dist-info/METADATA +313 -0
- tabagent-0.1.0.dist-info/RECORD +113 -0
- tabagent-0.1.0.dist-info/WHEEL +4 -0
- tabagent-0.1.0.dist-info/entry_points.txt +2 -0
- tabagent-0.1.0.dist-info/licenses/LICENSE +201 -0
- tabagent-0.1.0.dist-info/licenses/NOTICE +39 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Pick the adapter for a file or directory of agent logs by looking at its first records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from tabagent.adapters.cuga import CUGA_STEPS, CugaAdapter
|
|
10
|
+
from tabagent.adapters.logfiles import (
|
|
11
|
+
SKIP_FILES,
|
|
12
|
+
LoadNotes,
|
|
13
|
+
MixedFormatsError,
|
|
14
|
+
iter_files,
|
|
15
|
+
read_docs,
|
|
16
|
+
relative,
|
|
17
|
+
)
|
|
18
|
+
from tabagent.adapters.openai_messages import (
|
|
19
|
+
CALL_ITEMS,
|
|
20
|
+
OUTPUT_ITEMS,
|
|
21
|
+
ROLES,
|
|
22
|
+
OpenAIMessagesAdapter,
|
|
23
|
+
message_role,
|
|
24
|
+
unwrap_message,
|
|
25
|
+
)
|
|
26
|
+
from tabagent.adapters.tau2 import Tau2Adapter
|
|
27
|
+
from tabagent.tabschema.trajectory import GenericJsonAdapter, step_name
|
|
28
|
+
|
|
29
|
+
MAX_FILES = 20
|
|
30
|
+
FORMATS = """Supported formats:
|
|
31
|
+
* chat messages (OpenAI, Anthropic Messages API format, LangChain), .json or .jsonl, one run per object:
|
|
32
|
+
{"messages": [{"role": "user", "content": ...}, {"role": "assistant", "tool_calls": [...]}, ...],
|
|
33
|
+
"tools": [...], "reward": 1} -> OpenAIMessagesAdapter
|
|
34
|
+
* tau2-bench results: {"info": ..., "tasks": [...], "simulations": [...]} -> Tau2Adapter
|
|
35
|
+
* CUGA trajectories: {"intent": ..., "task_id": ..., "score": ..., "steps": [{"name": "ShortlisterAgent",
|
|
36
|
+
"prompts": [...], "data": ...}, ...]} -> CugaAdapter
|
|
37
|
+
* any log with named steps: {"steps": [{"name": <component>, ...}, ...]} -> GenericJsonAdapter
|
|
38
|
+
Or pass an adapter explicitly (see tabagent.adapters)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_name(cls: type) -> str:
|
|
42
|
+
"""The log format an adapter class reads, as messages name it."""
|
|
43
|
+
return getattr(cls, "format_name", cls.__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _family(cls: type) -> str:
|
|
47
|
+
if issubclass(cls, Tau2Adapter):
|
|
48
|
+
return "tau2"
|
|
49
|
+
if issubclass(cls, OpenAIMessagesAdapter):
|
|
50
|
+
return "chat"
|
|
51
|
+
if issubclass(cls, GenericJsonAdapter): # CUGA trajectories are step logs
|
|
52
|
+
return "steps"
|
|
53
|
+
return cls.__qualname__
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compatible(reader: type, found: type) -> bool:
|
|
57
|
+
"""Whether an adapter of class ``reader`` reads records detected as ``found``: the same format, where
|
|
58
|
+
CUGA trajectories and other logs with named steps count as one."""
|
|
59
|
+
return _family(reader) == _family(found)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _first_record(f: Path, root: Path | None = None) -> Any:
|
|
63
|
+
"""The first JSON record of a file (the first run of a list), read as the adapters read it; None when the
|
|
64
|
+
file holds none. A JSON Lines file of bare messages (a conversation, one message per line) gives its first
|
|
65
|
+
50 messages as one run."""
|
|
66
|
+
held: list[Any] = []
|
|
67
|
+
for doc in read_docs(root or f.parent, f, LoadNotes()):
|
|
68
|
+
data = doc.data
|
|
69
|
+
if doc.line is not None and isinstance(data, dict) and "messages" not in data and _is_message(data):
|
|
70
|
+
held.append(data)
|
|
71
|
+
if len(held) < 50:
|
|
72
|
+
continue
|
|
73
|
+
break
|
|
74
|
+
if held:
|
|
75
|
+
break
|
|
76
|
+
if isinstance(data, list):
|
|
77
|
+
if data and sum(_is_message(x) for x in data) * 2 >= len(data): # a bare message list: one run
|
|
78
|
+
return {"messages": data}
|
|
79
|
+
return next((x for x in data if isinstance(x, dict)), data)
|
|
80
|
+
return data
|
|
81
|
+
return {"messages": held} if held else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_message(m: Any) -> bool:
|
|
85
|
+
if not isinstance(m, dict):
|
|
86
|
+
return False
|
|
87
|
+
m = unwrap_message(m)
|
|
88
|
+
return message_role(m) in ROLES.values() or m.get("type") in (*CALL_ITEMS, *OUTPUT_ITEMS)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_turn(m: Any) -> bool:
|
|
92
|
+
"""Whether a message is the agent's: an assistant message or a Responses API call."""
|
|
93
|
+
if not isinstance(m, dict):
|
|
94
|
+
return False
|
|
95
|
+
m = unwrap_message(m)
|
|
96
|
+
return message_role(m) == "assistant" or m.get("type") in CALL_ITEMS
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def detect_format(record: Any) -> type | None:
|
|
100
|
+
"""The adapter class for one parsed record, or None. A chat run needs an agent turn: messages that are
|
|
101
|
+
only system or user text (a prompt template, a few-shot example) are not a run."""
|
|
102
|
+
if not isinstance(record, dict):
|
|
103
|
+
return None
|
|
104
|
+
if isinstance(record.get("simulations"), list):
|
|
105
|
+
return Tau2Adapter
|
|
106
|
+
msgs = record.get("messages")
|
|
107
|
+
if isinstance(msgs, list) and msgs and any(_is_message(m) for m in msgs[:5]):
|
|
108
|
+
return OpenAIMessagesAdapter if any(_is_turn(m) for m in msgs) else None
|
|
109
|
+
if _is_turn(record) and "content" in record: # an agent message on its own (a line of a conversation)
|
|
110
|
+
return OpenAIMessagesAdapter
|
|
111
|
+
steps = record.get("steps")
|
|
112
|
+
if isinstance(steps, list):
|
|
113
|
+
names = {step_name(s) for s in steps} - {None}
|
|
114
|
+
if names & CUGA_STEPS:
|
|
115
|
+
return CugaAdapter
|
|
116
|
+
if names:
|
|
117
|
+
return GenericJsonAdapter
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _census(path: str | Path, limit: int | None) -> tuple[list[tuple[str, type | None, Any]], int]:
|
|
122
|
+
"""``(file, format, first record)`` for the first ``limit`` log files (all with None), and how many run
|
|
123
|
+
summaries were passed over."""
|
|
124
|
+
root = Path(path)
|
|
125
|
+
files = iter_files(root)
|
|
126
|
+
summaries = sum(f.name in SKIP_FILES for f in files)
|
|
127
|
+
out = []
|
|
128
|
+
for f in [f for f in files if f.name not in SKIP_FILES][:limit]:
|
|
129
|
+
rec = _first_record(f, root if root.is_dir() else f.parent)
|
|
130
|
+
out.append((relative(root, f), detect_format(rec), rec))
|
|
131
|
+
return out, summaries
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pick(found: list[tuple[str, type]], path: Any, on_other_formats: str) -> type:
|
|
135
|
+
"""The adapter class for the formats found: CUGA over generic steps, else the format most files hold (the
|
|
136
|
+
first in file order on a tie). Several formats raise with ``on_other_formats="error"``."""
|
|
137
|
+
classes = list(dict.fromkeys(cls for _, cls in found))
|
|
138
|
+
if CugaAdapter in classes and GenericJsonAdapter in classes:
|
|
139
|
+
classes.remove(GenericJsonAdapter)
|
|
140
|
+
found = [(f, CugaAdapter if c is GenericJsonAdapter else c) for f, c in found]
|
|
141
|
+
counts = Counter(cls for _, cls in found)
|
|
142
|
+
if len(classes) > 1 and on_other_formats == "error":
|
|
143
|
+
first = {cls: next(f for f, c in found if c is cls) for cls in classes}
|
|
144
|
+
listed = "; ".join(
|
|
145
|
+
f"{format_name(cls)} ({cls.__name__}): {counts[cls]} file(s), e.g. {first[cls]}"
|
|
146
|
+
for cls in classes
|
|
147
|
+
)
|
|
148
|
+
raise MixedFormatsError(
|
|
149
|
+
f"{path} holds runs of more than one log format: {listed}. One onboarding reads one format: "
|
|
150
|
+
"point at a folder or file with one format, pass adapter= for the one you want, or use "
|
|
151
|
+
'on_other_formats="skip" (the default; CLI --on-other-formats skip) to read the most common one '
|
|
152
|
+
"and skip the others with a warning"
|
|
153
|
+
)
|
|
154
|
+
return max(classes, key=lambda cls: counts[cls]) # the first of the most common
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def detect_adapter(path: str | Path, *, on_other_formats: str = "skip", **kwargs: Any):
|
|
158
|
+
"""The adapter for the logs at ``path``, chosen from the first files; ``kwargs`` go to its constructor.
|
|
159
|
+
|
|
160
|
+
The first records of the first files (up to 20, run summaries skipped) decide: CUGA trajectories over
|
|
161
|
+
generic step logs, else the format most of them hold. Runs of another format are skipped by the adapter's
|
|
162
|
+
``load`` with a warning (``on_other_formats="skip"``), or stop it with ``MixedFormatsError``
|
|
163
|
+
(``"error"``)."""
|
|
164
|
+
if on_other_formats not in ("skip", "error"):
|
|
165
|
+
raise ValueError(f"on_other_formats must be 'skip' or 'error', not {on_other_formats!r}")
|
|
166
|
+
census, summaries = _census(path, MAX_FILES)
|
|
167
|
+
found = [(f, cls) for f, cls, _ in census if cls is not None]
|
|
168
|
+
if found:
|
|
169
|
+
adapter = _pick(found, path, on_other_formats)(**kwargs)
|
|
170
|
+
adapter.on_other_formats = on_other_formats
|
|
171
|
+
return adapter
|
|
172
|
+
seen = [
|
|
173
|
+
f"{f}: {sorted(map(str, rec))[:8] if isinstance(rec, dict) else type(rec).__name__}"
|
|
174
|
+
for f, _, rec in census
|
|
175
|
+
]
|
|
176
|
+
if seen:
|
|
177
|
+
found_text = "\n".join(f" {s}" for s in seen[:5])
|
|
178
|
+
elif summaries:
|
|
179
|
+
found_text = f" (only {summaries} run summaries: results.json, metadata.json)"
|
|
180
|
+
else:
|
|
181
|
+
found_text = " (no .json or .jsonl files)"
|
|
182
|
+
raise ValueError(f"could not recognize the log format of {path}. First records:\n{found_text}\n{FORMATS}")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Live prefixes: build a live decision's prefix exactly as an adapter built the training prefixes.
|
|
2
|
+
|
|
3
|
+
A head scores the prefix its adapter produced in training: for chat-message logs, the run-level task fields
|
|
4
|
+
(``instruction``, ``domain``, ...) and the normalized messages; for CUGA and generic step logs, the run so far
|
|
5
|
+
without its outcome fields. A live decision must reach the extractor in the same shape, or the extractor
|
|
6
|
+
reads a field that is not there. ``describe`` records which adapter, with which prefix settings, a head was
|
|
7
|
+
trained with (``head.json``); ``from_description`` rebuilds it when the head is loaded, and ``live_prefix``
|
|
8
|
+
applies it to a live run.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from tabagent.adapters.cuga import CugaAdapter
|
|
16
|
+
from tabagent.adapters.openai_messages import OpenAIMessagesAdapter
|
|
17
|
+
from tabagent.adapters.tau2 import Tau2Adapter
|
|
18
|
+
from tabagent.tabschema.trajectory import DecisionPoint, GenericJsonAdapter, Trajectory, redact, steps_of
|
|
19
|
+
|
|
20
|
+
_STEP_KEYS = ("steps_key", "step_name_key", "outcome_keys", "outcome_steps")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def describe(adapter: Any) -> dict[str, Any] | None:
|
|
24
|
+
"""A JSON description of ``adapter``: its class and the settings that shape a prefix (None for None; a
|
|
25
|
+
description passes through unchanged).
|
|
26
|
+
|
|
27
|
+
The built-in adapters are described fully; another class is named, but :func:`from_description` cannot
|
|
28
|
+
rebuild it, so a head trained with it needs the adapter passed again when it is loaded.
|
|
29
|
+
"""
|
|
30
|
+
if adapter is None or isinstance(adapter, dict):
|
|
31
|
+
return adapter
|
|
32
|
+
cls = type(adapter)
|
|
33
|
+
out: dict[str, Any] = {"class": cls.__name__, "module": cls.__module__}
|
|
34
|
+
if cls is Tau2Adapter:
|
|
35
|
+
out["config"] = {
|
|
36
|
+
"granularity": adapter.granularity,
|
|
37
|
+
"include_policy": adapter.include_policy,
|
|
38
|
+
"max_chars": adapter.max_chars,
|
|
39
|
+
}
|
|
40
|
+
elif cls is OpenAIMessagesAdapter:
|
|
41
|
+
out["config"] = {
|
|
42
|
+
"granularity": adapter.granularity,
|
|
43
|
+
"task_id_key": adapter.task_id_key,
|
|
44
|
+
"task_keys": list(adapter.task_keys),
|
|
45
|
+
"outcome_keys": list(adapter.outcome_keys),
|
|
46
|
+
"max_chars": adapter.max_chars,
|
|
47
|
+
}
|
|
48
|
+
elif cls in (CugaAdapter, GenericJsonAdapter):
|
|
49
|
+
cfg = {k: getattr(adapter, k) for k in (*_STEP_KEYS, "group_key", "task_keys")}
|
|
50
|
+
out["config"] = {k: list(v) if isinstance(v, tuple) else v for k, v in cfg.items()}
|
|
51
|
+
if cls is CugaAdapter: # fixed by CugaAdapter
|
|
52
|
+
del out["config"]["steps_key"], out["config"]["step_name_key"], out["config"]["group_key"]
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def from_description(desc: dict[str, Any] | None) -> Any:
|
|
57
|
+
"""The adapter a :func:`describe` output names, or None when it is not a built-in adapter."""
|
|
58
|
+
if not desc or "config" not in desc:
|
|
59
|
+
return None
|
|
60
|
+
classes = {c.__name__: c for c in (OpenAIMessagesAdapter, Tau2Adapter, CugaAdapter, GenericJsonAdapter)}
|
|
61
|
+
cls = classes.get(desc.get("class", ""))
|
|
62
|
+
return None if cls is None else cls(**desc["config"])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def live_prefix(adapter: Any, run: dict[str, Any], target: str | None = None) -> dict[str, Any]:
|
|
66
|
+
"""The prefix ``adapter`` builds for a live ``run`` (the run so far, in the logged format) before the
|
|
67
|
+
decision ``target``.
|
|
68
|
+
|
|
69
|
+
Chat-message adapters take ``{<task fields>, "messages": [...], "tools": [...]}``; CUGA and generic step
|
|
70
|
+
adapters take the trajectory so far (``{<task fields>, "steps": [...]}``), redacted as in training.
|
|
71
|
+
"""
|
|
72
|
+
if not isinstance(run, dict):
|
|
73
|
+
raise TypeError(f"a live run is a dict in the format of the training logs, not {type(run).__name__}")
|
|
74
|
+
if isinstance(adapter, OpenAIMessagesAdapter):
|
|
75
|
+
return adapter.live_prefix(run, target if target in adapter.targets else None)
|
|
76
|
+
if hasattr(adapter, "live_prefix"):
|
|
77
|
+
return adapter.live_prefix(run)
|
|
78
|
+
if isinstance(adapter, GenericJsonAdapter):
|
|
79
|
+
a = adapter
|
|
80
|
+
n = len(steps_of(run, a.steps_key))
|
|
81
|
+
return redact(run, a.steps_key, n, a.outcome_keys, a.outcome_steps, a.step_name_key)
|
|
82
|
+
raise TypeError(
|
|
83
|
+
f"{type(adapter).__name__} has no live_prefix(run): build the prefix as in training and pass it to "
|
|
84
|
+
"decide(prefix, ...)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_before(adapter: Any, traj: Trajectory, dp: DecisionPoint) -> dict[str, Any]:
|
|
89
|
+
"""The run of ``dp`` as the agent held it when the decision was made, in the logged format: what a live
|
|
90
|
+
caller passes to :func:`live_prefix`. For chat-message adapters, the run's fields with the messages before
|
|
91
|
+
the decision; for step adapters, the run with the steps before it. ``live_prefix(adapter, run_before(...),
|
|
92
|
+
target) == dp.prefix`` is the train/serve parity every built-in adapter keeps (checked by
|
|
93
|
+
``tabagent.scanner.check_logs`` and the tests)."""
|
|
94
|
+
raw = traj.raw
|
|
95
|
+
if isinstance(adapter, OpenAIMessagesAdapter):
|
|
96
|
+
messages = raw.get("messages") if isinstance(raw.get("messages"), list) else []
|
|
97
|
+
base = {k: v for k, v in raw.items() if k != "messages"}
|
|
98
|
+
|
|
99
|
+
def n_steps(m: int) -> int:
|
|
100
|
+
return len(adapter._steps({**base, "messages": messages[:m]})[0])
|
|
101
|
+
|
|
102
|
+
lo, hi = 0, len(messages) # the most messages whose steps end before the decision step
|
|
103
|
+
while lo < hi:
|
|
104
|
+
mid = (lo + hi + 1) // 2
|
|
105
|
+
if n_steps(mid) <= dp.step_index:
|
|
106
|
+
lo = mid
|
|
107
|
+
else:
|
|
108
|
+
hi = mid - 1
|
|
109
|
+
return {**base, "messages": messages[:lo]}
|
|
110
|
+
key = getattr(adapter, "steps_key", "steps")
|
|
111
|
+
return {**raw, key: steps_of(raw, key)[: dp.step_index]}
|