bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
621 lines
33 KiB
Markdown
621 lines
33 KiB
Markdown
# 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](#13-known-limitations--follow-ups).
|
||
|
||
---
|
||
|
||
## 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](#9-the-knowledgepill-dto-boundary)) |
|
||
| 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, default `false`.
|
||
- `provider` — enum `["bedrock", "openai", "azureOpenAI", "google", "anthropic", "noop"]`, default `noop` (Java enum `LLMProvider`).
|
||
- `maxConcurrentRequests` — integer, default `5`, minimum `1`.
|
||
- Per-provider objects (each `additionalProperties:false`):
|
||
|
||
| Object | Java type | Auth (masked?) | Notable fields / defaults |
|
||
|--------|-----------|----------------|---------------------------|
|
||
| `bedrock` | `LLMBedrockConfig` | `awsConfig` → `awsBaseConfig.json` | `modelId` `anthropic.claude-3-5-sonnet-20240620-v1:0`, temp 0.0, maxTokens 4096, timeout 60s |
|
||
| `openai` | `LLMOpenAIConfig` | `apiKey` **`mask:true`** | `modelId` `gpt-4o-mini`, `endpoint`, `deploymentName`, `apiVersion` `2024-02-01` (Azure), temp 0.0, maxTokens 4096, timeout 60s |
|
||
| `google` | `LLMGoogleConfig` | `apiKey` **`mask:true`** | `modelId` `gemini-2.5-flash`, `endpoint`, temp 0.0, maxTokens 4096, timeout 60s |
|
||
| `anthropic` | `LLMAnthropicConfig` | `apiKey` **`mask:true`** | `modelId` `claude-3-5-sonnet-20240620`, `baseUrl` `https://api.anthropic.com`, temp 0.0, maxTokens 4096, timeout 60s |
|
||
|
||
Masked 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](#12-configuration-reference)).
|
||
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 a `Semaphore`
|
||
(`maxConcurrentRequests` permits; ctor rejects `< 1`), the `complete()` /
|
||
`completeStructured()` template methods, JSON-array parsing, and code-fence
|
||
stripping. Abstract surface: `doComplete(systemPrompt, userPrompt)` and
|
||
`getModelId()`. `DEFAULT_MAX_CONCURRENT_REQUESTS = 5`.
|
||
- **Concretes** — `OpenAICompletionClient` (OpenAI + Azure OpenAI),
|
||
`AnthropicCompletionClient`, `BedrockCompletionClient` (AWS SDK v2,
|
||
`AutoCloseable`), `GoogleCompletionClient`, and `NoopCompletionClient`
|
||
(returns `"[]"`, model id `"noop"`).
|
||
- **`LLMCompletionClientFactory.create(LLMConfiguration)`** — switch on
|
||
`provider` → concrete; null config/provider → Noop. Note: the factory keys off
|
||
`provider` only; it does **not** check `enabled` (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** when `enabled == true`. `get()` never returns
|
||
null; `isEnabled()` gates the pipeline; `setForTesting()` is the test seam.
|
||
`volatile` fields + `synchronized` mutators.
|
||
|
||
### 5.3 `completeStructured` contract
|
||
|
||
```java
|
||
<T> List<T> completeStructured(String systemPrompt, String userPrompt, Class<T> elementType)
|
||
```
|
||
|
||
- Acquires a permit, calls `doComplete`, releases in `finally`.
|
||
- Strips a leading/trailing ` ``` ` code fence (drops the language tag line).
|
||
- Parses with a default `new ObjectMapper()` (so `FAIL_ON_UNKNOWN_PROPERTIES`
|
||
is **on**) into `List<elementType>`. Parse failure → `LLMCompletionException`.
|
||
- **One retry** on `LLMCompletionException` (logged `warn`), then propagates. The
|
||
retry catches `LLMCompletionException` broadly, so transport errors that surface
|
||
as `LLMCompletionException` also 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 when `endpoint` **and**
|
||
`deploymentName` are set → `.../openai/deployments/<dep>/chat/completions?api-version=<v>`
|
||
with `api-key` header; else `Authorization: Bearer`. Reads `choices[0].message.content`.
|
||
- **Anthropic**: JDK `HttpClient`, `<baseUrl>/v1/messages`, headers `x-api-key` +
|
||
`anthropic-version: 2023-06-01`. Reads `content[0].text`.
|
||
- **Bedrock**: AWS SDK v2 `BedrockRuntimeClient` (credentials via
|
||
`AwsCredentialsUtil.buildCredentialsProvider`, region required). Body uses
|
||
`anthropic_version: bedrock-2023-05-31`. Reads `content[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**). Reads `candidates[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`** — `sourceType` enum gains `FileExtraction`
|
||
(`FILE_EXTRACTION`); new property `sourceFile` (`entityReference`, not required).
|
||
- **`createContextMemory.json`** — new create-time `sourceFile` (`entityReference`).
|
||
- **`contextFile.json`** — `ProcessingStatus` enum gains `ExtractingContext` (3rd,
|
||
between `Analyzing` and `Processed`); new derived property `memoryCount`
|
||
(integer, default 0).
|
||
|
||
### 6.2 Repositories
|
||
|
||
- **`ContextMemoryRepository`** — `FIELD_SOURCE_FILE = "sourceFile"` added to
|
||
`PATCH_FIELDS`/`UPDATE_FIELDS`. `storeRelationships` adds an
|
||
`addRelationship(sourceFile.id /*from*/, memory.id /*to*/, CONTEXT_FILE,
|
||
CONTEXT_MEMORY, Relationship.MENTIONED_IN)` edge. `setFields` resolves it with
|
||
`findFrom(memory.id, CONTEXT_MEMORY, MENTIONED_IN, CONTEXT_FILE)` (bulk path via
|
||
`findFromBatch`). Patch handled via `updateFromRelationship(...)`.
|
||
- **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.
|
||
- **`ContextFileRepository`** — `memoryCount` computed in `setFields` as
|
||
`findTo(file.id, CONTEXT_FILE, MENTIONED_IN, CONTEXT_MEMORY).size()`. New
|
||
`postDelete` cascades: `deleteExtractedMemories(file, hardDelete)` deletes each
|
||
linked pill, propagating the file delete's `hardDelete` flag (soft→soft,
|
||
hard→hard). The same method is reused by the reprocess path with `hardDelete=true`.
|
||
- **`ContextMemoryMapper`** — passthrough of `create.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 keeps `Processed`** (it
|
||
received the raw text status).
|
||
- `Unsupported` exists in the enum but the service never writes it — it is passed
|
||
through verbatim from `ContextFileTextExtractor`.
|
||
|
||
### 7.2 Service (`ContextFileProcessingService`, was `ContextFileExtractionService`)
|
||
|
||
- Only call site: `ContextFileResource` upload path →
|
||
`extractionService.submit(fileId, contentId)`.
|
||
- `submit` wraps `executor.execute(() -> process(...))` and catches
|
||
`RejectedExecutionException` (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).
|
||
- `canonicalText` feeds 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
|
||
|
||
- `applyFailure` sets both file and content to `Failed`; `processingError` is
|
||
written to the **content snapshot only**, not the file entity.
|
||
- **Text-stage** failures clear `extractedText` (+ `pageCount`). **LLM-stage**
|
||
failures **retain** `extractedText` (so the file stays indexed and is retriable
|
||
by re-upload).
|
||
- `extractText` catches `Throwable` but re-throws `VirtualMachineError`.
|
||
- No automatic retry anywhere; "retry later" means re-POST the file.
|
||
|
||
## 8. Knowledge-pill extraction (`ContextMemoryExtractor`)
|
||
|
||
- `extract(file, text)`: `chunkText` → per-chunk `completeStructured(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 though `canonicalText` may supply up to ~1M chars.
|
||
- **Dedupe:** `LinkedHashMap` keyed by `question.trim().toLowerCase(ROOT)`,
|
||
first-wins, insertion order preserved.
|
||
- **`isValid`:** `question` and `answer` both non-blank (title/summary/memoryType
|
||
optional).
|
||
- **`toMemory`** sets the full server-owned envelope: `id` (random UUID), `name`
|
||
(`<fileName>-<uuid>`), `fullyQualifiedName`, `title`, `question`, `answer`,
|
||
`summary`, `memoryType` (via `parseType`), `status=ACTIVE`,
|
||
`sourceType=FILE_EXTRACTION`, `sourceFile=<fileRef>`,
|
||
`shareConfig.visibility=SHARED`, `updatedBy=admin`, `updatedAt=now`.
|
||
- **`parseType`:** case-insensitive match against `ContextMemoryType`
|
||
(`Preference | UseCase | Note | Runbook | Faq`); default `NOTE`.
|
||
- **`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:
|
||
|
||
1. **Strict unknown fields.** `completeStructured` uses a default `ObjectMapper`
|
||
(`FAIL_ON_UNKNOWN_PROPERTIES` on) and `contextMemory.json` is
|
||
`additionalProperties:false` — one stray model key would fail the whole array
|
||
parse. `KnowledgePill` carries `@JsonIgnoreProperties(ignoreUnknown = true)`.
|
||
2. **Enum leniency.** `ContextMemory.memoryType` is the `ContextMemoryType` enum
|
||
(Jackson throws on a non-exact value); `KnowledgePill.memoryType` is a raw
|
||
String, so casing/variant forms degrade to `NOTE` in `parseType`.
|
||
3. **Server-owned identity.** `contextMemory.json` requires `id`/`name`, and the
|
||
whole envelope (FQN, status, sourceType, sourceFile, shareConfig,
|
||
updatedBy/At) is set server-side in `toMemory` — the model neither produces
|
||
nor should influence these.
|
||
4. **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 `sourceFile` object (id/type/name/displayName/fqn/deleted
|
||
as keywords) added identically to all four
|
||
`elasticsearch/{en,jp,ru,zh}/context_memory_search_index.json` files (the
|
||
per-language copies are byte-identical; no language-specific analyzers).
|
||
- `ContextMemoryBodyTextContributor` — appends `source 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 existing `GET /api/v1/system/status` validation. It fails
|
||
fast when object storage is disabled/missing or the factory holds a
|
||
`NoOpAssetService`; otherwise it runs a **live write→read→delete round-trip
|
||
probe** (tiny `text/plain` asset, 10s per-op timeout, asserts the bytes match,
|
||
cleans up in `finally`).
|
||
- **`AssetServiceFactory.init`** logs a `warn` when storage is disabled,
|
||
naming Context Center Drive and the exact failure string.
|
||
- Test (`SystemRepositoryObjectStorageValidationTest`) exercises the real
|
||
`AssetServiceFactory` + `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 to `entityType=contextMemory`,
|
||
`sourceType=FileExtraction`, **and `visibility=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 — the `fullyQualifiedName` from a search hit).
|
||
- **Lookup:** `Entity.getEntityByName(CONTEXT_MEMORY, fqn,
|
||
"sourceFile,owners,tags,domains", null)`.
|
||
- **Exposability gate:** returns the pill only if `sourceType=FILE_EXTRACTION`
|
||
**and** `shareConfig.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 a `ui-core-components` `Badge` (size
|
||
`sm`, no icon) from a `ProcessingStatus`, returning `null` when status is
|
||
absent. Mapping:
|
||
|
||
| Status | Color | Label key |
|
||
|--------|-------|-----------|
|
||
| `Uploaded` | gray | `label.uploaded` |
|
||
| `Analyzing` | blue | `label.analyzing` |
|
||
| `ExtractingContext` | indigo | `label.extracting-context` |
|
||
| `Processed` | success | `label.processed` |
|
||
| `Failed` | error | `label.failed` |
|
||
| `Unsupported` | warning | `label.unsupported` |
|
||
|
||
- **`DocumentsView`** renders the badge inline next to each document's filename,
|
||
driven by `file.processingStatus`.
|
||
- **i18n:** 4 new `en-us` keys (`label.analyzing`, `label.extracting-context`,
|
||
`label.unsupported`, `label.uploaded`); `label.processed`/`label.failed`
|
||
pre-existed. Synced across all locales.
|
||
|
||
## 14. Security & privacy
|
||
|
||
- LLM provider API keys are `mask:true` config 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 the `Shared` scope and a `VIEW_ALL` authorize 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) to `Processed`, `assumeTrue(memoryCount > 0)` to skip
|
||
gracefully when no LLM provider is configured; otherwise asserts
|
||
`memoryCount == pills.size()` and per-pill `sourceType=FILE_EXTRACTION`, non-null
|
||
`sourceFile` matching 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
|
||
|
||
```yaml
|
||
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
|
||
|
||
1. **Chunk tail dropped.** Documents over ~480K chars (8 × 60K) lose their tail to
|
||
the LLM, even though `canonicalText` supplies up to ~1M chars. Consider raising
|
||
`MAX_CHUNKS` or summarizing the remainder.
|
||
2. **`memoryCount` not in bulk path.** It is populated only in single-entity
|
||
`setFields`, not `setFieldsInBulk` — list endpoints requesting `memoryCount`
|
||
may return it unset.
|
||
3. **`processingError` is content-only.** The file entity flips to `Failed` with
|
||
no error string; clients must read the content snapshot for the reason.
|
||
4. **No checksum short-circuit.** Identical re-uploads pay the full text + LLM
|
||
cost; the captured checksum is stored but never compared.
|
||
5. **Wholesale reprocess.** Pills are hard-deleted and regenerated with new
|
||
UUIDs/FQNs each run — no continuity/versioning for an unchanged pill.
|
||
6. **Bedrock client never closed.** `BedrockCompletionClient` is `AutoCloseable`
|
||
but `LLMClientHolder` never calls `close()`; a re-`initialize` would leak the
|
||
prior SDK client. Low impact (single startup init).
|
||
7. **Factory ignores `enabled`.** Only the holder gates on it; calling the factory
|
||
directly with `enabled=false` + a real provider would still build a live client.
|
||
8. **MCP authorization is coarse.** A single global `VIEW_ALL` + ES
|
||
`visibility=Shared` filter, with no per-pill owner re-check.
|
||
9. **`Unsupported` status** is in the enum but never written by the service
|
||
(passthrough from the text extractor only).
|
||
10. **Per-language ES index files are byte-identical** — the jp/ru/zh
|
||
`sourceFile` mappings carry no CJK/Russian-specific analyzers.
|
||
|
||
## 18. File touch-list
|
||
|
||
**New — backend**
|
||
- `openmetadata-spec/.../json/schema/configuration/llmConfiguration.json`
|
||
- `openmetadata-service/.../service/llm/` — `LLMCompletionClient`,
|
||
`LLMCompletionClientFactory`, `LLMClientHolder`, `LLMCompletionException`,
|
||
`KnowledgePill`, `NoopCompletionClient`, `OpenAICompletionClient`,
|
||
`AnthropicCompletionClient`, `BedrockCompletionClient`, `GoogleCompletionClient`
|
||
- `openmetadata-service/.../service/drive/ContextMemoryExtractor.java`
|
||
- `openmetadata-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.yaml`
|
||
- `service/drive/ContextFileExtractionService.java` → **renamed**
|
||
`ContextFileProcessingService.java`
|
||
- `resources/drive/ContextFileResource.java`
|
||
- `jdbi3/ContextMemoryRepository.java`, `jdbi3/ContextFileRepository.java`,
|
||
`jdbi3/SystemRepository.java`, `attachments/AssetServiceFactory.java`
|
||
- `resources/context/ContextMemoryMapper.java`
|
||
- `search/indexes/ContextMemoryIndex.java`,
|
||
`search/vector/ContextMemoryBodyTextContributor.java`,
|
||
`elasticsearch/{en,jp,ru,zh}/context_memory_search_index.json`
|
||
- `openmetadata-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](#15-testing)).
|
||
|
||
**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 `enabled` kill-switch (gates extraction **and** the agent)
|
||
- `memoryExtraction.{fromFiles, fromPages}`
|
||
- `memoryAgent.{enabled, deriveGlossaryTerms, deriveMetrics, deletionPolicy}`
|
||
(`deletionPolicy` ∈ `cascade | orphan | deprecate`, default `cascade`)
|
||
- `prompts.{memoryExtraction, memoryAgent}.systemPrompt`
|
||
|
||
**Deltas to this doc:**
|
||
- The hardcoded `ContextMemoryExtractor.SYSTEM_PROMPT` ([§8](#8-knowledge-pill-extraction-contextmemoryextractor))
|
||
moves to `AISettings.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](#72-service-contextfileprocessingservice-was-contextfileextractionservice)
|
||
`submit`) and page extraction now gate **additionally** on AISettings
|
||
(`enabled && memoryExtraction.fromFiles` / `fromPages`), not only on
|
||
`LLMClientHolder.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_FROM`** relationship (appended last in `entityRelationship.json`;
|
||
`derivedEntity → sourceMemory`).
|
||
- `MemoryExtractor` (pure `derive`) + `MemoryReconciler` (owns writes) +
|
||
`MemoryProcessingEngine` (orchestrator) in the `drive` package — siblings to
|
||
this pill pipeline, same hash-gate / throttle / reconcile idioms — triggered
|
||
from new `ContextMemoryRepository.postCreate` / `postUpdate` hooks.
|
||
- Per memory, two **independent** axes (Term, Metric), each:
|
||
REUSE (`RELATED_TO` edge to an existing entity, not owned) /
|
||
CREATE (`provider=automation`, `DERIVED_FROM` edge, 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→user` on a human PATCH
|
||
(mirrors the pills Manual-guard); `deletionPolicy` governs memory-delete cleanup
|
||
of owned entities (`cascade` hard-deletes, `orphan` releases, `deprecate` flags).
|
||
Agent writes as a dedicated `memory-bot`.
|
||
- **Schema:** new `ContextMemory.memoryStats` (hash-gate + derived/reused
|
||
counts); new `provider` field on `Metric` ([§6.1](#61-schemas)) for uniform
|
||
ownership across Terms/Metrics/Glossaries.
|
||
- Renders the full **File → Memory → Term/Metric** provenance chain in the UI.
|