aegean.ai¶
ai ¶
Multi-provider AI layer — grounded, exploratory-labeled.
Providers: Anthropic (default), OpenAI, xAI Grok, Google Gemini, OpenRouter, and local
(a locally hosted OpenAI-compatible endpoint: Ollama, LM Studio, llama.cpp, vLLM). Each is an
optional extra, lazily imported. Capabilities: translate, gloss,
decipher_hypotheses, nlp_assist, ask, summarize. Every generative output is an
ExploratoryResult with provenance and an unverified flag.
from aegean import ai
client = ai.get_client("anthropic") # needs pyaegean[anthropic] + a key
result = ai.translate("μῆνιν ἄειδε θεά", client=client)
print(result.labeled()) # carries the EXPLORATORY tag
ResponseCache ¶
Get/set completions by content hash, optionally persisted to JSON.
AIError ¶
Bases: RuntimeError
Base class for AI-layer errors.
ExploratoryResult
dataclass
¶
ExploratoryResult(text: str, kind: str, provider: str, model: str, prompt_version: str, grounding: tuple[GroundingItem, ...] = (), exploratory: bool = True, data: Any = None, grounding_runtime: dict[str, Any] | None = None, draft: ExploratoryResult | None = None)
A generative result, explicitly labeled exploratory and provenanced.
grounding is the structured corpus/lexicon/analysis evidence fed to the
model (each a GroundingItem with a source and a ref). Use labeled when
surfacing to a user so the caveat travels with the text, trace to audit
which local evidence grounded the output, and data (when set by a structured
capability) for the parsed JSON payload. grounding_runtime retains the
complete local execution provenance; a compact backend/coverage summary may
also appear among the prompt evidence.
draft retains the complete original result when a translation receives
an optional model checking pass. Translation audit data separates the final
wording, model-reported uncertainty and changes, and the exact raw response;
a structured format status validates its shape, not the translation.
trace ¶
A human-readable provenance trace: the generative step and the local, non-generative evidence that grounded it, grouped by source.
Makes the exploratory result auditable — every grounding line names the source (corpus, lexicon, analysis step) and the ref it came from, so a reader can check the output against the evidence it was given rather than taking it on trust.
to_dict ¶
A stable, JSON-ready serialization. Keeps the exploratory flag, the text, the
grounding, and any structured data — so a saved AI result can never be mistaken
for ground truth when read back.
to_json ¶
Serialize to JSON: returns the string, or writes it to path and returns None.
from_dict
classmethod
¶
Reconstruct an ExploratoryResult from to_dict output.
LLMClient ¶
LLMClient(model: str | None = None, *, api_key: str | None = None, cache: ResponseCache | None = None)
Bases: ABC
Abstract provider client. Subclasses implement _complete.
complete ¶
complete(prompt: str, *, system: str | None = None, max_tokens: int = 1024) -> LLMResponse
A cached single-turn completion (cache is keyed on provider identity/model/ system/prompt/max_tokens so re-asking is free and deterministic).
LLMResponse
dataclass
¶
A raw completion from a provider.
ProviderCallError ¶
Bases: AIError
Raised when a provider's API call fails (bad model id, authentication, rate
limit, network). Wraps the SDK's exception so callers see one AIError type
instead of a provider-specific traceback; the underlying error is the __cause__.
CaseResult
dataclass
¶
CaseResult(name: str, used: tuple[str, ...], missing: tuple[str, ...], fabricated: tuple[str, ...], groundedness: float, clean: bool, text: str)
The scored outcome of one case.
EvalReport
dataclass
¶
EvalReport(cases: tuple[CaseResult, ...], groundedness: float, fabrication_rate: float, n: int = 0)
Aggregate over a case set: mean groundedness and the fabrication rate
(fraction of cases where any must_avoid appeared).
GroundingCase
dataclass
¶
GroundingCase(name: str, prompt: str, grounding: tuple[str | GroundingItem, ...] = (), must_use: tuple[str, ...] = (), must_avoid: tuple[str, ...] = (), kind: str = 'ask', note: str = '')
One eval case: a prompt, the evidence to feed, and the facts a faithful answer should use / must not fabricate.
kind picks the capability (ask / decipher / gloss /
summarize / translate). must_use are strings a grounded answer
should reference; must_avoid are strings that, if present, signal the
model went beyond (or against) its evidence.
GroundingItem
dataclass
¶
GroundingItem(content: str, source: str = 'custom', ref: str = '', status: str = 'unclassified', provenance: dict[str, Any] | None = None)
One piece of grounding evidence and its provenance.
content is the evidence text; source is the provenance category
(e.g. "corpus:lineara", "lexicon:LSJ", "lemmatizer",
"transliteration", "analysis:cooccurrence"); ref is the specific
locator it concerns (a word, lemma, or document id). status distinguishes
predictions, source annotations, and field-level human review; provenance
carries supporting details. Both reach the model through evidence_block.
Plain strings remain unclassified evidence, never implicitly verified.
Stringification retains the historical content-only behavior.
RegimeSignal
dataclass
¶
An estimate of whether lexical grounding helps this text (EXPLORATORY).
regime is "help" / "neutral" / "hurt"; rarity,
polysemy, and markedness are the three component signals (each
0..1-ish), and content_words is how many content words fed the estimate.
A difficulty signal, not a measured accuracy.
SenseCandidate
dataclass
¶
SenseCandidate(marker: str, level: int, gloss: str, score: float, overlap: tuple[str, ...], dominant: bool)
One ranked LSJ sense of a word (EXPLORATORY: a hypothesis, not a decision).
marker/level/gloss mirror the underlying greek.lexicon.Sense;
score is the context-fit total; overlap are the context lemmas (accent-
stripped) that the sense's cited Greek shares; dominant flags the order-1
sense (the gloss content_glosses would have used).
ask ¶
ask(question: str, *, grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Answer a question over corpus/commentary grounding.
decipher_hypotheses ¶
decipher_hypotheses(text: str, *, grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Offer decipherment hypotheses for an undeciphered (Linear A) sequence, each tied to cited corpus evidence. Strictly exploratory.
extract ¶
extract(text: str, *, instruction: str = 'Extract the structured data from the following.', schema: Mapping[str, str] | str | None = None, grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Ask for structured (JSON) output and parse it into result.data so
the AI layer can feed a pipeline or database.
schema describes the wanted shape — a mapping of field → description
(rendered as a field list) or a free-form shape string — and is appended to
instruction. The model is told to return JSON only; the response is
parsed leniently (parse_json). result.data is the parsed value (or
None if the model didn't return parseable JSON — result.text always
has the raw response). Still exploratory and grounded like every capability.
r = extract("KN Fp 1: OLE S 1", schema={"commodity": "ideogram", ... "amount": "number"}, client=client) # doctest: +SKIP r.data # doctest: +SKIP
gloss ¶
gloss(text: str, *, source: str = 'Ancient Greek', grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Produce an interlinear, word-by-word gloss of the source text.
nlp_assist ¶
nlp_assist(text: str, *, task: str = 'lemma and POS disambiguation', grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Ask the model to disambiguate an NLP analysis (lemma/POS/parse) where the rule-based pipeline is uncertain.
parse_json ¶
Best-effort parse of a JSON value from a model response. Returns None
(never raises) when nothing parseable is found.
Tolerant of the ways models wrap JSON: a `json fenced block, or prose
around a bare object/array. Tries the fenced content, then the whole string,
then the outermost/[...]`` slice.
summarize ¶
summarize(text: str, *, grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Summarize a corpus excerpt or commentary.
translate ¶
translate(text: str, *, source: str = 'Ancient Greek', target: str = 'English', style: str = 'annotated', grounding: Grounding = (), source_context: str | None = None, max_tokens: int = 2048, client: LLMClient | None = None) -> ExploratoryResult
Translate into a separately retained translation and model-reported notes.
style selects concise or annotated presentation. Both retain uncertainty,
change information and the exact provider response in result.data. Invalid
structured responses remain raw text with an explicit format status; no extra
provider call attempts to repair their format. max_tokens is the positive
integer output-token limit (default 2048); it does not trigger retries.
source_context is separately fenced interpretive background for referents,
speakers and meaning. Only text is translated. Every supplied string,
including blank/whitespace strings, is retained exactly in
result.data['source_context']; None supplies no context and retains
the existing context-free prompt and data shape. Context is untrusted data,
not directions or a license to invent unprovided source history.
verify_translation ¶
verify_translation(text: str, draft: str | ExploratoryResult, *, source: str = 'Ancient Greek', target: str = 'English', style: str = 'annotated', grounding: Grounding = (), source_context: str | None = None, max_tokens: int = 2048, check_policy: str = 'standard', client: LLMClient | None = None) -> ExploratoryResult
Check a draft translation against provisional grounding and repair only definite contradictions, retaining the draft, notes and reported changes.
The model is shown the source text, the draft, and the local analysis
(morphology, syntax, dictionary glosses, idiom meanings), and is asked to fix
only clear errors the analysis contradicts (wrong voice, subject/object, case
relation, a rare word's or idiom's sense, an omission or addition) and
otherwise keep the draft. This is the repair half of a translate-then-check
pass: the grounding never touches the draft, so it cannot bias it, though a
wrong analysis can still mislead the repair. The result is a
translate-kind ExploratoryResult carrying the grounding and structured
translation audit data. An ExploratoryResult draft retains its complete
original provenance in result.draft; a string draft is retained exactly
in result.data['draft_text'] without inventing provenance.
text_changed compares exact wording only when both responses are structured;
change_report_status flags changed wording omitted from the model's report.
max_tokens is the positive integer output-token limit (default 2048),
independently supplied for this checking call; no format-repair call is added.
source_context=None inherits a saved result's recorded source context,
including an exact blank string. An explicitly supplied non-None context must
equal any recorded context exactly; a mismatch raises before the provider call,
and None cannot clear inherited context. Saved results without a context key
can receive explicit context, without asserting what their original prompt used.
String drafts use only the explicit argument. Supplied context and draft text
are retained exactly; context guides interpretation but is never translated or
followed as instructions.
check_policy='clauses' additionally requests a model-reported audit of
source propositions, roles, negation, qualifications and speech acts. It uses
the same single completion, preserving exact source wording and occurrence
references. Local validation checks literal spans and supplied references,
not meaning or scholarly correctness. Missing/invalid audit data remains
explicit without discarding an otherwise structured translation. The default
'standard' retains the existing checking policy and response shape.
get_client ¶
get_client(provider: str = 'anthropic', *, model: str | None = None, api_key: str | None = None, cache: ResponseCache | None = None) -> LLMClient
Construct a client for provider (default Anthropic). Importing
aegean.ai registers all built-in providers.
register_provider ¶
Register an LLMClient subclass under its provider name (each adapter calls this).
run_eval ¶
run_eval(cases: Sequence[GroundingCase], client: LLMClient) -> EvalReport
Run each case through its capability with client and aggregate.
Needs a working LLMClient (a provider with a key, or a stub). Returns an
EvalReport with mean groundedness and the fabrication rate — the AI
layer's analogue of the lemmatizer's held-out accuracy.
score_text ¶
score_text(text: str, case: GroundingCase) -> CaseResult
Score one answer against a case (case-insensitive substring containment).
list_providers ¶
Sorted names of registered providers, e.g. ['anthropic', 'gemini', 'grok', 'local', 'openai', 'openrouter'].
annotation_evidence ¶
annotation_evidence(tokens: Iterable[Token], *, source: str = 'annotation', source_annotated: bool = False, fields: Sequence[str] = ('lemma', 'upos', 'morph', 'feats', 'head', 'deprel')) -> list[GroundingItem]
Render existing token annotations without rerunning or approving analysis.
Set source_annotated=True when the tokens retain annotations from the
named source edition. Missing provenance otherwise remains unclassified:
absence of model metadata does not establish annotation origin.
Only a current per-field accept/correct decision with its
verification flag grants human-reviewed status. Deferred, rejected and stale
decisions remain visible. Reviewing a lemma never certifies adjacent fields.
Pass the returned items as grounding= to an AI or hybrid translation call.
as_item ¶
as_item(x: str | GroundingItem) -> GroundingItem
Coerce a string or GroundingItem to a GroundingItem (strings become
source="custom").
clean_gloss ¶
Reduce a raw dictionary line to its bare English meaning, or "".
Concise dictionaries (Middle Liddell, Cunliffe, Abbott-Smith, Dodson) are the right
source for grounding a translator, but their lines carry apparatus a model should not
see asserted as the meaning: a leading headword: repeat, the lemma's Greek
etymology run, = X cross-reference redirects, and editorial-abbreviation leads.
This strips those and returns the first English clause, length-capped. A trailing
parenthetical that opened an etymology/cross-reference note (…, reckoning (cf. λέγω…))
is dropped whole rather than left dangling as …, reckoning (cf when the Greek inside
it is cut. Returns "" when nothing definition-like survives (a bare redirect, a
Greek-only line, or an etymology note), so the caller can fall through to the next
dictionary rather than inject a non-gloss.
concise_gloss ¶
A cleaned, concise, common-sense-first gloss for lemma, or "".
Cascades over the loaded concise dictionaries (Middle Liddell, Cunliffe for Homer,
Abbott-Smith / Dodson for the NT) via greek.gloss(lemma, dictionary=...), cleans each
candidate with clean_gloss, and returns the first that survives. Requires at least one
of those concise dictionaries to be loaded: this gloss is never taken from LSJ. LSJ
orders senses etymologically, so its lead sense is frequently the archaic one (καιρός =
"row of thrums in a loom", βίος = "bow", λόγος = "computation"), and asserting that as
the meaning injects exactly the errors this layer exists to avoid; emitting nothing is
strictly better than emitting the archaic trap. So with only use_lsj() loaded and no
concise dictionary, this returns "" and the caller omits the gloss rather than
grounding on a misleading sense. Only whichever concise dictionaries are actually loaded
are consulted; a dictionary that is registered but not active is skipped, never raised on.
Returns "" when no loaded concise source yields a clean gloss.
content_glosses ¶
content_glosses(text: str, *, max_senses: int = 6, limit: int = 20, skip_lemmas: frozenset[str] | None = None, source: str = 'lsj', rarity_gate: bool = False, analysis: Sequence[TokenRecord] | None = None) -> list[GroundingItem]
Gated dictionary glosses for the content words of text — grounding that
helps a model without misleading it.
Two gloss sources:
source="lsj"(default, legacy): for each content word (not a function word, deduped by lemma) that has an LSJ entry with at mostmax_sensessenses, emit one concise dominant-sense gloss. The polysemy cap is deliberate: a first-sense gloss for a highly polysemous word (στάσις, κρίσις, ἄρουρα) is often the wrong contextual sense, so those are left to the model's own reading; obscure, dominant-sense vocabulary is where a gloss adds real signal. Requires the LSJ lexicon (greek.use_lsj()); empty without it. Source taglexicon:LSJ.source="cascade"(recommended): gloss each content lemma from a concise, common-sense-first dictionary cascade (Middle Liddell, Cunliffe for Homer, Abbott-Smith / Dodson for the NT), cleaned (seeconcise_gloss). These are dictionary candidates, not verified contextual senses. A historical LSJ lead sense can differ from the sense appropriate to this passage. It therefore requires a concise dictionary and never falls back to the LSJ first sense: with onlygreek.use_lsj()loaded and no concise dictionary, the cascade emits nothing for that lemma rather than the archaic LSJ-lead trap. Uses whichever concise dictionaries are loaded and never requires a specific one. Source taglexicon:concise.
rarity_gate (cascade source) restricts glossing to the text's rare content lemmas,
measured against the Greek NT via greek.terminology_rarity: a gloss helps most on the
rare words and is noise on common ones (πολύς, λόγος). An all-common passage is therefore
glossed not at all, not glossed wholesale. It degrades to glossing every content lemma
only when no reference corpus is available offline (the rarity signal is absent), never
raising.
analysis may supply an already-computed GreekPipeline result. When present,
those contextual lemmas and parts of speech drive both filtering and rarity gating;
no module-level analysis backend is consulted. This lets an isolated pipeline remain
isolated through translation grounding.
skip_lemmas is an optional set of lemmas to not gloss — pass a high-frequency
lemma list to focus grounding on genuinely rare words. The package bundles no such list
(frequency is corpus- and register-dependent); supply one, use rarity_gate=True, or
omit it.
Best-effort throughout. Gloss coverage on rare or inflected forms depends on the active
lemmatizer: the joint neural pipeline (greek.use_neural_pipeline()) gives
sentence-contextual lemmas and POS-based function-word filtering; the neural lemmatizer
generates lemmas for unseen forms; the AGDT treebank folds inflections attested in the
literary corpus; the baseline seed table misses most. ref = the surface word.
cooccurrence_evidence ¶
cooccurrence_evidence(corpus: object, word: str, *, limit: int = 12) -> list[GroundingItem]
Grounding for an undeciphered-script query: the words that most often
share a document with word. Source analysis:cooccurrence,
ref=word. Empty if word co-occurs with nothing.
corpus is a Corpus, the results of a Corpus.query (whose matched documents
are its inscriptions), or a list of documents, so grounding can be drawn from a
queried subset and reflects the co-occurrence in that subset. Anything that is not a
body of documents raises a TypeError naming what arrived: an empty grounding list
reads as "this word co-occurs with nothing", which a wrong argument must never say.
corpus_context ¶
corpus_context(corpus: object, *, limit: int = 20) -> list[GroundingItem]
A small grounding context from a corpus: its most frequent words.
Kept deliberately small — this is seed grounding, not retrieval. Accepts any
object exposing word_frequencies() (e.g. aegean.Corpus); the source is
tagged corpus:<script_id> so the trace names the corpus.
evidence_block ¶
evidence_block(evidence: Iterable[str | GroundingItem]) -> str
Render grounding evidence as a compact, labeled bullet list (or empty).
Status, source, locator and supplied provenance travel with each evidence line. They describe evidence, not instructions; capabilities wrap this block as untrusted data before sending it to a provider.
lexicon_evidence ¶
lexicon_evidence(words: Iterable[str], *, limit: int = 20) -> list[GroundingItem]
Grounding from the active LSJ lexicon: a short gloss per word that has an
entry. Returns nothing if the lexicon isn't loaded (greek.use_lsj()) —
grounding is best-effort, never a hard dependency. Source lexicon:LSJ.
wrap_untrusted ¶
Delimit untrusted source text with an explicit do-not-follow note.
idiom_glosses ¶
idiom_glosses(text: str, *, analysis: Sequence[TokenRecord] | None = None) -> list[GroundingItem]
Detect curated Greek idioms in text and gloss their real (non-literal) meaning.
For each idiom from the bundled lexicon that is present in text, returns one
GroundingItem whose content is "<surface>: <gloss>" (e.g.
"ἐφ' ἡμῖν: in our power, up to us"), source="lexicon:idiom", and
ref the idiom's surface form. These ground a translator in the meaning of a
non-compositional phrase, the one class of error per-token morphology cannot fix.
Detection is two-pronged and not a parser:
- surface (primary): accent-insensitive match of the idiom's surface form, with elision/apostrophe normalized away, so fixed idioms are caught verbatim (including elided and gapped-correlative spellings);
- contiguous lemma match (secondary): the idiom's content lemmas appearing as an
adjacent run among the text's content lemmas (via
greek.pipeline/greek.lemmatize), which catches inflected idioms (οἷός τε ἐστίforοἷός τε εἰμί) without firing on the same function words scattered across an unrelated sentence. Explicitly gapped...correlatives are not routed through this path (the surface path catches them). Inflection coverage depends on the active lemmatizer; the path simply yields fewer matches without a rich backend loaded.
analysis may supply already-computed TokenRecords. Their lemmas drive the
secondary path without consulting or rerunning a module-level Greek backend; hybrid
translation uses this to preserve explicit GreekPipeline isolation.
When idioms overlap, the longest match wins and its shorter sub-idioms are
suppressed, on both the surface path (by token span) and the lemma path (by
lemma-index span); identical glosses are de-duplicated. Best-effort and offline:
returns [] (never raises) on empty input or a missing backend. The lexicon is a
curated set of vetted non-compositional expressions, not an exhaustive idiom
dictionary; a gloss is a meaning aid, not a syntactic claim.
grounding_regime ¶
grounding_regime(text: str, *, corpus: object | None = None) -> RegimeSignal
Estimate whether lexical grounding helps text (EXPLORATORY, offline).
Combines three deterministic signals: rarity (corpus-relative via
greek.terminology_rarity when corpus is given, else a length/charset
heuristic), polysemy load (mean LSJ senses over the content words, when
greek.use_lsj() is active; else 0), and register markedness (share of
content words LSJ tags as dialectal/poetic/technical). High rarity favours
grounding (help); high polysemy without rarity favours hurt (the
dominant-gloss trap); little of either is neutral.
A best-effort heuristic: it never raises on missing backends, returning the signals it can compute. The label is exploratory — a guide for when to apply grounding, not a measured accuracy.
select_sense ¶
select_sense(word: str, context: str, *, max_candidates: int = 3, overlap_weight: float = 1.0, markedness_weight: float = 0.25, prior_weight: float = 0.15) -> list[SenseCandidate]
Rank word's LSJ senses by fit to context; best first (EXPLORATORY).
Returns up to max_candidates SenseCandidates, the contextually-best sense
first. Each sense scores by lexical overlap (overlap_weight) between the
Greek it cites and the context's content lemmas, a markedness bonus
(markedness_weight) when the sense carries the entry's dialect/register
markers, and a mild sense-order/length prior (prior_weight) favouring the
earlier, more central senses. Ties keep LSJ order, but the score is a sum, so with
no overlap signal the leading sense is not necessarily the dominant one: a later
sense carrying the entry's markers, or a short one favoured by the length prior,
can outrank the entry's first sense. Read dominant rather than the position.
Best-effort and offline: returns [] if the LSJ lexicon is not loaded
(greek.use_lsj()) or the word has no entry. A ranked sense is a hypothesis
from a lexical-overlap heuristic, not a word-sense disambiguation — label it
unverified at point of use.