33 KiB
Company Context — File-Extracted Knowledge Pills
- Date: 2026-06-12
- Status: Implemented (as-built spec for branch
pmbrull/companycontext-entity) - Author: pmbrull (with Claude)
- Scope: 72 files, ~+2818/−81. Backend (Java), schema (JSON), ingestion of file content into reusable memories, embedding/search, MCP tools, UI badge.
This document describes the feature as shipped, not the earlier pre-implementation design. Where the implementation diverged from that design, the as-built behavior is recorded here and flagged in §13 Known limitations.
1. Summary
The Context Center ("Drive") lets users upload documents. Previously an upload
was stored, its raw text extracted into ContextFile.extractedText, and that was
the end. This feature turns that raw text into structured, retrievable
knowledge pills — short question/answer facts about the company — that are
embedded, indexed, linked back to the originating file, and reachable by any
MCP-connected agent.
Pills are not a new entity. They are ContextMemory rows tagged
sourceType=FileExtraction with a sourceFile back-reference, riding the
entity's existing embedding + search plumbing.
End-to-end:
upload ──▶ store blob (object storage / AssetService)
──▶ extract text (status: Analyzing)
──▶ LLM extracts knowledge pills (status: ExtractingContext)
──▶ pills stored as ContextMemory rows, linked to the file
──▶ pills auto-embedded + indexed on create
──▶ pills retrievable via MCP + searchable by source filename
file carries a processing status surfaced in the UI (status: Processed)
2. Motivation
ContextMemory already existed — "Reusable context memory for Context Center
and AI-assisted retrieval", a question/answer pill that is already
vector-embedded (contextMemory is in AvailableEntityTypes, has
ContextMemoryBodyTextContributor + ContextMemoryIndex, and its index carries
parent aliases ["all", "dataAssetEmbeddings"]). It needed only a new source
type and a file back-reference to become the backing store for file-extracted
knowledge, avoiding a second overlapping "knowledge" concept and a full new
entity stack.
The real new infrastructure is a generic LLM completion layer — there was no
chat/completion client before (only EmbeddingClient and a NoOp NLQService).
It is built decoupled from Context Center so other features (e.g. MCP Chat) can
reuse it.
3. Architecture & data flow
POST /v1/contextCenter/drive/files (ContextFileResource:208)
└─ store asset (AssetServiceFactory.getService().upload), persist ContextFile + content
└─ extractionService.submit(fileId, contentId) (ContextFileResource:287)
ContextFileProcessingService.process(fileId, contentId) (drive/ContextFileProcessingService.java)
guard: contentId == file.headContentId (else abandon — stale re-upload)
1. markAnalyzing() → status Analyzing (file + content)
2. extractText() [DEFAULT_EXECUTOR, CPU] → read blob via AssetService, text-extract
├─ failure → status Failed (+ processingError on content), extractedText cleared
└─ success → text status (Processed / Unsupported)
3. if text Processed AND LLMClientHolder.isEnabled():
status ExtractingContext (file) [content snapshot stays Processed]
submitMemoryExtraction() [LLM_EXECUTOR, network]
runMemoryExtraction():
deleteExtractedMemories(file, hardDelete=true) ← wholesale replace
ContextMemoryExtractor.extract(file, canonicalText)
chunk → per-chunk LLM call → dedupe → create ContextMemory rows
{sourceType:FileExtraction, sourceFile:<ref>, status:Active,
visibility:Shared} └─ VectorEmbeddingHandler auto-embeds
status Processed (file)
├─ LLM failure → status Failed (+ processingError), extractedText RETAINED
└─ LLM queue full → status Failed ("…queue is full. Please retry later.")
else (LLM disabled): text Processed is terminal; LLM stage never enqueued
Two separate thread pools by design (ContextFileProcessingService):
| Pool | Thread-name prefix | Work | Sizing |
|---|---|---|---|
DEFAULT_EXECUTOR |
context-file-extraction- |
CPU-bound text extraction | threads = max(2, cores/2), ArrayBlockingQueue(max(64, threads*8)), AbortPolicy, daemon |
LLM_EXECUTOR |
context-memory-extraction- |
network-bound LLM calls | identical sizing |
Mixing seconds-long LLM calls into the text pool would starve text extraction; the pools are kept separate (the class javadoc spells this out). They are currently sized identically — the only difference is the thread-name prefix.
4. Key design decisions
| Decision | Choice |
|---|---|
| Entity | Reuse ContextMemory (new sourceType=FileExtraction + sourceFile); no new entity, no DDL migration |
| LLM layer | New generic LLMCompletionClient abstraction + per-provider concretes + factory + process-wide holder, modeled on EmbeddingClient |
| LLM→entity boundary | A narrow KnowledgePill DTO is the anti-corruption layer between untrusted model JSON and the entity (see §9) |
| File↔pill link | Relationship.MENTIONED_IN edge, ContextFile → ContextMemory; memoryCount surfaced on the file |
| Reprocess | Wholesale hard-delete + recreate of a file's pills on every extraction run (no versioning, no checksum skip) |
| Async | Reuse + rename the live ContextFileExtractionService → ContextFileProcessingService; LLM step on its own executor |
| Status | Extend the single ProcessingStatus enum: insert ExtractingContext between Analyzing and Processed |
| Embedding | Free — ContextMemory already auto-embeds on create; this PR only threads sourceFile through the index/body-text |
| MCP | Two purpose-built tools search_company_context + get_company_context |
| Storage guardrail | New /system/validate "Object Storage" step + startup warn — uploads are useless if object storage is NoOp/broken |
5. Generic LLM completion layer
Package org.openmetadata.service.llm (all new).
5.1 Config schema
openmetadata-spec/.../json/schema/configuration/llmConfiguration.json →
org.openmetadata.schema.configuration.LLMConfiguration.
-
enabled— boolean, defaultfalse. -
provider— enum["bedrock", "openai", "azureOpenAI", "google", "anthropic", "noop"], defaultnoop(Java enumLLMProvider). -
maxConcurrentRequests— integer, default5, minimum1. -
Per-provider objects (each
additionalProperties:false):Object Java type Auth (masked?) Notable fields / defaults bedrockLLMBedrockConfigawsConfig→awsBaseConfig.jsonmodelIdanthropic.claude-3-5-sonnet-20240620-v1:0, temp 0.0, maxTokens 4096, timeout 60sopenaiLLMOpenAIConfigapiKeymask:truemodelIdgpt-4o-mini,endpoint,deploymentName,apiVersion2024-02-01(Azure), temp 0.0, maxTokens 4096, timeout 60sgoogleLLMGoogleConfigapiKeymask:truemodelIdgemini-2.5-flash,endpoint, temp 0.0, maxTokens 4096, timeout 60santhropicLLMAnthropicConfigapiKeymask:truemodelIdclaude-3-5-sonnet-20240620,baseUrlhttps://api.anthropic.com, temp 0.0, maxTokens 4096, timeout 60sMasked API keys round-trip through the Secrets Manager. Bedrock delegates secrecy to
awsBaseConfig.json.
conf/openmetadata.yaml carries an llmConfiguration block, fully env-var
overridable (LLM_ENABLED, LLM_PROVIDER, LLM_MAX_CONCURRENT_REQUESTS, and
per-provider LLM_<PROVIDER>_* keys — see §12).
Its header comment states plainly that uploaded document content is sent to the
configured provider when enabled.
5.2 Client hierarchy
LLMCompletionClient(abstract,@Slf4j): owns aSemaphore(maxConcurrentRequestspermits; ctor rejects< 1), thecomplete()/completeStructured()template methods, JSON-array parsing, and code-fence stripping. Abstract surface:doComplete(systemPrompt, userPrompt)andgetModelId().DEFAULT_MAX_CONCURRENT_REQUESTS = 5.- Concretes —
OpenAICompletionClient(OpenAI + Azure OpenAI),AnthropicCompletionClient,BedrockCompletionClient(AWS SDK v2,AutoCloseable),GoogleCompletionClient, andNoopCompletionClient(returns"[]", model id"noop"). LLMCompletionClientFactory.create(LLMConfiguration)— switch onprovider→ concrete; null config/provider → Noop. Note: the factory keys offprovideronly; it does not checkenabled(the holder does).LLMClientHolder— process-wide holder of the single shared client.initialize(config):enabled = config != null && Boolean.TRUE.equals(getEnabled());instance = enabled ? factory.create(config) : new NoopCompletionClient(). So a real client is built only whenenabled == true.get()never returns null;isEnabled()gates the pipeline;setForTesting()is the test seam.volatilefields +synchronizedmutators.
5.3 completeStructured contract
<T> List<T> completeStructured(String systemPrompt, String userPrompt, Class<T> elementType)
- Acquires a permit, calls
doComplete, releases infinally. - Strips a leading/trailing
```code fence (drops the language tag line). - Parses with a default
new ObjectMapper()(soFAIL_ON_UNKNOWN_PROPERTIESis on) intoList<elementType>. Parse failure →LLMCompletionException. - One retry on
LLMCompletionException(loggedwarn), then propagates. The retry catchesLLMCompletionExceptionbroadly, so transport errors that surface asLLMCompletionExceptionalso get one retry.
5.4 Providers
All providers use plain chat/messages JSON mode (no tool/function calling);
non-200 → LLMCompletionException("<provider> API returned status …").
- OpenAI / Azure: JDK
HttpClient. Azure detected whenendpointanddeploymentNameare set →.../openai/deployments/<dep>/chat/completions?api-version=<v>withapi-keyheader; elseAuthorization: Bearer. Readschoices[0].message.content. - Anthropic: JDK
HttpClient,<baseUrl>/v1/messages, headersx-api-key+anthropic-version: 2023-06-01. Readscontent[0].text. - Bedrock: AWS SDK v2
BedrockRuntimeClient(credentials viaAwsCredentialsUtil.buildCredentialsProvider, region required). Body usesanthropic_version: bedrock-2023-05-31. Readscontent[0].text.close()exists but the holder never calls it. - Google Gemini: JDK
HttpClient,.../models/<modelId>:generateContent?key=<apiKey>(API key in the URL query string). Readscandidates[0].content.parts[0].text.
5.5 Startup wiring
OpenMetadataApplicationConfig gains
@JsonProperty("llmConfiguration") private LLMConfiguration llmConfiguration.
OpenMetadataApplication startup calls
LLMClientHolder.initialize(catalogConfig.getLlmConfiguration()) (new), then the
pre-existing LlmConfigHolder.initialize(...) (publishes raw config for MCP
Chat). Two holders, same config source.
6. Schema & persistence changes
No DDL / Flyway migration — ContextMemory and ContextFile use the generic
JSON-column entity tables, and the file↔pill link lives in the generic
entity_relationship table.
6.1 Schemas
contextMemory.json—sourceTypeenum gainsFileExtraction(FILE_EXTRACTION); new propertysourceFile(entityReference, not required).createContextMemory.json— new create-timesourceFile(entityReference).contextFile.json—ProcessingStatusenum gainsExtractingContext(3rd, betweenAnalyzingandProcessed); new derived propertymemoryCount(integer, default 0).
6.2 Repositories
ContextMemoryRepository—FIELD_SOURCE_FILE = "sourceFile"added toPATCH_FIELDS/UPDATE_FIELDS.storeRelationshipsadds anaddRelationship(sourceFile.id /*from*/, memory.id /*to*/, CONTEXT_FILE, CONTEXT_MEMORY, Relationship.MENTIONED_IN)edge.setFieldsresolves it withfindFrom(memory.id, CONTEXT_MEMORY, MENTIONED_IN, CONTEXT_FILE)(bulk path viafindFromBatch). Patch handled viaupdateFromRelationship(...).- Collision-free by construction: each relationship hierarchy uses a
distinct
Relationship—rootMemory→CONTAINS,parentMemory→PARENT_OF,domains→HAS,sourceFile→MENTIONED_IN— so the four resolvers query disjoint(relation, fromType)tuples.
- Collision-free by construction: each relationship hierarchy uses a
distinct
ContextFileRepository—memoryCountcomputed insetFieldsasfindTo(file.id, CONTEXT_FILE, MENTIONED_IN, CONTEXT_MEMORY).size(). NewpostDeletecascades:deleteExtractedMemories(file, hardDelete)deletes each linked pill, propagating the file delete'shardDeleteflag (soft→soft, hard→hard). The same method is reused by the reprocess path withhardDelete=true.ContextMemoryMapper— passthrough ofcreate.getSourceFile().
7. Processing pipeline & status
7.1 Status state machine (ProcessingStatus)
Uploaded ─▶ Analyzing ─▶ (text result) ─▶ Processed ─▶ ExtractingContext ─▶ Processed
│ ▲ (LLM enabled) (final)
├─▶ Failed └─ if LLM disabled: terminal here
└─▶ Unsupported (passthrough from text extractor)
any stage failure ─▶ Failed
- The file and its content snapshot carry status independently. When the file
advances to
ExtractingContext, the content snapshot keepsProcessed(it received the raw text status). Unsupportedexists in the enum but the service never writes it — it is passed through verbatim fromContextFileTextExtractor.
7.2 Service (ContextFileProcessingService, was ContextFileExtractionService)
- Only call site:
ContextFileResourceupload path →extractionService.submit(fileId, contentId). submitwrapsexecutor.execute(() -> process(...))and catchesRejectedExecutionException(queue full →Failed, "Processing queue is full").- Every stage re-reads from the repo and re-checks
headContentId; a newer upload silently abandons the stale content's writes (concurrent re-upload guard). canonicalTextfeeds the LLM the content snapshot's extracted text (capped ~1M chars) in preference to the file's indexed text (capped ~200K).
7.3 Error handling
applyFailuresets both file and content toFailed;processingErroris written to the content snapshot only, not the file entity.- Text-stage failures clear
extractedText(+pageCount). LLM-stage failures retainextractedText(so the file stays indexed and is retriable by re-upload). extractTextcatchesThrowablebut re-throwsVirtualMachineError.- No automatic retry anywhere; "retry later" means re-POST the file.
8. Knowledge-pill extraction (ContextMemoryExtractor)
extract(file, text):chunkText→ per-chunkcompleteStructured(SYSTEM_PROMPT, chunk, KnowledgePill.class)→dedupe→memoryRepository.create(toMemory(pill, fileRef)).- Chunking:
MAX_PROMPT_CHARS = 60_000,MAX_CHUNKS = 8. Chunk ends snap back to the last\n\n/\n/boundary in the second half of the budget, else hard-cut at the char cap. Text past 8 chunks (~480K chars) is dropped with a warning — even thoughcanonicalTextmay supply up to ~1M chars. - Dedupe:
LinkedHashMapkeyed byquestion.trim().toLowerCase(ROOT), first-wins, insertion order preserved. isValid:questionandanswerboth non-blank (title/summary/memoryType optional).toMemorysets the full server-owned envelope:id(random UUID),name(<fileName>-<uuid>),fullyQualifiedName,title,question,answer,summary,memoryType(viaparseType),status=ACTIVE,sourceType=FILE_EXTRACTION,sourceFile=<fileRef>,shareConfig.visibility=SHARED,updatedBy=admin,updatedAt=now.parseType: case-insensitive match againstContextMemoryType(Preference | UseCase | Note | Runbook | Faq); defaultNOTE.SYSTEM_PROMPT: "You extract reusable company knowledge from a document as a JSON array. Each element is an object with keys: title, question, answer, summary, memoryType (one of Faq, Note, Runbook, UseCase, Preference). Capture durable facts, definitions, policies, and how-to guidance. Return ONLY the JSON array, no prose."
Reprocess / idempotency
Every LLM run first calls deleteExtractedMemories(file, hardDelete=true),
hard-deleting all of the file's existing pills, then recreates from scratch
with fresh random IDs/FQNs. There is no supersede/versioning and no
checksum short-circuit — re-uploading identical bytes re-runs text extraction
and the LLM and regenerates pills. (Rationale in code: machine-generated pills
are replaced wholesale, so soft-deleted rows would only accumulate with no
restore path.)
9. The KnowledgePill DTO boundary
KnowledgePill (service/llm/KnowledgePill.java) is a 5-field record
(title, question, answer, summary, memoryType) used only as the LLM
deserialization target — deliberately not the ContextMemory entity. It is the
anti-corruption boundary between untrusted model JSON and the trusted entity
model. Parsing model output straight into ContextMemory is avoided because:
- Strict unknown fields.
completeStructureduses a defaultObjectMapper(FAIL_ON_UNKNOWN_PROPERTIESon) andcontextMemory.jsonisadditionalProperties:false— one stray model key would fail the whole array parse.KnowledgePillcarries@JsonIgnoreProperties(ignoreUnknown = true). - Enum leniency.
ContextMemory.memoryTypeis theContextMemoryTypeenum (Jackson throws on a non-exact value);KnowledgePill.memoryTypeis a raw String, so casing/variant forms degrade toNOTEinparseType. - Server-owned identity.
contextMemory.jsonrequiresid/name, and the whole envelope (FQN, status, sourceType, sourceFile, shareConfig, updatedBy/At) is set server-side intoMemory— the model neither produces nor should influence these. - Minimal prompt contract. The DTO is exactly the five fields the prompt asks for; targeting the entity would expose its nested schema to the model.
toMemory + parseType is the single mapping layer; validation and dedupe
operate on the DTO before any entity is built.
10. Embedding & search
contextMemory was already vector-indexable and auto-embedded on create
(AvailableEntityTypes, VectorEmbeddingHandler, parent aliases
["all", "dataAssetEmbeddings"]) — unchanged here. This PR threads the new
sourceFile reference through three layers so file-derived pills are searchable
by their origin filename:
ContextMemoryIndex—doc.put("sourceFile", getEntityWithDisplayName(...)).- ES/OS mapping — a new
sourceFileobject (id/type/name/displayName/fqn/deleted as keywords) added identically to all fourelasticsearch/{en,jp,ru,zh}/context_memory_search_index.jsonfiles (the per-language copies are byte-identical; no language-specific analyzers). ContextMemoryBodyTextContributor— appendssource file: <name>to the embedding body text.
11. Object-storage validation (guardrail)
Uploaded file content lives in object storage (S3 / Azure / in-memory) via
AssetService. If that backend is disabled or NoOp, uploads silently "succeed"
but their bytes are discarded and extraction fails with "Unable to read file
content from object storage" — producing zero pills. To surface this before
users hit it:
SystemRepository.getObjectStorageValidation(config)adds an "Object Storage" step to the existingGET /api/v1/system/statusvalidation. It fails fast when object storage is disabled/missing or the factory holds aNoOpAssetService; otherwise it runs a live write→read→delete round-trip probe (tinytext/plainasset, 10s per-op timeout, asserts the bytes match, cleans up infinally).AssetServiceFactory.initlogs awarnwhen storage is disabled, naming Context Center Drive and the exact failure string.- Test (
SystemRepositoryObjectStorageValidationTest) exercises the realAssetServiceFactory+InMemoryAssetService: disabled, missing config, NoOp-after-config-drift, and a passing live in-memory round-trip.
This reuses the existing /system/status framework (no new endpoint) but exists
solely for the Drive → object-storage → extraction pipeline.
12. MCP tools
Two tools in openmetadata-mcp/.../tools/, registered in tools.json and
dispatched by new DefaultToolContext switch cases (via the 3-arg, no-limits
execute — neither tool records usage or enforces limits).
search_company_context
- Input:
query(string, required);size(integer, default 10, clamped 1..50). - Query:
OpenSearchVectorService.search(query, filters, size, from=0, k=100, threshold=0.0)with filters pinned toentityType=contextMemory,sourceType=FileExtraction, andvisibility=Shared. - Output:
{query, results:[{fullyQualifiedName, name, title, question, answer, summary, sourceFile, similarityScore}], returnedCount}. - Auth: global
authorize(CONTEXT_MEMORY, VIEW_ALL). Requires vector embedding enabled, else returns an error payload.
get_company_context
- Input:
fqn(string, required — thefullyQualifiedNamefrom a search hit). - Lookup:
Entity.getEntityByName(CONTEXT_MEMORY, fqn, "sourceFile,owners,tags,domains", null). - Exposability gate: returns the pill only if
sourceType=FILE_EXTRACTIONandshareConfig.visibility=SHARED, else an error. - Output:
{fullyQualifiedName, name, title, question, answer, summary, memoryType, sourceFile (as FQN)}. - Auth: same global
VIEW_ALL.
Both gate on a single global VIEW_ALL plus the Shared-visibility scope; there
is no per-pill owner re-check beyond that.
13. UI
-
DocumentStatusBadge(new) renders aui-core-componentsBadge(sizesm, no icon) from aProcessingStatus, returningnullwhen status is absent. Mapping:Status Color Label key Uploadedgray label.uploadedAnalyzingblue label.analyzingExtractingContextindigo label.extracting-contextProcessedsuccess label.processedFailederror label.failedUnsupportedwarning label.unsupported -
DocumentsViewrenders the badge inline next to each document's filename, driven byfile.processingStatus. -
i18n: 4 new
en-uskeys (label.analyzing,label.extracting-context,label.unsupported,label.uploaded);label.processed/label.failedpre-existed. Synced across all locales.
14. Security & privacy
- LLM provider API keys are
mask:trueconfig through the Secrets Manager — never logged, never returned in API responses. - File content is sent to the configured LLM provider during extraction. This
is an explicit, admin-enabled action (
llmConfiguration.enabled), and the provider is the admin's configured choice — documented in the yaml header. - Generated pills default to
visibility=Shared(org-readable). MCP retrieval enforces theSharedscope and aVIEW_ALLauthorize on every call. - No new unauthenticated surface; MCP tools ride existing OAuth/JWT auth.
15. Testing
Java unit (openmetadata-service)
drive/ContextFileProcessingServiceTest— status machine: analyzing→processed, LLM-enabled extraction, LLM-rejection/failure keeps text, storage-unavailable & null-stream failures, executor rejection, head-content skip, VME rethrow.drive/ContextMemoryExtractorTest— one-memory-per-pill, dedupe + skip-invalid, skip-when-no-text, multi-chunk dedupe, chunk cap.jdbi3/SystemRepositoryObjectStorageValidationTest— the four storage-validation scenarios.llm/LLMClientHolderTest,llm/LLMCompletionClientFactoryTest,llm/LLMCompletionClientTest— holder stability + disabled-builds-no-client, factory dispatch, structured parsing / code-fence / concurrency guard.
MCP (openmetadata-mcp)
tools/GetCompanyContextToolTest— missing/blank fqn, auth-denied, shared-pill projection, non-file & private rejection.tools/SearchCompanyContextToolTest— missing/blank query, auth-denied (input guards; does not exercise the vector path).
Integration (openmetadata-integration-tests)
it/drive/CompanyContextPipelineIT— end-to-end: upload.txt, poll (Awaitility, ≤45s) toProcessed,assumeTrue(memoryCount > 0)to skip gracefully when no LLM provider is configured; otherwise assertsmemoryCount == pills.size()and per-pillsourceType=FILE_EXTRACTION, non-nullsourceFilematching the file id, non-blank question/answer.
UI — DocumentStatusBadge.test.tsx (renders nothing without status; it.each
over the six status→label→color rows). No new Playwright spec.
16. Configuration reference
llmConfiguration:
enabled: ${LLM_ENABLED:-false}
provider: ${LLM_PROVIDER:-noop} # noop | openai | azureOpenAI | bedrock | google | anthropic
maxConcurrentRequests: ${LLM_MAX_CONCURRENT_REQUESTS:-5}
openai:
apiKey: ${LLM_OPENAI_API_KEY:-""}
modelId: ${LLM_OPENAI_MODEL_ID:-"gpt-4o-mini"}
endpoint: ${LLM_OPENAI_ENDPOINT:-""}
deploymentName:${LLM_OPENAI_DEPLOYMENT:-""}
apiVersion: ${LLM_OPENAI_API_VERSION:-"2024-02-01"}
maxTokens: ${LLM_OPENAI_MAX_TOKENS:-4096}
bedrock:
awsConfig:
awsRegion: ${AWS_DEFAULT_REGION:-""}
# IAM or static keys via AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN
modelId: ${LLM_BEDROCK_MODEL_ID:-"eu.anthropic.claude-haiku-4-5-20251001-v1:0"}
maxTokens: ${LLM_BEDROCK_MAX_TOKENS:-4096}
google:
apiKey: ${LLM_GOOGLE_API_KEY:-""}
modelId: ${LLM_GOOGLE_MODEL_ID:-"gemini-2.5-flash"}
maxTokens: ${LLM_GOOGLE_MAX_TOKENS:-4096}
anthropic:
apiKey: ${LLM_ANTHROPIC_API_KEY:-""}
modelId: ${LLM_ANTHROPIC_MODEL_ID:-"claude-3-5-sonnet-20240620"}
baseUrl: ${LLM_ANTHROPIC_BASE_URL:-"https://api.anthropic.com"}
maxTokens: ${LLM_ANTHROPIC_MAX_TOKENS:-4096}
17. Known limitations & follow-ups
- Chunk tail dropped. Documents over ~480K chars (8 × 60K) lose their tail to
the LLM, even though
canonicalTextsupplies up to ~1M chars. Consider raisingMAX_CHUNKSor summarizing the remainder. memoryCountnot in bulk path. It is populated only in single-entitysetFields, notsetFieldsInBulk— list endpoints requestingmemoryCountmay return it unset.processingErroris content-only. The file entity flips toFailedwith no error string; clients must read the content snapshot for the reason.- No checksum short-circuit. Identical re-uploads pay the full text + LLM cost; the captured checksum is stored but never compared.
- Wholesale reprocess. Pills are hard-deleted and regenerated with new UUIDs/FQNs each run — no continuity/versioning for an unchanged pill.
- Bedrock client never closed.
BedrockCompletionClientisAutoCloseablebutLLMClientHoldernever callsclose(); a re-initializewould leak the prior SDK client. Low impact (single startup init). - Factory ignores
enabled. Only the holder gates on it; calling the factory directly withenabled=false+ a real provider would still build a live client. - MCP authorization is coarse. A single global
VIEW_ALL+ ESvisibility=Sharedfilter, with no per-pill owner re-check. Unsupportedstatus is in the enum but never written by the service (passthrough from the text extractor only).- Per-language ES index files are byte-identical — the jp/ru/zh
sourceFilemappings carry no CJK/Russian-specific analyzers.
18. File touch-list
New — backend
openmetadata-spec/.../json/schema/configuration/llmConfiguration.jsonopenmetadata-service/.../service/llm/—LLMCompletionClient,LLMCompletionClientFactory,LLMClientHolder,LLMCompletionException,KnowledgePill,NoopCompletionClient,OpenAICompletionClient,AnthropicCompletionClient,BedrockCompletionClient,GoogleCompletionClientopenmetadata-service/.../service/drive/ContextMemoryExtractor.javaopenmetadata-mcp/.../tools/SearchCompanyContextTool.java,GetCompanyContextTool.java
New — frontend
.../components/.../ContextCenter/DocumentStatusBadge/(component, interface, test)
Modified — schema
entity/context/contextMemory.json,api/context/createContextMemory.json,entity/data/contextFile.json(+ generated TS mirrors)
Modified — backend
OpenMetadataApplication.java,OpenMetadataApplicationConfig.java,conf/openmetadata.yamlservice/drive/ContextFileExtractionService.java→ renamedContextFileProcessingService.javaresources/drive/ContextFileResource.javajdbi3/ContextMemoryRepository.java,jdbi3/ContextFileRepository.java,jdbi3/SystemRepository.java,attachments/AssetServiceFactory.javaresources/context/ContextMemoryMapper.javasearch/indexes/ContextMemoryIndex.java,search/vector/ContextMemoryBodyTextContributor.java,elasticsearch/{en,jp,ru,zh}/context_memory_search_index.jsonopenmetadata-mcp/.../tools/DefaultToolContext.java,openmetadata-mcp/.../resources/json/data/mcp/tools.json
Modified — frontend
.../DocumentsView/DocumentsView.component.tsx, locale files (17), generated TS
Tests — across openmetadata-service, openmetadata-mcp,
openmetadata-integration-tests, and UI (see §15).
Codegen after schema edits: make generate, mvn spotless:apply, UI
checkstyle for generated TS, npx tsc --noEmit.
19. Follow-on: AISettings + Memory Agent (designed)
Status: Designed, not yet built. Full design:
docs/superpowers/specs/2026-06-18-ai-settings-memory-agent-design.md. Built on
branch pmbrull/ottawa. Two capabilities layered on top of the pill pipeline above.
19.1 AISettings (config plane)
A SearchSettings-style settings entity (configuration/aiSettings.json,
SettingsType.AI_SETTINGS, seeded + merged via SettingsCache / a new
AISettingsHandler, read/written through the generic SystemResource
GET/PUT/reset, UI page under Preferences → AI). Shape:
- master
enabledkill-switch (gates extraction and the agent) memoryExtraction.{fromFiles, fromPages}memoryAgent.{enabled, deriveGlossaryTerms, deriveMetrics, deletionPolicy}(deletionPolicy∈cascade | orphan | deprecate, defaultcascade)prompts.{memoryExtraction, memoryAgent}.systemPrompt
Deltas to this doc:
- The hardcoded
ContextMemoryExtractor.SYSTEM_PROMPT(§8) moves toAISettings.prompts.memoryExtraction.systemPrompt— seed JSON carries the default, the code constant is kept only as the cache-miss fallback, and the extractor reads it at run time. This addresses the un-tunable-prompt gap. - File extraction (§7.2
submit) and page extraction now gate additionally on AISettings (enabled && memoryExtraction.fromFiles/fromPages), not only onLLMClientHolder.isEnabled().
19.2 Memory Agent (memory → ontology)
On ContextMemory create/update, an async, throttled agent reconciles the memory
against the existing instance ontology and derives Glossary Terms / Metrics —
reusing what already covers the memory, creating only what is missing.
- New
DERIVED_FROMrelationship (appended last inentityRelationship.json;derivedEntity → sourceMemory). MemoryExtractor(purederive) +MemoryReconciler(owns writes) +MemoryProcessingEngine(orchestrator) in thedrivepackage — siblings to this pill pipeline, same hash-gate / throttle / reconcile idioms — triggered from newContextMemoryRepository.postCreate/postUpdatehooks.- Per memory, two independent axes (Term, Metric), each:
REUSE (
RELATED_TOedge to an existing entity, not owned) / CREATE (provider=automation,DERIVED_FROMedge, agent-owned; mints a glossary when none fits) / SKIP. Grounding is top-K search over the existing term/metric indexes (no full-ontology load). - Ownership lifecycle: adopt-on-touch flips
automation→useron a human PATCH (mirrors the pills Manual-guard);deletionPolicygoverns memory-delete cleanup of owned entities (cascadehard-deletes,orphanreleases,deprecateflags). Agent writes as a dedicatedmemory-bot. - Schema: new
ContextMemory.memoryStats(hash-gate + derived/reused counts); newproviderfield onMetric(§6.1) for uniform ownership across Terms/Metrics/Glossaries. - Renders the full File → Memory → Term/Metric provenance chain in the UI.