Changelog
Unreleased
v2.0.0 — 2026-08-12
Changed
- Project license changed from AGPL-3.0 to Apache-2.0. Versions before v2.0.0 remain under AGPL-3.0 for anyone who already has them — this change applies going forward, starting with this release. See License for what this means for you.
Breaking changes
distribution_summary's return shape changes:DistributionSummaryResult.distributionsis nowlist[ColumnDistribution], notlist[MetricDistribution]. Call signature stays compatible (distribution_summary(result_id)still works — the newgroup_byparameter is optional and defaults to today's["metric_name"]grouping), butmetric_name: stris replaced bygroup_key: dict[str, str], andmin_val/max_valchange fromfloat | Nonetostr | None(ISO-8601 for temporal values,str()otherwise):
# Before
for d in result.distributions:
print(d.metric_name, d.min_val) # min_val: float
# After
for d in result.distributions:
print(d.group_key["metric_name"], d.min_val) # min_val: str
distribution_summary also now pushes aggregation down to the backend via
group_by().aggregate() instead of materializing the prior result to pandas first —
transparent for correct usage, but any caller relying on client-side pandas execution
(e.g. via mocking) needs to account for the new query path.
Fixed
compute()withoutby_entityno longer fails against BigQuery withArrowNotImplementedError: Unsupported cast from int64 to null using function cast_null.entity_idwas built from a bare, untypedibis.null()whenby_entitywasn't set — DuckDB tolerates this, but BigQuery assigns its own default type to an untypedNULLliteral, which then conflicts with ibis's expectednulldtype when pyarrow materializes the result.entity_idnow uses the same explicitly-string-typed null already used formetric_format/period_start_date/period_end_date.- Non-
all_time(period-granularity) queries against non-DuckDB backends (confirmed: BigQuery) no longer fail with a dialect syntax error.QueryBuilderpreviously hand-assembled every query as a SQL string, including aVALUES-with-column-list periods CTE that DuckDB accepts but BigQuery's grammar rejects, plus hardcoded DuckDB type names. It now builds real Ibis expressions throughout — the periods table is generated server-side viaibis.range()and interval arithmetic instead, verified across DuckDB/BigQuery/Postgres. User-authored SQL (numerator/denominator/where) is spliced in viaTable.sql(), uniformly across all four fragment kinds — fixing, as a byproduct, a related bug where segmentwherefragments were always re-emitted in DuckDB's dialect regardless of the query's actual target backend. - DefinitionBot's BigQuery URI prompt row now matches its DuckDB/Postgres
siblings' pattern (
bigquery://<project>/<dataset>/<table>, all-slash), removing an inconsistent extra step (slash then dot) that caused multi-attempt retry loops.validate_spec()translates the LLM's form to the core-canonicalproject/dataset.tablebefore storage. No breaking change —ConnectionManager._parse_bigquery_uristill accepts every shape it did before. QueryBot's spec catalog no longer goes stale when it shares aSpecCachewith aDefinitionBotthat commits, updates, or deletes a spec. PreviouslyQueryBot's LLM-visible catalog was built once at construction time; a spec added by aDefinitionBotholding the sameSpecCacheinstance was immediately queryable but never appeared inQueryBot's own catalog for the rest of its session.QueryBot(andDefinitionBot) now rebuild their agent whenSpecCache.versionmoves, keeping the catalog visible starting the next turn while staying cache-eligible (no rebuild) on turns where nothing changed.list_tables()/describe_table()now return/accept a ready-to-usesource:URI instead of a bare table name, soDefinitionBotnever has to assemble one from parts. Previously both tools dealt in bare table names, requiring the LLM to reconstruct asource:URI (project, dataset, schema) from memory — confirmed live to produce fabricated, non-existent project/dataset values that passed structural validation and committed. Breaking change:describe_table's signature changes from(table_name, backend_type)to(source);DescribeTableResultdropstable_name/backend_typein favor ofsource;ListTablesResult.tables' entries are now fullsource:URIs. Asource:that doesn't resolve to a real, accessible table now failsvalidate_spec(viacolumn_errors) instead of committing with a warning.IbisConnector.get_table()now resolves correctly against a real Postgres backend — previously failed for every schema, includingpublic. The old code joined schema and table into one dotted string ("public.specs") before passing it to Ibis, which requires the schema as a separatedatabase=keyword argument.get_table()now takes(table_name, database=None)as two explicit parameters for every backend, removing the join-then-resplit round trip — and, as a byproduct, the ambiguity a Postgres quoted identifier containing a literal.would otherwise have created.TableOutOfScopeErrorand BigQuery dataset-scope enforcement are removed: confirmed live that Ibis does not enforcedatabase=against a connection's configured project/dataset, so the app-level check only blocked legitimate cross-dataset specs without adding real protection — the connection's own credentials are the actual boundary.dataset_id/project_idon a BigQuery connection remain resolution defaults only.compute()now honors a BigQuery metric/segment source's own project instead of silently substituting the connection's default project. The table-reference resolver used byQueryBuilderdiscarded asource:URI's project for BigQuery; a metric naming a different project than the connection's default silently queried the wrong (or a nonexistent) table under the connection's own project instead of failing loudly. Table reference resolution moves offQueryBuilderonto a new publicConnectionManager.resolve_table_reference(), shared bydescribe_table(),compute(), andscan().
Added
DefinitionBot.commit_specandDefinitionBot.delete_spectools.commit_spec(spec_draft_token)saves a validated draft to the sharedSpecCache— adding or updating based on live cache state at commit time.delete_spec(spec_type, name)removes a spec immediately, blocked bySpecCache's referential-integrity check if another spec still depends on it.DefinitionOutput/DefinitionPayloadgaincommitted_spec_type,committed_spec_name, andcommitted_actionfields, set when either tool succeeds.SpecCache.update()andSpecCache.remove(), plus a transactionaladd(): all three mutate-then-validate-then-commit-or-rollback againstSpecCache's existing cross-reference validator, leaving the cache untouched on a rejected mutation.SpecCache.versionis a new monotonic counter, incremented on every successful mutation (includingclear()).QueryBot.column_distributiontool. Summarizes a metric's raw source-table column (count, null_count, min/max, and — for numeric columns — mean/std/p25/median/p75, or — for non-numeric columns — distinct_count) directly against the source table, beforecompute_metricsand with nospec_tokengate. Added to let the LLM discover a metric's real column bounds (e.g. its actual timestamp range) instead of fabricating atime_window— confirmed live to otherwise produce either too narrow a window or a catastrophically wide one (['1900-01-01', '2026-08-06']) against BigQuery, the latter triggering a 10+ minute query. Accepts an optionalfilter(a plain SQL boolean predicate against the source table's own columns, e.g."order_value > 1000"); rejects any filter containing a subquery.record_intentgainscolumn_distribution_result_id, an alternative totime_window: when set,time_windowis derived server-side from a priorcolumn_distributionresult's real column bounds.resolve_intentthen validates the referenced result was computed against the same metric being resolved — a mismatch surfaces as a newNearMiss.why_notvalue,"column_distribution_metric_mismatch".DefinitionBotcan now ground a spec'swhere:threshold in real data instead of inventing a number, via three new/reused tools:date_range(bounds of a temporal column on any raw table — for cohort/window boundaries),DefinitionBot's owncompute_metrics(validates a proposed metric/slices/segment/by_entity/period_type against the catalog the same wayQueryBot'sresolve_intentdoes, then executes in one call, nospec_tokengate), andcolumn_distribution/distribution_summary— reused verbatim fromQueryBot, now shared via a newaitaem.agent.common_toolsmodule.column_distributionstays deliberately metric-only on both bots: there is no rawsource:mode, so the only path to a percentile-capable statistic is through a catalog metric.DefinitionDeps.dependent_metrics/DefinitionPayload.dependent_metricsrecord which metrics were referenced while drafting, appended only on a successfulcolumn_distribution/compute_metricscall.
v1.0.0 — 2026-07-23
This release bundles the last breaking changes expected before v1.0's stability
guarantees take effect (see Breaking changes below), alongside aitaem[agent]'s
stability declaration — from this release forward, convenience bot
constructors, primitives base classes, default tool schemas, and RunTrace/
BotResponse field shapes are semver-stable; default prompt content is explicitly not.
Breaking changes
MetricCompute.__init__tmp_dirparameter removed. Passtmp_dirtoConnectionManager()orConnectionManager.from_yaml()instead:
# Before
mc = MetricCompute(cache, conn, tmp_dir="/data/tmp")
# After
conn = ConnectionManager(tmp_dir="/data/tmp")
mc = MetricCompute(cache, conn)
ValidateSpecResult.spec_draft_tokenis no longer a constructor argument — it's a read-only property derived from the newresult_idfield:
# Before
ValidateSpecResult(spec_draft_token="dd_abc123")
# After
ValidateSpecResult(result_id="dd_abc123")
ValidateSpecResult now also rejects unknown constructor arguments
(extra="forbid"), so the old call raises ValidationError rather than
silently constructing an object with spec_draft_token=None. Only affects
direct construction of ValidateSpecResult (custom tooling, or tests
standing in for validate_spec()'s return value) — callers going through
DefinitionBot/validate_spec() see no change.
Fixed
RunTrace.tool_calls[i].result_idis now populated for tool calls that mint a newResultStoreentry (previously alwaysNoneregardless of what the tool returned).RunTrace.tool_calls[i].duration_msis now populated per tool call (previously alwaysNone— only the whole-turnRunTrace.duration_msaggregate was set). No fields were added or removed onRunTrace/ToolCall— both already existed; only their population was fixed.compute_metricsno longer permanently consumes aspec_tokenon a failed compute. Previously any exception during the compute (warehouse error, transient connection failure, etc.) burned the token, forcing a fullrecord_intent/resolve_intentround trip to retry — even though the failure had nothing to do with resolution validity. The token is now restored on failure and can be reused directly; a successful call still permanently consumes it.DefinitionBot's Anthropic prompt-cache setting now matchesQueryBot's. It previously usedanthropic_cache(a different, "automatic caching" mode) instead ofanthropic_cache_instructions, despite its docstring claiming to mirrorQueryBot's cache config — the two bots had different actual cache-hit/cost behavior for a mechanism presented as shared. No public API change; both bots now place the cache breakpoint after the last static instruction block (Layer B), excluding the per-turn dynamic date context (Layer C) from the cached prefix.
Added
ConnectionManager.__init__acceptstmp_dir: str | None = "/tmp"to control where the temporary DuckDB file is written during cross-backend compute calls. Previously this was aMetricComputeconcern.ConnectionManager.from_yaml()acceptstmp_diras a keyword argument.ConnectionManager.close_all()now also tears down the cross-backend DuckDB connection and deletes its temporary file. Anyibis.Tableobjects returned bycompute()that are backed by this database become invalid afterclose_all().aitaem.agent: tool composition primitives forQueryBotandDefinitionBot(Phase 5.2). The constructortools=[...]parameter,Bot.add_tool(), and the per-callextra_tools=[...]parameter onchat()/ask()are now functional — previously all three were accepted but silently inert, andadd_tool()raisedNotImplementedError.tools=/add_tool()register persistently for the bot's lifetime;extra_tools=is scoped to a single call. Tool-name collisions raisepydantic_ai.exceptions.UserErrorrather than being silently resolved.load_history()now warns (UserWarning) if a reloaded bundle referencesadd_tool()-added tools that aren't present after reload — the callables themselves aren't portably serializable, so pass them again viatools=[...]or re-add them to restore. GenericBot.as_tool()/add_bot()composition remains deferred (seeplans/agent_module/07-non-decisions.md, ND-11).
This work is intentionally sequenced ahead of Phase 4 (SetupBot) and
Phase 5.1. Comprehensive user testing of these composition primitives
hasn't happened yet — this release is what establishes whether bot
composition is shippable at all, even as an MVP. SetupBot isn't being
skipped outright; its need simply isn't assumed by default, and it's
picked back up on an explicit ask. See plans/28-agent-phase5.2-composition.md.
- tests/evals/ — a runnable reference harness (pydantic-evals) demonstrating
how to wire tool-selection, refusal, and deterministic-correctness evaluators
against QueryBot/DefinitionBot's RunTrace/ResultStore/BotResponse
substrate (resolves plans/agent_module/07-non-decisions.md ND-09). Runs in
CI via the new evals job against scripted FunctionModels — no live LLM
calls or API keys required. Validates that the substrate is consumable by
pydantic_evals.Evaluators, not agent behavior; point it at a live model
outside CI to evaluate actual quality. See plans/29-agent-phase6-evals.md.
- Agent module docs. A new Agent documentation section — Getting Started,
Building Your Own Bot, Evaluating Your Agent, and Stability & Limitations —
plus a full API reference page (docs/api/agent.md) covering all 33 public
symbols in aitaem.agent.__all__.
- pip install "aitaem[agent]" — a provider-neutral agent extra (Anthropic
+ OpenAI), alongside a new agent-core extra (pydantic-ai with no provider
SDK, for building/testing bots against TestModel/FunctionModel).
agent-anthropic is unchanged and remains the extra every example in this
repo is tested against.
- examples/04_evaluating_agents_example.py / .ipynb — writing
pydantic_evals evaluations against a live QueryBot, including a
pass_rate() helper for repeated-run confidence. The live-model companion
to tests/evals/'s CI-safe substrate harness.
Changed
- Example files under
examples/are now numbered (01_definition_bot_example,02_query_bot_example,03_intent_resolution_example,04_evaluating_agents_example) to suggest a reading order. No content changes to the existing three examples beyond the rename.
v0.4.0 — 2026-06-26
Breaking changes
MetricCompute.compute()returnsibis.Tableinstead ofpd.DataFrame. Call.to_pandas()on the result to materialise. When all metrics share the same source backend the Table is a fully deferred expression — no data is transferred until materialised. When metrics span multiple backends the results are materialised internally and re-exposed as a Table backed by a temporary DuckDB database managed by theMetricComputeinstance.
# Before (v0.3.x)
df = mc.compute("ctr")
# After (v0.4.0)
table = mc.compute("ctr") # lazy ibis.Table
df = table.to_pandas() # materialise when needed
MetricCompute.compute()output_formatparameter removed. The parameter had no observable effect (only"pandas"was supported and was the default). Remove it from anycompute()call sites.QueryExecutor.execute()output_formatparameter removed for the same reason.aitaem.connectors.Connectorremoved. The abstract base class has been deleted.IbisConnectoris now a plain class and the sole connector implementation.from aitaem.connectors import Connectorwill raise anImportError; remove the import and useIbisConnectordirectly.- SQL literals are now typed.
metric_formatabsent emitsCAST(NULL AS VARCHAR)(previously untypedNULL).metric_valueis alwaysCAST(... AS DOUBLE). This is transparent for most callers but affects anyone inspecting raw ibis expression schemas.
Added
MetricCompute.__init__tmp_dirparameter (str | None, default"/tmp"). Controls where the temporary DuckDB file is written for cross-backend compute calls. Set toNoneto use an in-memory DuckDB instead (safe when result sets are known to be small). The file is deleted automatically when theMetricComputeinstance is garbage collected.
v0.3.1 — 2026-06-03
Added
-
MetricCompute.scan()— pre-flight compatibility scan that introspects source table schemas and returns aScanResultwith oneCompatibilityResultper metric × slice and per metric × segment pair. Schema introspection is batched by unique source URI. -
CompatibilityResult— frozen dataclass carrying the compatibility verdict for a single metric × spec pair:compatible,valid_join_keys,missing_columns, andreason. -
ScanResult— container for the full compatibility matrix with query helpers:compatible_slices(),compatible_segments(),compatible_metrics(),for_metric(), andfor_spec().
v0.3.0 — 2026-06-03
Added
-
SegmentSpec.entity_id— required field identifying the primary key column on the DIM table. Used as the right-hand side of the generated JOIN ON condition (_dim.<entity_id>). -
SegmentSpec.join_keys— optional whitelist of fact-table FK columns that may be used as join keys for this segment. When non-empty, the join key supplied atcompute()time must appear in this list; otherwise aQueryBuildErroris raised. -
segmentsdict form inMetricCompute.compute()—segmentsnow acceptsdict[str, str] | str | None. The dict form maps exactly one segment name to an explicit fact-table FK column, enabling the same segment spec to be joined via different columns (e.g.,buyer_idvsseller_idon a transactions table). -
DIM-table JOIN in generated SQL — when a segment has
entity_idset, aitaem generates a proper JOIN from the fact table to the DIM table rather than applying segment predicates inline against the fact table. Unqualified column references invalues[].whereexpressions are automatically qualified with_dim.via sqlglot AST rewriting. -
referenced_columnsfor segment specs —ValidationResult.referenced_columnsnow includes"entity_id","join_keys"(when non-empty), and"values[i].where"keys for segment specs.
Changed (Breaking)
-
SegmentSpec.entity_idis now required. Existing segment specs without this field will fail validation with aSpecValidationError. Addentity_id: <dim_pk_column>to every segment spec YAML file. -
SegmentSpec.sourceis now used. Previously parsed but ignored,sourceis now the URI of the DIM table that will be joined at query time. Ensure it points to the correct DIM table, not the fact table. -
segmentsincompute()no longer acceptslist[str]. The parameter type changed fromstr | list[str] | Nonetodict[str, str] | str | None. Multi-segment calls are no longer supported in a singlecompute()call; callcompute()once per segment instead.
v0.2.2 — 2026-06-01
Added
ValidationResult.referenced_columns— populated on successful spec validation; adict[str, list[str]]mapping each spec field to the unqualified column names it references.Nonewhen the spec is invalid. Intended for downstream consumers who hold a warehouse connection and want to verify that every referenced column is present in the source table before computing metrics. See Column introspection for usage.
v0.2.1 — 2026-05-28
Added
-
MetricSpec.format— optional metadata field for metric value interpretation. Allowed values:percentage,absolute,ratio,currency, andcurrency:<CODE>where<CODE>is a 3-letter uppercase ISO 4217 currency code (e.g.currency:USD). Plain"currency"is valid for monetary metrics with mixed or unspecified currency. Validated at spec load time; invalid values raiseSpecValidationError. -
metric_formatoutput column — everycompute()result now includes ametric_formatcolumn (inserted aftermetric_name) carrying the spec'sformatvalue, orNonewhenformatis not set. The output schema now has 11 columns. -
hourlyperiod type —period_type="hourly"produces one output row per clock hour.time_windownow accepts full ISO datetime strings (e.g."2024-01-15T08:00:00") when using hourly granularity; plain date strings fall back to midnight. Sub-hour precision in the start value is silently truncated to the nearest full hour. -
METRIC_FORMAT_VALUES— new constant exported fromaitaem, afrozensetof the simple format values:{"percentage", "absolute", "ratio", "currency"}.
Changed (Breaking)
STANDARD_COLUMNSnow has 11 entries. Themetric_formatcolumn is inserted at index 5 (aftermetric_name). Code that relies on column position or count (e.g.df.iloc[:, 9]) must be updated.
v0.2.0 — 2026-05-27
Changed (Breaking)
MetricSpec,SliceSpec,SegmentSpec: thenamefield is now validated as a SQL identifier at load time. Names must match^[A-Za-z_][A-Za-z0-9_]*$— letters, digits, and underscores only, starting with a letter or underscore. Specs whose names contain spaces, hyphens, dots, or other characters will raiseSpecValidationErrorat load time rather than failing silently or raisingQueryExecutionErrorat compute time.
Migration: rename any affected specs.
For example: "English speaking countries" → "english_speaking_countries",
"revenue-2024" → "revenue_2024". The validation error message includes a
suggested replacement name.
SpecCache.from_yaml(),SpecCache.from_string(),SpecCache.add(): now raiseSpecValidationErrorwhen a spec with a duplicate name is loaded. Previouslyfrom_yaml()logged a warning and overwrote the earlier spec;from_string()andadd()silently kept the first. Uniqueness is enforced per spec type (metrics, slices, and segments have independent namespaces).
Migration: ensure all spec files have unique names per type. If you were relying on
the overwrite behaviour to update a spec at runtime, use cache.clear() followed by a
fresh load instead.
ConnectionErrorrenamed toAitaemConnectionErrorthroughout the library to avoid shadowing Python's built-inConnectionError.
Migration: replace any except ConnectionError or from aitaem... import ConnectionError
with AitaemConnectionError, which is now importable directly from aitaem.
Added
STANDARD_COLUMNS: list[str]is now importable directly fromaitaem. Contains the ordered list of column names thatMetricCompute.compute()always returns:period_type,period_start_date,period_end_date,entity_id,metric_name,slice_type,slice_value,segment_name,segment_value,metric_value.- Spec types (
MetricSpec,SliceSpec,SliceValue,SegmentSpec,SegmentValue) are now importable directly fromaitaem(previously only fromaitaem.specs). IbisConnectoris now importable directly fromaitaem(previously only fromaitaem.connectorsoraitaem.connectors.ibis_connector).- All exception classes are now importable directly from
aitaem(previously required internal import paths such asaitaem.utils.exceptions). PeriodType— aLiteraltype alias for validperiod_typevalues; importable fromaitaem. Use in Pydantic models or type annotations.VALID_PERIOD_TYPES— afrozenset[str]of validperiod_typevalues; importable fromaitaem. Derived fromPeriodTypeso both are always in sync.MetricCompute.compute():period_typeparameter is now annotated asPeriodType(previously barestr), enabling IDE completions and static analysis warnings.SpecCache.metrics,SpecCache.slices,SpecCache.segments— read-onlyMappingproperties for iterating over all loaded specs without individualget_*lookups.
v0.1.5 — 2026-04-22
Added
SliceSpec: new wildcard variant — setwhere: <column_name>at the spec level (instead of listingvalues) to auto-populate slice values from the column's distinct values at query time. Supports simple and dot-qualified column names.
Fixed
MetricSpec.from_yaml(),SliceSpec.from_yaml(),SegmentSpec.from_yaml(): no longer raise an unhandledOSErrorwhen a YAML string longer than the OSPATH_MAXvalue is passed. The path-existence check now wrapspath.is_file()intry/except OSErrorand falls back to treating the input as YAML content.
v0.1.4 — 2026-03-23
Changed
MetricSpec: removedaggregationfield. Aggregation type is now inferred from the SQL function embedded innumerator(anddenominator). Ratio is implied whendenominatoris present. Validation enforces that bothnumeratoranddenominator(when present) contain a recognised aggregate function call (SUM,AVG,COUNT,MIN,MAX).
Migration guide
- Remove
aggregation:from all metric YAML specs. - Ensure
numerator(anddenominatorwhen present) contain an explicit aggregate function call such asSUM(col),AVG(col),COUNT(*),MIN(col), orMAX(col).
Added
MetricSpec: new optionalentitiesfield — declares which entity columns the metric supports for disaggregation (e.g.entities: [user_id, device_id]). Must be a non-empty list if provided.MetricCompute.compute(): newby_entityparameter — groups results by an entity column declared in each metric'sentitieslist; raisesQueryBuildErrorif any metric does not support the requested entity column.- Standard output schema gains an
entity_idcolumn (position 4, betweenperiod_end_dateandmetric_name);Nonewhenby_entityis not set. - Added PostgreSQL backend support via
ibis-framework[postgres](pip install "aitaem[postgres]") - New
aitaem.connectors.backend_specsmodule withDuckDBConfig,BigQueryConfig, andPostgresConfigdataclasses — centralizes backend field validation for all connectors - PostgreSQL source URI format:
postgres://schema/table(e.g.postgres://public/orders)
v0.1.3 — 2026-03-17
- New
aitaem.helpersmodule for user-facing convenience functions - New
load_csvs_to_duckdb(csv_path, db_path, overwrite=True)helper — loads a single CSV or all top-level CSVs in a folder into a DuckDB file and returns a connectedIbisConnector MetricSpec: unknown-fields check now usesdataclasses.fields()instead of a hard-coded set- README: updated CSV loading example to use
load_csvs_to_duckdb
v0.1.2 — 2026-03-14
- Updated installation instructions to use PyPI
- Added CI, PyPI version, and Python version badges to README
v0.1.1
- Bug fixes and internal improvements
v0.1.0 — Initial release
MetricSpec,SliceSpec,SegmentSpecwith YAML parsing and validationSpecCachewith eager loading from files, directories, or stringsConnectionManagerwith DuckDB and BigQuery supportMetricCompute— primary user interface for computing metrics- Cross-product (composite) slice support
- Standard 9-column output DataFrame
- Example ad campaigns dataset with sample YAML specs
For full release diffs, see GitHub Releases.