Release Notes — v0.2.2
Highlights
This release makes the accelerator deployable outside the US without editing source, makes non-Latin questions actually retrievable, and adds an opt-in agentic Tier-2 strategy for hard analytic SQL.
Every Bedrock model ID is now a deploy-config key, and the resolved values drive the container environment, the IAM grants, and the cost dashboards from one place — so a non-US deployment no longer has to edit six files and still end up with dark widgets. Tier-2/Tier-3 retrieval gains bidirectional label containment, which is what lets a Chinese, Japanese, Thai, Korean or Indic-script question reach a stored label the serve layer cannot segment. Tier 2 also gains a caller-pinnable options.strategy, including a bounded tool-use agent that discovers schemas, generates, executes and self-corrects.
It closes 3 field-filed bugs (#92, #94, #95), removes the last AGPL-licensed dependency from the document pipeline, fixes a family of silent failures (a truncated LLM generation reported as HTTP 200, a row cap that landed inside a SQL comment, matched classes dropped past a row limit, a masked answer counted as allowed), and adds ~6,300 lines of unit tests.
What's New
Deploy outside us-east-1 with no source edits
-
Every Bedrock model ID is now an SSM deploy-config key (
/{prefix}/config). Each falls back to its existing default, so a deployment that sets none of them behaves exactly as before.Config key Sets the model for bedrockLlmModelIdServe query LLM (NL→SPARQL, synthesis) bedrockEmbedModelIdAll embeddings — induction, doc-KG-build, metric matching, serve retrieval bedrockEmbedDimensionsVector dimension for the embedding model above bedrockInductionLlmModelIdInduction, grounding rerank, description generation bedrockChatModelIdSource enrichment, constraint inference, doc-ingestion extraction -
Both geographic inference profiles (
us./eu./apac./jp./global.) and bare in-region model IDs are accepted — bare IDs matter because some models publish geo profiles for only a subset of regions. A newbedrockModelArn()helper picks the right ARN shape per form (inference-profile/…vsfoundation-model/…), so IAM grants can no longer fail closed on the wrong shape. -
Model IDs are resolved once per stack and reused for the container environment, the Bedrock IAM grants, and the CloudWatch dashboards'
ModelIddimensions — previously duplicate literals, so changing a model left cost widgets querying a model nothing invokes. -
Preflight got two new checks: CDK bootstrap in both the deploy region and
us-east-1(the*-edge-wafstack always deploys there), and a warning when a configured model ID isn't listed in the deploy region.
Multilingual query understanding
- Bidirectional label containment in Tier-2 T-Box fallback and Tier-3 keyword entity search. The forward direction (
stored_label CONTAINS term) is unchanged; the new reverse direction (query_run CONTAINS stored_label) finds a stored label sitting inside a query word — which is what a scriptless-boundary language requires, since the serve layer cannot segment Chinese/Japanese/Thai/Khmer or strip the particles Korean, Hindi and Bengali agglutinate onto a noun. No per-language segmenter, morphology table, or stop-word list is needed: the graph already stores the exact words as labels. - Tokenization moved to Unicode extended grapheme clusters (UAX #29, via
regex). Pythonre's\wexcludes combining marks, which split Thai/Indic words and splitİstanbulafter lowercasing. - Latin and Greek are deliberately excluded from reverse containment (reliable whitespace boundaries; Greek inflection rewrites the ending rather than appending). Cyrillic is included because case forms append to the nominative stem.
Opt-in agentic Tier-2 strategy
options.strategylets a caller pin how Tier 2 produces its query:best,ontop,nl_to_sql,ontop_first,nl_to_sql_first(default, unchanged), and the newagentic.agenticis a bounded Strands tool-use agent that starts from pre-fetched candidate tables and schemas, discovers more as needed (search_tables/get_table_schema), delegates generation (generate_sql), then executes and self-corrects (execute_sql). It has no raw-SQL execution path:generate_sqlreturns a handle, and only a generated statement can be executed. It is never part of an automatic or fallback chain — it runs solely when pinned. The loop is bounded by the request deadline andSERVE_AGENTIC_EXEC_TIMEOUT_S, not by a turn count.- New reusable Tier-2 tooling (
tier2/tools/{sql_tools,table_tools,graph_tools}.py) and a shared execute-with-authz primitive (sql_execution.py) sit under both the agentic and flat paths. - Tuning knobs:
SERVE_AGENTIC_PREFETCH_SCHEMAS,SERVE_AGENTIC_EXEC_TIMEOUT_S,SERVE_AGENTIC_GRAPH_TRAVERSAL,SERVE_AGENTIC_INTENT_REVIEW.
Experimental: ontology FK-graph expansion for flat NL→SQL
SERVE_NL2SQL_GRAPH_EXPAND(deployment-wide) /options.flatGraphExpand(per request) appends the tables one induced foreign-key hop from the retrieved classes — with their columns — to the flat NL→SQL schema context. The flat path retrieves once and never re-retrieves, so a table similarity didn't rank has no other route into the prompt.- Measured +6 to +12.5pp retrieval full-hit across three cells (pooled Spider 2.0, per-DB Spider 2.0, BIRD) and +2.2 to +5.2pp execution accuracy in all four Spider 2.0 arm×seed comparisons (44 win / 25 loss, p=0.030) at unchanged latency. Ships off: no single cell reaches significance on its own and BIRD is a two-repeat wash. Capped by
SERVE_NL2SQL_GRAPH_EXPAND_MAX_TABLES(default 15).
Guardrail outcomes are now separable
GuardrailInvocationsgains aDecisiondimension covering ALLOW / ANONYMIZED / BLOCK / UNKNOWN / MODEL_FILTERED, plus a newGuardrailUnknownmetric.- Masking (PII
NAME→{NAME}) is no longer invisible — an NTSB/TimeBank benchmark found 68% of one arm's answers silently carrying placeholders that no existing metric would have surfaced.
Ontology induction reports what it dropped
InductionReportgainsdroppedTables(tableName+reason, e.g.gen_llm_empty/parse_failed), so a table the generation LLM silently failed on is now reported instead of quietly missing from the proposal. Empty for strategies that never drop tables.
Ontology review UI: ambiguous grounding is visible
skos:relatedMatchnow renders as an "Ambiguous" origin badge ("low confidence, review before accepting") in the proposal table, class detail panel, and grounding tab, sorting between Grounded and Novel.
Bounded query length at every boundary
- A new
NaturalLanguageQuerySmithy shape and sharedvalidate_query_text()bound NL queries at 4,000 code points (not bytes, so the allowance doesn't shrink for non-Latin scripts) at the Data Layer, the Serve entry points, and the request model. The API's WAF separately caps the request body at 8,192 bytes; that ceiling is now documented with measurements so a generated client doesn't accept 4,000 characters and then surface an unexplained 403.
Documentation
- Oracle and Snowflake are documented as federation-only — no direct-SQL driver on the serve path, so every query runs through the Athena federated catalog and
queryEngineis alwaysATHENA. - Snowflake onboarding is fully specified:
warehouseis mandatory (federation is rejected at connection-creation time without it), the optionalroleis a Snowflake RBAC role honored during discovery only, port 443 + port 80 (OCSP) egress and theconnector_ocsp_egress/connector_egress_cidrscontext keys, plus two new troubleshooting rows. - The "Where the model IDs live" section is rewritten around the new config keys, with an
ap-northeast-1example and a warning that nothing validates model IDs at synth time.
Groundwork: custom Athena federation connectors
- API contract for a new
ATHENA_CONNECTORdatabase sub-type (AthenaConfiguration:metadataFunctionArn, optionalrecordFunctionArn,databaseName, table filters), a validatedLambdaFunctionArnshape that rejects Lambda's partial-ARN and name-only forms, the Athena data-catalog lifecycle IAM grant, and a parser for@pk/@fk(...)constraint tags carried in column comments (the federation protocol has no field for key constraints). Not yet onboardable — the registrar handler lands in a following release.
Bug Fixes
Deployment & Regions
- Fixed every MCP tool failing outside
us-east-1.MCPConfig.aws_regiondefaulted to a hardcodedus-east-1unlessSCL_AWS_REGIONwas set — and nothing ininfra/sets it (the MCP stack setsAWS_REGION). Every downstream call (Lambda invokes, Context Manager endpoint) was pinned tous-east-1. Region now resolves through a sharedresolve_region()(AWS_REGION→AWS_DEFAULT_REGION→us-east-1) viadefault_factory, keeping pydantic's env precedence intact. The same fix applies to the sharedSCLConfig. (#92) - Fixed deploying to a non-US region requiring source edits. Model IDs were hardcoded literals scattered across CDK stacks, Python runtime fallbacks and TypeScript constants, so a non-US deploy meant editing source in several places — and missing one left that path invoking a
us.profile that Bedrock rejects at first invocation withValidationException. Three previously unset paths are now configured explicitly: table enrichment (BEDROCK_CHAT_MODEL_ID, which had no model env var and always took theus.default), doc-KG-build embedding, and the doc-ingestion trigger'sBEDROCK_MODEL_ARN. (#94, supersedes #93) - Fixed the deploy failing partway through on the
*-edge-wafstack for non-us-east-1deployments. That stack always deploys tous-east-1, so two bootstraps are required; the docs showed one and preflight didn't check. Preflight now verifies both regions and fails fast, anddeploying.mddocuments it.
Query & Retrieval (Serve)
- Fixed the Tier-1 residual-qualifier gate being inert for Korean. The gate's tokenizer was ASCII-only (
[a-z0-9_%$]+), so a Korean question produced zero residual tokens and sailed through as if it had no unconsumed qualifiers — the exact confident-but-wrong metric answer the gate was added in v0.2.1 to prevent. Hangul syllables are now tokenized, and Korean question scaffolding (request verbs, question words, aggregate-restating counters, connectives) is added to the stop-word groups. (#95 — Korean; Han/Kana/Thai tokenization remains open) - Fixed NL→SQL silently truncating long analytic queries. Generation was capped at 1,024 output tokens. Bedrock returns a cap-hit generation as a partial body with HTTP 200, and the extractor accepted it — so the symptom was a confusing execution-time syntax error, not a generation error. Measured against 867 generated Spider 2.0 queries, ~5% exceed 1,024 tokens of SQL alone, and on a reasoning model (which spends the same budget on thinking blocks first) roughly half exceeded what was left. The cap is now 4,096, configurable via
SERVE_NL2SQL_MAX_TOKENS(clamped to 512–32768, clamping logged), and truncation is surfaced explicitly (ConverseResult.stop_reason/truncated, plus annl_to_sql_generation_truncatedwarning). - Fixed the Athena row cap being silently removed by a trailing SQL comment. When the LIMIT couldn't be injected via the parse tree, it was appended on the same line — and LLM-generated SQL frequently ends with a
-- commentline, so the cap landed inside the comment. The query still executed, just unbounded. The fallback now appendsLIMITon its own line. - Fixed matched classes silently vanishing from the Tier-2 prompt. The keyword-fallback T-Box query joined classes against a datatype-property
OPTIONAL, so one class fanned out to (properties × distinct values × parents × labels) rows; with noORDER BY, a class whose rows all landed past the 2,000-row cap disappeared entirely. Reverse containment made this reachable (it matches far more classes, and a multilingual namespace carries one label per language). Now split into aDISTINCTclass query plus a bounded property query, de-duplicated before theVALUEScap, with truncation logged rather than silent. - Fixed the SQL firewall rejecting already-transpiled statements as unparseable.
evaluate()always parsed astrino; it now takes adialectso the direct-JDBC path can pass the engine's own dialect.
Guardrails & Observability
- Fixed a masked answer being counted as allowed.
stopReason=guardrail_intervenedis used for masking and blocking alike, so the two were indistinguishable. Outcomes are now classified (GuardrailOutcome: NONE / ANONYMIZED / BLOCKED / UNKNOWN) and a merely-anonymized answer is no longer discarded as blocked content. - Fixed an unconfirmed suppression being reported as a confirmed policy block. A guardrail that intervened without returning a trace can't be classified; the call is still suppressed (fail closed) but now increments
GuardrailUnknowninstead ofGuardrailBlocked, so the two numbers stay recoverable. A non-zeroGuardrailUnknownmeans traces stopped arriving — previously that destroyed answers silently. - Fixed a model's own output filter being read as a guardrail block.
stopReason=content_filteredis native model-side filtering, not a Bedrock Guardrail policy action. It now raises a typedContentFilteredErrorand reportsMODEL_FILTERED. - Guardrail trace logging now emits redacted assessment actions — traces carry the raw matched text.
Documents (Ingestion)
- Removed the last AGPL-licensed dependency. PDF scanned-detection and 300-DPI page rendering moved from PyMuPDF (AGPL-3.0) to pypdfium2 + pillow (permissive).
owlready2(LGPL-3.0) is now the only entry in the external-dependency license table. - Fixed a malformed PDF raising a raw PDFium traceback. Open/extract/render failures are now caught and logged with a reason: scanned-detection reports "scanned" (a usable verdict), page count reports 0, and Textract rendering returns empty rather than crashing the task.
- Added resource guards on PDF rasterization. A few thousand pages, or one page with absurd dimensions, compresses to almost nothing on disk but expands to gigabytes of bitmap at 300 DPI — enough to OOM the task. Now capped at 1,000 pages and 50 MP per rendered page, both checked before any bitmap is allocated, with a clear per-file error. Native bitmap and textpage handles are closed explicitly rather than left to GC.
Sources (API)
- Fixed
GET /sources/{id}returning 500 for a database sub-type with no config mapping. The sources table stores the config blob in one untyped column, and anything that wasn'tJDBC_DATABASEwas reported asglueConfiguration— which requirescatalogIdandregion, so response validation failed and a cosmetic mislabel became a 500. Sub-type → config member is now an explicit map, with an unmapped sub-type logged rather than silently read as Glue.
Ontology Review UI
- Fixed a class with only a sibling-proposal
skosmatch rendering as "Grounded" with a blank target. Abody.includes(...)fallback setmatchTypeeven when no foundational target resolved, somatchTypeandgroundedTocould disagree.matchTypeis now derived solely from the resolved foundational match, so a sibling-only (dedup) match correctly renders "Novel".
Dependency & Base Image Updates
- All six service images rebased onto a newer pinned
python:3.12-slimdigest (context-manager, ontology-engine, vkg, sources DB-enrichment, doc kg-build, doc preprocessing). pypdfium2>=4.30.0+pillow>=12.3.0replacePyMuPDF>=1.28.2in document preprocessing.regex==2026.5.9added to context-manager for UAX #29 grapheme segmentation.
Upgrading from v0.2.1
make deploy-dev — no migration steps for an existing us-east-1 deployment. Defaults are unchanged, options.strategy defaults to today's nl_to_sql_first, and both graph-expansion flags ship off.
Two things to know before you set the new config keys:
bedrockEmbedModelIdandbedrockEmbedDimensionsare initial-deploy settings. All embeddings must come from the same model, and the dimension is baked into the OpenSearch index at creation. Changing either on a deployment that already holds data is a re-ingestion, not a config change.- Nothing validates model IDs at synth time. A deploy with unusable IDs still reaches
CREATE_COMPLETEand fails at first invocation. Check availability in your target region (aws bedrock list-inference-profiles,aws bedrock list-foundation-models) — preflight now warns, but only when the AWS CLI andjqare available.