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.
Files changed (113) hide show
  1. tabagent/__init__.py +57 -0
  2. tabagent/adapters/__init__.py +68 -0
  3. tabagent/adapters/common.py +138 -0
  4. tabagent/adapters/cuga.py +623 -0
  5. tabagent/adapters/detect.py +182 -0
  6. tabagent/adapters/live.py +111 -0
  7. tabagent/adapters/logfiles.py +728 -0
  8. tabagent/adapters/openai_messages.py +672 -0
  9. tabagent/adapters/tau2.py +195 -0
  10. tabagent/agent.py +219 -0
  11. tabagent/api.py +1016 -0
  12. tabagent/artifact.py +262 -0
  13. tabagent/auto.py +260 -0
  14. tabagent/baselines.py +62 -0
  15. tabagent/cards/__init__.py +1 -0
  16. tabagent/cards/appworld_subagent_routing.yaml +77 -0
  17. tabagent/cards/llmrouterbench_model_routing.yaml +113 -0
  18. tabagent/cards/mind2web_element.yaml +88 -0
  19. tabagent/cards/mind2web_operation.yaml +55 -0
  20. tabagent/cards/skillsbench_skill_gating.yaml +57 -0
  21. tabagent/cards/skillsbench_skill_gating_enriched.yaml +81 -0
  22. tabagent/cards/skillsbench_skill_select.yaml +61 -0
  23. tabagent/cards/skillsbench_skill_select_noprior.yaml +53 -0
  24. tabagent/cards/tau2_domain_shortlisting.yaml +85 -0
  25. tabagent/cards/tau2_tool_shortlisting.yaml +58 -0
  26. tabagent/cli.py +851 -0
  27. tabagent/datasets/__init__.py +138 -0
  28. tabagent/datasets/_core.py +171 -0
  29. tabagent/datasets/appworld.py +207 -0
  30. tabagent/datasets/folds/appworld_subagent_routing.json +590 -0
  31. tabagent/datasets/folds/llmrouterbench_tau2.json +283 -0
  32. tabagent/datasets/folds/mind2web_element_150.json +155 -0
  33. tabagent/datasets/folds/mind2web_groupkfold5.json +1014 -0
  34. tabagent/datasets/folds/mind2web_split.json +1014 -0
  35. tabagent/datasets/folds/skillsbench_skill_gating.json +89 -0
  36. tabagent/datasets/folds/skillsbench_skill_gating_enriched.json +92 -0
  37. tabagent/datasets/folds/skillsbench_skill_select.json +92 -0
  38. tabagent/datasets/folds/tau2_agents.json +599 -0
  39. tabagent/datasets/folds/tau2_airline.json +29 -0
  40. tabagent/datasets/folds/tau2_all_domains.json +144 -0
  41. tabagent/datasets/folds/tau2_retail.json +59 -0
  42. tabagent/datasets/folds/tau2_telecom.json +66 -0
  43. tabagent/datasets/llmrouterbench.py +249 -0
  44. tabagent/datasets/manifests/llmrouterbench_tau2_dense.json +102 -0
  45. tabagent/datasets/manifests/mind2web_train.json +71 -0
  46. tabagent/datasets/manifests/skillsbench_official_selected.json +328 -0
  47. tabagent/datasets/manifests/tau2_official_final.json +185 -0
  48. tabagent/datasets/manifests/tau2_sft_final.json +17 -0
  49. tabagent/datasets/mind2web.py +342 -0
  50. tabagent/datasets/skillsbench.py +207 -0
  51. tabagent/datasets/tau2.py +361 -0
  52. tabagent/defaults.py +23 -0
  53. tabagent/drift.py +701 -0
  54. tabagent/evaluation/__init__.py +36 -0
  55. tabagent/evaluation/cv.py +220 -0
  56. tabagent/evaluation/discovery.py +320 -0
  57. tabagent/evaluation/metrics.py +266 -0
  58. tabagent/evaluation/stats.py +116 -0
  59. tabagent/hub.py +44 -0
  60. tabagent/integrations/__init__.py +1 -0
  61. tabagent/integrations/cuga.py +186 -0
  62. tabagent/integrations/openai_tools.py +232 -0
  63. tabagent/prometheus.py +196 -0
  64. tabagent/py.typed +0 -0
  65. tabagent/report.py +237 -0
  66. tabagent/scanner.py +1113 -0
  67. tabagent/serve.py +873 -0
  68. tabagent/sklearn.py +353 -0
  69. tabagent/tabhead.py +427 -0
  70. tabagent/table.py +178 -0
  71. tabagent/tabschema/__init__.py +22 -0
  72. tabagent/tabschema/agents.py +1304 -0
  73. tabagent/tabschema/audit.py +408 -0
  74. tabagent/tabschema/card.py +392 -0
  75. tabagent/tabschema/checks.py +307 -0
  76. tabagent/tabschema/compile.py +1811 -0
  77. tabagent/tabschema/estimate.py +326 -0
  78. tabagent/tabschema/features.py +114 -0
  79. tabagent/tabschema/fitted.py +474 -0
  80. tabagent/tabschema/judges.py +203 -0
  81. tabagent/tabschema/llm.py +908 -0
  82. tabagent/tabschema/orchestrator.py +1823 -0
  83. tabagent/tabschema/prompts/__init__.py +80 -0
  84. tabagent/tabschema/prompts/code_extractor.txt +93 -0
  85. tabagent/tabschema/prompts/code_validator.txt +39 -0
  86. tabagent/tabschema/prompts/feature_aggregator.txt +37 -0
  87. tabagent/tabschema/prompts/feature_analyzer.txt +35 -0
  88. tabagent/tabschema/prompts/judge_generality.txt +51 -0
  89. tabagent/tabschema/prompts/judge_impact.txt +52 -0
  90. tabagent/tabschema/prompts/judge_relevance.txt +49 -0
  91. tabagent/tabschema/prompts/meta_judge.txt +65 -0
  92. tabagent/tabschema/prompts/output_auditor.txt +34 -0
  93. tabagent/tabschema/prompts/tabagent_analyzer_candidates.txt +6 -0
  94. tabagent/tabschema/prompts/tabagent_auditor_candidates.txt +5 -0
  95. tabagent/tabschema/prompts/tabagent_candidate_note.txt +3 -0
  96. tabagent/tabschema/prompts/tabagent_extractor.txt +3 -0
  97. tabagent/tabschema/prompts/tabagent_extractor_candidates.txt +9 -0
  98. tabagent/tabschema/prompts/tabagent_validator_candidates.txt +9 -0
  99. tabagent/tabschema/runlog.py +626 -0
  100. tabagent/tabschema/runstore.py +267 -0
  101. tabagent/tabschema/sampling.py +143 -0
  102. tabagent/tabschema/sandbox.py +623 -0
  103. tabagent/tabschema/trajectory.py +989 -0
  104. tabagent/tabschema/usefulness.py +193 -0
  105. tabagent/tabschema/verify.py +1303 -0
  106. tabagent/tabschema/warm.py +1073 -0
  107. tabagent/tabsynth.py +344 -0
  108. tabagent-0.1.0.dist-info/METADATA +313 -0
  109. tabagent-0.1.0.dist-info/RECORD +113 -0
  110. tabagent-0.1.0.dist-info/WHEEL +4 -0
  111. tabagent-0.1.0.dist-info/entry_points.txt +2 -0
  112. tabagent-0.1.0.dist-info/licenses/LICENSE +201 -0
  113. 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]}