althing 1.7.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 (156) hide show
  1. althing/__init__.py +46 -0
  2. althing/__main__.py +9 -0
  3. althing/__version__.py +1 -0
  4. althing/_compat_env.py +27 -0
  5. althing/_runners.py +953 -0
  6. althing/agent_assets/__init__.py +3 -0
  7. althing/agent_assets/commands/althing-poll.md +75 -0
  8. althing/agent_assets/skills/concept-test/SKILL.md +72 -0
  9. althing/agent_assets/skills/focus-group/SKILL.md +77 -0
  10. althing/agent_assets/skills/name-test/SKILL.md +62 -0
  11. althing/agent_assets/skills/pricing-probe/SKILL.md +75 -0
  12. althing/agent_assets/skills/survey-prescreen/SKILL.md +76 -0
  13. althing/aggregation.py +240 -0
  14. althing/analysis/__init__.py +46 -0
  15. althing/analysis/distribution.py +323 -0
  16. althing/analysis/inspect.py +586 -0
  17. althing/analysis/subgroup.py +168 -0
  18. althing/analysis/subgroup_cli.py +598 -0
  19. althing/analyze.py +839 -0
  20. althing/attachments/__init__.py +77 -0
  21. althing/attachments/filter.py +67 -0
  22. althing/attachments/models.py +50 -0
  23. althing/attachments/pdf.py +537 -0
  24. althing/attachments/store.py +111 -0
  25. althing/calibration.py +239 -0
  26. althing/checkpoint.py +763 -0
  27. althing/cli/__init__.py +1 -0
  28. althing/cli/commands.py +8080 -0
  29. althing/cli/mcp_install.py +487 -0
  30. althing/cli/output.py +106 -0
  31. althing/cli/parser.py +1706 -0
  32. althing/cli/progress.py +93 -0
  33. althing/cli/repl.py +122 -0
  34. althing/cli/slash.py +197 -0
  35. althing/conditions.py +261 -0
  36. althing/convergence.py +1091 -0
  37. althing/cost.py +950 -0
  38. althing/cost_summary.py +452 -0
  39. althing/credentials.py +311 -0
  40. althing/data/__init__.py +8 -0
  41. althing/data/synthbench-snapshot.json +120 -0
  42. althing/diff.py +331 -0
  43. althing/domains.py +284 -0
  44. althing/ensemble.py +922 -0
  45. althing/fetch/__init__.py +38 -0
  46. althing/fetch/cache.py +360 -0
  47. althing/fetch/ladder.py +437 -0
  48. althing/fetch/lower.py +199 -0
  49. althing/fetch/perimeter.py +494 -0
  50. althing/instrument.py +751 -0
  51. althing/integrations/__init__.py +21 -0
  52. althing/integrations/composio.py +345 -0
  53. althing/llm/__init__.py +32 -0
  54. althing/llm/aliases.py +138 -0
  55. althing/llm/capabilities.py +86 -0
  56. althing/llm/client.py +267 -0
  57. althing/llm/errors.py +162 -0
  58. althing/llm/fast_default.py +71 -0
  59. althing/llm/models.py +340 -0
  60. althing/llm/providers/__init__.py +18 -0
  61. althing/llm/providers/_anthropic_format.py +281 -0
  62. althing/llm/providers/_openai_format.py +381 -0
  63. althing/llm/providers/anthropic.py +172 -0
  64. althing/llm/providers/base.py +60 -0
  65. althing/llm/providers/gemini.py +107 -0
  66. althing/llm/providers/openai_compat.py +102 -0
  67. althing/llm/providers/openrouter.py +357 -0
  68. althing/llm/providers/xai.py +98 -0
  69. althing/llm/retry.py +113 -0
  70. althing/logging_config.py +80 -0
  71. althing/main.py +308 -0
  72. althing/mcp/__init__.py +7 -0
  73. althing/mcp/compat.py +110 -0
  74. althing/mcp/data.py +700 -0
  75. althing/mcp/sampling.py +380 -0
  76. althing/mcp/server.py +3371 -0
  77. althing/metadata.py +214 -0
  78. althing/metadata_migrations.py +70 -0
  79. althing/orchestrator.py +2119 -0
  80. althing/pack_diff.py +271 -0
  81. althing/packs/__init__.py +3 -0
  82. althing/packs/ai-eval-buyers.yaml +247 -0
  83. althing/packs/broad-professionals.yaml +251 -0
  84. althing/packs/developer.yaml +271 -0
  85. althing/packs/enterprise-ai-buyers.yaml +245 -0
  86. althing/packs/enterprise-buyer.yaml +252 -0
  87. althing/packs/general-consumer.yaml +271 -0
  88. althing/packs/healthcare-patient.yaml +271 -0
  89. althing/packs/instruments/__init__.py +1 -0
  90. althing/packs/instruments/churn-diagnosis.yaml +73 -0
  91. althing/packs/instruments/feature-prioritization.yaml +75 -0
  92. althing/packs/instruments/general-survey.yaml +46 -0
  93. althing/packs/instruments/happiness-probe.yaml +41 -0
  94. althing/packs/instruments/landing-page-comprehension.yaml +75 -0
  95. althing/packs/instruments/market-research.yaml +50 -0
  96. althing/packs/instruments/name-test.yaml +74 -0
  97. althing/packs/instruments/pricing-discovery.yaml +80 -0
  98. althing/packs/instruments/product-feedback.yaml +46 -0
  99. althing/packs/job-seekers.yaml +188 -0
  100. althing/packs/market-research-critics.yaml +217 -0
  101. althing/packs/product-research.yaml +248 -0
  102. althing/packs/recruiters-talent.yaml +264 -0
  103. althing/packs/skeptical-executives.yaml +227 -0
  104. althing/packs/startup-founder.yaml +242 -0
  105. althing/packs/students.yaml +352 -0
  106. althing/persistence.py +510 -0
  107. althing/perturbation.py +333 -0
  108. althing/plugins/__init__.py +34 -0
  109. althing/plugins/hooks.py +159 -0
  110. althing/plugins/lint.py +352 -0
  111. althing/plugins/manager.py +176 -0
  112. althing/plugins/manifest.py +100 -0
  113. althing/plugins/registry.py +66 -0
  114. althing/poll_summary.py +1195 -0
  115. althing/preflight.py +155 -0
  116. althing/profiles.py +264 -0
  117. althing/prompts.py +311 -0
  118. althing/py.typed +0 -0
  119. althing/question_budget.py +140 -0
  120. althing/registry/__init__.py +178 -0
  121. althing/registry/cache.py +223 -0
  122. althing/registry/fetch.py +104 -0
  123. althing/registry/github.py +212 -0
  124. althing/reporting/__init__.py +17 -0
  125. althing/reporting/loader.py +69 -0
  126. althing/reporting/markdown.py +450 -0
  127. althing/response_coercion.py +161 -0
  128. althing/routing.py +132 -0
  129. althing/runtime.py +552 -0
  130. althing/schemas/__init__.py +53 -0
  131. althing/schemas/v1.0.0.json +161 -0
  132. althing/sdk.py +1458 -0
  133. althing/stats.py +1522 -0
  134. althing/structured/__init__.py +5 -0
  135. althing/structured/models.py +149 -0
  136. althing/structured/output.py +376 -0
  137. althing/structured/retry.py +170 -0
  138. althing/structured/schemas.py +127 -0
  139. althing/structured/validate.py +303 -0
  140. althing/structured/verdict.py +146 -0
  141. althing/synthbench.py +768 -0
  142. althing/synthbench_submit.py +582 -0
  143. althing/synthesis.py +1529 -0
  144. althing/synthesis_recovery.py +467 -0
  145. althing/templates.py +179 -0
  146. althing/text_width.py +126 -0
  147. althing-1.7.0.dist-info/METADATA +1473 -0
  148. althing-1.7.0.dist-info/RECORD +156 -0
  149. althing-1.7.0.dist-info/WHEEL +5 -0
  150. althing-1.7.0.dist-info/entry_points.txt +3 -0
  151. althing-1.7.0.dist-info/licenses/LICENSE +21 -0
  152. althing-1.7.0.dist-info/top_level.txt +3 -0
  153. synth_panel/__init__.py +60 -0
  154. synth_panel/__main__.py +17 -0
  155. synthpanel/__init__.py +60 -0
  156. synthpanel/__main__.py +17 -0
althing/pack_diff.py ADDED
@@ -0,0 +1,271 @@
1
+ """Diff two persona packs side-by-side (GH-308).
2
+
3
+ Compares packs by persona name (added / removed / unchanged / changed) and
4
+ summarizes composition deltas (age range, role distribution, gender split
5
+ when present). Loads either built-in / user-saved pack IDs or file paths.
6
+
7
+ Name-based matching is intentionally simple — renames will surface as
8
+ ``added`` + ``removed`` rather than a single change. The README documents
9
+ this so users can interpret results correctly.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from collections import Counter
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ import yaml
21
+
22
+ from althing.mcp.data import get_persona_pack
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Data models
26
+ # ---------------------------------------------------------------------------
27
+
28
+ # Persona-level fields the diff inspects when both packs contain a persona of
29
+ # the same name. Adding new fields here is the supported way to broaden the
30
+ # field-level diff without touching the matching logic.
31
+ _FIELD_KEYS: tuple[str, ...] = (
32
+ "age",
33
+ "occupation",
34
+ "background",
35
+ "personality_traits",
36
+ "gender",
37
+ )
38
+
39
+ _SENIORITY_PREFIXES = {"junior", "senior", "staff", "principal", "lead", "chief"}
40
+
41
+
42
+ @dataclass
43
+ class CompositionStats:
44
+ """Aggregate demographic snapshot for a single pack.
45
+
46
+ ``gender_split`` is empty when no persona declares a ``gender`` field
47
+ (the common case for the bundled packs). ``role_distribution`` is keyed
48
+ on a one-word bucket derived from each persona's ``occupation``.
49
+ """
50
+
51
+ persona_count: int
52
+ age_min: int | None
53
+ age_max: int | None
54
+ age_mean: float | None
55
+ gender_split: dict[str, int] = field(default_factory=dict)
56
+ role_distribution: dict[str, int] = field(default_factory=dict)
57
+
58
+
59
+ @dataclass
60
+ class PersonaChange:
61
+ """Field-level diff for a persona present in both packs.
62
+
63
+ ``changed`` maps each differing field to ``{"a": <a-value>, "b": <b-value>}``.
64
+ For ``personality_traits`` the values are the raw lists; the renderer
65
+ surfaces added/removed traits separately for readability.
66
+ """
67
+
68
+ name: str
69
+ changed: dict[str, dict[str, Any]]
70
+
71
+
72
+ @dataclass
73
+ class PackDiff:
74
+ pack_a_id: str
75
+ pack_b_id: str
76
+ pack_a_name: str
77
+ pack_b_name: str
78
+ composition_a: CompositionStats
79
+ composition_b: CompositionStats
80
+ added: list[str]
81
+ removed: list[str]
82
+ unchanged: list[str]
83
+ changed: list[PersonaChange]
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Loading
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ def load_pack(source: str) -> tuple[dict[str, Any], str]:
92
+ """Load a persona pack from a file path or built-in / user-saved pack ID.
93
+
94
+ File paths win when *source* points to an existing file — this lets
95
+ callers diff unpublished packs without first installing them. Otherwise
96
+ the loader falls back to :func:`althing.mcp.data.get_persona_pack`,
97
+ which checks user-saved packs first and then bundled packs.
98
+
99
+ Returns ``(pack_dict, source_label)`` — the label is the file stem for
100
+ paths and the original ID for installed packs, used for human-readable
101
+ output.
102
+ """
103
+ p = Path(source)
104
+ if p.is_file():
105
+ data = yaml.safe_load(p.read_text(encoding="utf-8"))
106
+ if not isinstance(data, dict):
107
+ raise ValueError(f"Pack file does not contain a mapping: {source}")
108
+ return data, p.stem
109
+ return get_persona_pack(source), source
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Composition stats
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ def _normalize_traits(traits: Any) -> list[str]:
118
+ """Collapse traits to a sorted list of stripped lowercase strings."""
119
+ if traits is None:
120
+ return []
121
+ if isinstance(traits, str):
122
+ items = [t.strip().lower() for t in traits.split(",") if t.strip()]
123
+ elif isinstance(traits, list):
124
+ items = [str(t).strip().lower() for t in traits if str(t).strip()]
125
+ else:
126
+ return []
127
+ return sorted(items)
128
+
129
+
130
+ def _role_bucket(occupation: str) -> str:
131
+ """Coarse single-word bucket for an occupation string.
132
+
133
+ Drops seniority prefixes (Junior, Senior, Staff, Principal, Lead, Chief)
134
+ and returns the first remaining alphabetical token, lowercased. Falls
135
+ back to the raw stripped string when no tokens match.
136
+ """
137
+ tokens = re.findall(r"[A-Za-z][A-Za-z0-9-]+", occupation)
138
+ if not tokens:
139
+ return occupation.strip().lower()
140
+ cleaned = [t.lower() for t in tokens if t.lower() not in _SENIORITY_PREFIXES]
141
+ if not cleaned:
142
+ cleaned = [t.lower() for t in tokens]
143
+ return cleaned[0]
144
+
145
+
146
+ def _compose(personas: list[dict[str, Any]]) -> CompositionStats:
147
+ ages = [int(p["age"]) for p in personas if isinstance(p.get("age"), int)]
148
+ age_min = min(ages) if ages else None
149
+ age_max = max(ages) if ages else None
150
+ age_mean = round(sum(ages) / len(ages), 1) if ages else None
151
+
152
+ gender_counter: Counter[str] = Counter()
153
+ for p in personas:
154
+ g = p.get("gender")
155
+ if g:
156
+ gender_counter[str(g).lower().strip()] += 1
157
+
158
+ role_counter: Counter[str] = Counter()
159
+ for p in personas:
160
+ occ = p.get("occupation")
161
+ if occ:
162
+ role_counter[_role_bucket(str(occ))] += 1
163
+
164
+ return CompositionStats(
165
+ persona_count=len(personas),
166
+ age_min=age_min,
167
+ age_max=age_max,
168
+ age_mean=age_mean,
169
+ gender_split=dict(gender_counter),
170
+ role_distribution=dict(role_counter),
171
+ )
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Field-level diff
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ def _persona_field_diff(a: dict[str, Any], b: dict[str, Any]) -> dict[str, dict[str, Any]]:
180
+ """Return the subset of ``_FIELD_KEYS`` that differ between two personas.
181
+
182
+ ``personality_traits`` is compared after normalization (sorted, lowercase,
183
+ stripped) so cosmetic ordering or casing changes don't trip the diff.
184
+ Other fields are compared with raw equality and ``None == None`` is not
185
+ flagged.
186
+ """
187
+ changed: dict[str, dict[str, Any]] = {}
188
+ for key in _FIELD_KEYS:
189
+ av = a.get(key)
190
+ bv = b.get(key)
191
+ if key == "personality_traits":
192
+ if _normalize_traits(av) != _normalize_traits(bv):
193
+ changed[key] = {"a": av, "b": bv}
194
+ else:
195
+ if av is None and bv is None:
196
+ continue
197
+ if av != bv:
198
+ changed[key] = {"a": av, "b": bv}
199
+ return changed
200
+
201
+
202
+ def trait_delta(change: PersonaChange) -> tuple[list[str], list[str]]:
203
+ """Return ``(added, removed)`` traits for a persona change, or empty lists."""
204
+ payload = change.changed.get("personality_traits")
205
+ if not payload:
206
+ return [], []
207
+ a = set(_normalize_traits(payload.get("a")))
208
+ b = set(_normalize_traits(payload.get("b")))
209
+ return sorted(b - a), sorted(a - b)
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Main entry point
214
+ # ---------------------------------------------------------------------------
215
+
216
+
217
+ def compute_pack_diff(
218
+ pack_a: dict[str, Any],
219
+ pack_b: dict[str, Any],
220
+ *,
221
+ pack_a_id: str = "",
222
+ pack_b_id: str = "",
223
+ ) -> PackDiff:
224
+ """Compute a side-by-side diff of two persona packs.
225
+
226
+ Personas are matched by ``name`` (exact). Renames appear as one entry in
227
+ ``added`` and one in ``removed``. ``unchanged`` lists matched personas
228
+ whose tracked fields are all equal; ``changed`` lists matched personas
229
+ with at least one differing field.
230
+ """
231
+ personas_a = pack_a.get("personas") or []
232
+ personas_b = pack_b.get("personas") or []
233
+ if not isinstance(personas_a, list):
234
+ personas_a = []
235
+ if not isinstance(personas_b, list):
236
+ personas_b = []
237
+
238
+ by_name_a = {p["name"]: p for p in personas_a if isinstance(p, dict) and p.get("name")}
239
+ by_name_b = {p["name"]: p for p in personas_b if isinstance(p, dict) and p.get("name")}
240
+
241
+ names_a = set(by_name_a)
242
+ names_b = set(by_name_b)
243
+
244
+ added = sorted(names_b - names_a)
245
+ removed = sorted(names_a - names_b)
246
+ common = sorted(names_a & names_b)
247
+
248
+ unchanged: list[str] = []
249
+ changed: list[PersonaChange] = []
250
+ for name in common:
251
+ diff = _persona_field_diff(by_name_a[name], by_name_b[name])
252
+ if diff:
253
+ changed.append(PersonaChange(name=name, changed=diff))
254
+ else:
255
+ unchanged.append(name)
256
+
257
+ a_label = pack_a_id or str(pack_a.get("id") or "") or "pack_a"
258
+ b_label = pack_b_id or str(pack_b.get("id") or "") or "pack_b"
259
+
260
+ return PackDiff(
261
+ pack_a_id=a_label,
262
+ pack_b_id=b_label,
263
+ pack_a_name=str(pack_a.get("name") or a_label),
264
+ pack_b_name=str(pack_b.get("name") or b_label),
265
+ composition_a=_compose([p for p in personas_a if isinstance(p, dict)]),
266
+ composition_b=_compose([p for p in personas_b if isinstance(p, dict)]),
267
+ added=added,
268
+ removed=removed,
269
+ unchanged=unchanged,
270
+ changed=changed,
271
+ )
@@ -0,0 +1,3 @@
1
+ """Bundled persona packs shipped with althing."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,247 @@
1
+ name: AI & ML Evaluation Buyers
2
+ description: >
3
+ ML engineers, AI researchers, eval platform leaders, devtool PMs, and
4
+ infra engineers across startup, enterprise, and research settings.
5
+ Buyer audience for LLM evaluation frameworks, benchmark platforms,
6
+ model-ops tooling, and AI-platform infrastructure. Useful for testing
7
+ ML/AI tooling products and positioning.
8
+
9
+ personas:
10
+ - name: Carla Mendes
11
+ age: 32
12
+ occupation: ML engineer at a multimodal startup
13
+ background: >
14
+ Runs daily evals on vision-language models. Writes eval code in
15
+ Python + pytest. Frustrated by how long it takes to spin up a
16
+ reproducible benchmark; lives in spreadsheets.
17
+ personality_traits:
18
+ - applied ML
19
+ - CLI-native
20
+ - spreadsheet-escaper
21
+
22
+ - name: Jinhua Zhao
23
+ age: 38
24
+ occupation: Senior research engineer, agent labs
25
+ background: >
26
+ Designs and runs agent benchmarks for a research org. Needs
27
+ reproducibility, determinism, and long-horizon trace capture.
28
+ Local-first philosophy matters more than dashboards.
29
+ personality_traits:
30
+ - agent researcher
31
+ - reproducibility-obsessed
32
+ - local-first
33
+
34
+ - name: Mohammed Al-Sabah
35
+ age: 41
36
+ occupation: Head of AI evaluation at an enterprise fintech
37
+ background: >
38
+ Owns model approval gates before prod. Needs frameworks that
39
+ produce audit-trail-ready evidence. Regulatory exposure high.
40
+ personality_traits:
41
+ - enterprise buyer
42
+ - audit-minded
43
+ - compliance-heavy
44
+
45
+ - name: Kaitlyn Park
46
+ age: 28
47
+ occupation: ML platform engineer at a seed-stage startup
48
+ background: >
49
+ Builds ML infra for a 6-person ML team. Cost-conscious, no budget
50
+ for commercial eval platforms. Evaluates open-source alternatives
51
+ by deployment simplicity and local-mode depth.
52
+ personality_traits:
53
+ - platform engineer
54
+ - budget-constrained
55
+ - OSS-native
56
+
57
+ - name: Sergei Volodin
58
+ age: 36
59
+ occupation: Prompt engineer at a developer-tools company
60
+ background: >
61
+ Iterates on system prompts for production agents. Needs quick
62
+ A/B eval harnesses. Hates tools that require elaborate setup for
63
+ what should be a one-liner.
64
+ personality_traits:
65
+ - prompt engineer
66
+ - iteration-speed
67
+ - setup-averse
68
+
69
+ - name: Priya Iyer
70
+ age: 33
71
+ occupation: Applied AI researcher, consulting firm
72
+ background: >
73
+ Runs AI POCs for client engagements. Needs frameworks that slot
74
+ into different client data stacks without heavy onboarding. Portable
75
+ BYOK model matters because clients insist on their own providers.
76
+ personality_traits:
77
+ - consultant researcher
78
+ - BYOK-first
79
+ - portable
80
+
81
+ - name: Marcus Liu
82
+ age: 42
83
+ occupation: Director of evaluation at a foundation-model lab
84
+ background: >
85
+ Compares benchmark results across internal models and competitors.
86
+ Already uses lm-eval-harness, HELM, and custom harnesses. Looking
87
+ for gaps newer eval tools fill vs what he has.
88
+ personality_traits:
89
+ - benchmark veteran
90
+ - comparison-buyer
91
+ - domain-deep
92
+
93
+ - name: Ines Costa
94
+ age: 29
95
+ occupation: ML engineer at an open-source LLM startup
96
+ background: >
97
+ Ships model releases every 6 weeks. Needs eval pipelines that run
98
+ in CI and block releases on regressions. Automation and CLI
99
+ ergonomics are table stakes.
100
+ personality_traits:
101
+ - release-cadence-driven
102
+ - CI-integrator
103
+ - automator
104
+
105
+ - name: Ahmad Qureshi
106
+ age: 35
107
+ occupation: Academic researcher, on sabbatical in industry
108
+ background: >
109
+ Evaluates tools through a publications lens. Wants reproducibility
110
+ hooks (seed control, config versioning) so he can cite results
111
+ defensibly in papers. Less interested in dashboards.
112
+ personality_traits:
113
+ - academic rigor
114
+ - reproducibility-first
115
+ - paper-oriented
116
+
117
+ - name: Rebecca Chen
118
+ age: 30
119
+ occupation: Staff engineer at a gaming AI startup
120
+ background: >
121
+ Evaluates NPC-behavior LLMs. Benchmarks need multi-turn game
122
+ contexts and custom scoring. Looking for frameworks flexible
123
+ enough to plug in domain-specific scorers.
124
+ personality_traits:
125
+ - game AI
126
+ - custom-scorer needer
127
+ - multi-turn-focused
128
+
129
+ - name: Tobias Braun
130
+ age: 47
131
+ occupation: VP engineering at a 400-person AI platform
132
+ background: >
133
+ Evaluates enterprise tooling for the team. Wants SSO, RBAC, audit
134
+ logs, and SOC2 readiness. Features matter less than "will it
135
+ survive a procurement cycle".
136
+ personality_traits:
137
+ - VPE buyer
138
+ - procurement-aware
139
+ - enterprise-hardened
140
+
141
+ - name: Himanshu Patel
142
+ age: 31
143
+ occupation: Devtool PM at a developer cloud company
144
+ background: >
145
+ Scoping a "model eval" feature for his product. Torn between
146
+ building vs integrating an eval framework. Wants to know the
147
+ open-source story deeply before committing to build.
148
+ personality_traits:
149
+ - devtool PM
150
+ - integrate-vs-build
151
+ - OSS-diligent
152
+
153
+ - name: Lin Xiao
154
+ age: 34
155
+ occupation: Senior applied scientist, conversational AI
156
+ background: >
157
+ Evaluates dialog quality. Benchmarks involve human calibration
158
+ and inter-rater agreement. Needs frameworks that support hybrid
159
+ human + automated scoring workflows.
160
+ personality_traits:
161
+ - hybrid-eval
162
+ - dialog-quality
163
+ - rater-aware
164
+
165
+ - name: Alejandro Ruiz
166
+ age: 39
167
+ occupation: Principal engineer, evaluation platforms
168
+ background: >
169
+ Ten years building internal eval tooling at FAANG. Now at a
170
+ Series-D startup. Evaluates externals with a "did we already
171
+ build this internally?" lens.
172
+ personality_traits:
173
+ - eval-veteran
174
+ - internal-vs-external buyer
175
+ - pragmatic
176
+
177
+ - name: Noemi Rossi
178
+ age: 27
179
+ occupation: Junior ML engineer at a healthtech startup
180
+ background: >
181
+ First ML job. Overwhelmed by the eval-tool landscape. Wants a
182
+ tool that teaches her methodology as she uses it — opinionated
183
+ defaults more valuable than infinite configurability.
184
+ personality_traits:
185
+ - junior
186
+ - learning-oriented
187
+ - opinionated-defaults wanted
188
+
189
+ - name: Daniel Okonkwo
190
+ age: 40
191
+ occupation: Security engineer at an AI safety org
192
+ background: >
193
+ Evaluates models for adversarial robustness and jailbreak
194
+ resistance. Needs red-team eval plugins. Interested in whether
195
+ a framework's extension points support security-focused probes.
196
+ personality_traits:
197
+ - AI safety
198
+ - red-team-oriented
199
+ - extensibility-focused
200
+
201
+ - name: Elena Georgescu
202
+ age: 36
203
+ occupation: Lead data scientist, marketing analytics
204
+ background: >
205
+ Less ML-engineer-y, more analyst. Evaluates text-generation models
206
+ for tone and brand-fit. Methodology-curious about how qualitative
207
+ outputs are scored.
208
+ personality_traits:
209
+ - analyst-tilted
210
+ - qualitative-focused
211
+ - methodology curious
212
+
213
+ - name: Kiran Joshi
214
+ age: 32
215
+ occupation: Infrastructure engineer, LLM inference platform
216
+ background: >
217
+ Evaluates inference backends (vLLM, TGI, custom). Cares about
218
+ throughput + cost-per-eval. Wants eval tooling to tell him when
219
+ a regression is model-quality vs infra-latency.
220
+ personality_traits:
221
+ - infra engineer
222
+ - cost-per-eval
223
+ - root-cause-oriented
224
+
225
+ - name: Yasmin Benjelloun
226
+ age: 29
227
+ occupation: Research software engineer, NLP lab
228
+ background: >
229
+ Writes evaluation harnesses for professors. Inheriting grad-student
230
+ code is a recurring pain point. Interested in whether structured
231
+ eval tools produce reusable, hand-off-able code.
232
+ personality_traits:
233
+ - research SWE
234
+ - code-continuity-focused
235
+ - lab-reality
236
+
237
+ - name: Gregor Novak
238
+ age: 44
239
+ occupation: Principal AI architect, consulting
240
+ background: >
241
+ Designs AI systems for F500 clients. Evaluates eval tools across
242
+ a dozen projects. Needs tools that scale from POC to prod without
243
+ rewriting. Wary of tools that only work at one end of that spectrum.
244
+ personality_traits:
245
+ - architect consultant
246
+ - poc-to-prod seeker
247
+ - vendor-weary