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)
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 facts grounded the output, and data (when set by a structured
capability) for the parsed JSON payload.
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 facts 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
¶
One piece of grounding evidence and its provenance.
content is what the model sees; 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). Stringifies to
content so it drops into the prompt like a plain evidence line.
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', grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Translate source text, grounded in optional lexicon/corpus evidence.
verify_translation ¶
verify_translation(text: str, draft: str, *, source: str = 'Ancient Greek', target: str = 'English', grounding: Grounding = (), client: LLMClient | None = None) -> ExploratoryResult
Check a draft translation against deterministic grounding and repair only definite contradictions, returning the corrected translation.
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, so callers
handle it exactly like a translate result.
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'].
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) -> 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). This is the validated source: LSJ orders senses etymologically, so its first sense is often the archaic one and asserting it injects errors, whereas a concise dictionary leads with the common sense. 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.
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_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).
Only the content reaches the prompt — provenance is for the trace, not
the model — so the wording stays stable across GroundingItem and plain
strings.
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) -> 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.
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, so with no overlap signal at
all the dominant sense leads, exactly as content_glosses assumes.
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.