Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.d/2.20.1-orchestrator-temperature-negotiation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 2.20.1 — Provider temperature capability negotiation

- Compose now pins ContextualWisdomLab/contextual-orchestrator#791 so providers
that accept only their default sampling temperature can retry the same
request without the unsupported optional field, alongside the orchestrator's
embedding-capability selection contract.
- The pin remains an immutable upstream commit and does not copy provider
behavior or credentials into LineageWeave.
5 changes: 5 additions & 0 deletions CHANGELOG.d/2.20.2-orchestrator-owned-embedding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 2.20.2

- Delegate embedding-model selection to contextual-orchestrator, retain the
resolved model as database provenance, and keep provider model selectors out
of the LineageWeave backend process.
4 changes: 3 additions & 1 deletion backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ def load_settings() -> Settings:
],
orchestrator_base_url=os.environ.get("ORCHESTRATOR_BASE_URL", ""),
orchestrator_api_key=os.environ.get("ORCHESTRATOR_API_KEY", ""),
embedding_model=os.environ.get("LLM_GATEWAY_EMBEDDING_MODEL", "").strip(),
# contextual-orchestrator owns embedding model discovery. Keep the
# empty compatibility value for older callers; never read a selector.
embedding_model="",
valkey_url=os.environ.get("VALKEY_URL", "redis://localhost:16379/0"),
searxng_base_url=os.environ.get("SEARXNG_BASE_URL", ""),
tepp_transport_url=os.environ.get("TEPP_TRANSPORT_URL", ""),
Expand Down
34 changes: 31 additions & 3 deletions backend/app/post_content_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async def post_content_is_complete(
conn: asyncpg.Connection,
post_id: str,
*,
embedding_model_code: str,
embedding_model_code: str | None,
require_structure: bool = False,
) -> bool:
"""Require configured semantic, structure, and region evidence before ready."""
Expand All @@ -61,8 +61,35 @@ async def post_content_is_complete(
where unit.post_id = $1
)
and (
$2 = ''
or (
(
nullif($2::text, '') is null
and (
not exists(
select 1
from post_content_unit unit
left join post_content_embedding embedding
on embedding.post_content_unit_id = unit.post_content_unit_id
where unit.post_id = $1
and embedding.post_content_embedding_id is null
)
and not exists(
select 1
from post_content_unit unit
join post_content_image image
on image.post_content_unit_id = unit.post_content_unit_id
join post_content_image_region region
on region.post_content_image_id = image.post_content_image_id
left join post_content_image_region_embedding embedding
on embedding.post_content_image_region_id = region.post_content_image_region_id
where unit.post_id = $1
and region.description_status_code = 'described'
and embedding.post_content_image_region_embedding_id is null
)
)
)
Comment on lines +64 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Automatic completeness requires an embedding for every unit, including text-less image units

In automatic mode (embedding_model_code empty/None, the only runtime path now), post_content_is_complete requires that every post_content_unit has a post_content_embedding (the not exists(... embedding.post_content_embedding_id is null) clause at post_content_queue.py). Image units whose rendered unit_text is empty are never added to embeddable in post_content_persistence.py, so they get no unit embedding, which would keep such a post permanently incomplete and drive the worker to FAILED after retries. This behavior is not newly introduced (the prior exact-model branch imposed the same per-unit requirement while the runtime supplied a configured model), so it is not a regression from this PR — but it is worth confirming that all embeddable units actually receive embeddings under the pinned orchestrator, otherwise ingestion fails closed indefinitely.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

or (
nullif($2::text, '') is not null
and (
not exists(
select 1
from post_content_unit unit
Expand All @@ -88,6 +115,7 @@ async def post_content_is_complete(
)
)
)
)
Comment on lines 115 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Completeness semantics for empty/absent embedding model changed from 'skip' to 'require'

Previously post_content_is_complete used $2 = '' or (...), so an empty/unset embedding model made the embedding requirement TRUE (embeddings not required for completeness). The new code at post_content_queue.py routes both '' and NULL (via nullif($2::text,'')) into the auto branch, which REQUIRES an embedding for every unit and every described image region regardless of model code. This is the intended ADR 0118 behavior, but it has an operational implication: a deployment that previously ran without an embedding agent (no LLM_GATEWAY_EMBEDDING_MODEL) had post-content jobs succeed with no embeddings; after this change those posts are considered incomplete. When a wake-up event re-claims such a SUCCEEDED job (post_content_worker.py:106-114), it will reprocess and, if the orchestrator has no eligible embedding agent, drive the job to FAILED after the retry limit. This fail-closed outcome is consistent with ADR 0118, but worth confirming that all target deployments do configure an orchestrator embedding agent.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 115 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Completeness now requires embeddings regardless of model

The old predicate treated a blank model as complete ($2 = '' or ...). The new null branch instead requires an embedding for every unit and every described region. This matches the fail-closed persistence path and the candidate query in queue_post_content_backfill.py, which also dropped its model filter. Existing rows embedded under an old model code still satisfy the null branch since any embedding counts. The non-null branch stays equivalent to the original.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

and (
not $3::boolean
or not exists(
Expand Down
3 changes: 0 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,6 @@ services:
# LLM_GATEWAY_API_KEY in the orchestrator's private env file.
ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-http://orchestrator:8000}
ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-${CONTEXTUAL_ORCHESTRATOR_TOKEN:-lineageweave-orchestrator-dev-only}}
# This is a non-secret embedding contract. Provider credentials remain
# private to contextual-orchestrator and are never injected into backend.
LLM_GATEWAY_EMBEDDING_MODEL: ${LLM_GATEWAY_EMBEDDING_MODEL:-text-embedding-3-large}
SEARXNG_BASE_URL: http://searxng:8080
TEPP_TRANSPORT_URL: ${TEPP_TRANSPORT_URL:-}
TEPP_API_KEY: ${TEPP_API_KEY:-}
Expand Down
5 changes: 3 additions & 2 deletions docker/contextual-orchestrator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ FROM python:3.12-slim@sha256:423ed6ab25b1921a477529254bfeeabf5855151dc2c3141699a
WORKDIR /app

# Reuse the upstream implementation without copying it into LineageWeave.
# Pin the runtime to PR #765's pushed commit until that protected PR merges;
# Pin the runtime to ContextualWisdomLab/contextual-orchestrator#791's pushed
# integration commit until its protected dependency stack merges;
# this keeps model selection, structured synthesis, and reasoning policy in
# contextual-orchestrator itself.
ADD https://github.com/ContextualWisdomLab/contextual-orchestrator/archive/7df051a.tar.gz /tmp/contextual-orchestrator.tar.gz
ADD https://github.com/ContextualWisdomLab/contextual-orchestrator/archive/0246e91f107c8dbb82eb0b421d0aeb0f5fe21ecf.tar.gz /tmp/contextual-orchestrator.tar.gz
RUN mkdir /tmp/contextual-orchestrator \
&& tar -xzf /tmp/contextual-orchestrator.tar.gz --strip-components=1 -C /tmp/contextual-orchestrator \
&& cp -R /tmp/contextual-orchestrator/contextual_orchestrator /app/contextual_orchestrator \
Expand Down
8 changes: 4 additions & 4 deletions docs/adr/0030-external-llm-gateway-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ These credentials are not interchangeable. The first is the local service
boundary; the second is the provider credential.

The backend must not load `~/.env` wholesale. Compose injects the provider
credential and URL only into contextual-orchestrator; the backend receives
only its internal orchestrator credential and the non-secret embedding model
identifier. This prevents an unrelated application process from holding the
provider secret while preserving the single LLM/Vision boundary.
credential, URL, and embedding-agent allowlist only into
contextual-orchestrator; the backend receives only its internal orchestrator
credential. This prevents an unrelated application process from holding a
provider secret or selector while preserving the single LLM/Vision boundary.

## Decision

Expand Down
23 changes: 18 additions & 5 deletions docs/adr/0083-orchestrator-runtime-commit-pin.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ The orchestrator's `auto` reasoning mode is an internal routing decision and
must not be forwarded as an upstream provider `reasoning_effort` value. The
runtime also must discover provider models from the configured gateway rather
than requiring `LLM_GATEWAY_MODEL`, and structured requests must remain
multi-agent.
multi-agent. Some provider deployments accept only their default sampling
temperature; retrying the same rejected value cannot recover that capability
mismatch.

## Decision

`docker/contextual-orchestrator/Dockerfile` pins the downloaded archive to
commit `6db772e`, the pushed head of contextual-orchestrator PR #765. The pin
remains explicit and immutable until the protected PR merges; it is not a
moving `main` reference and it is not a LineageWeave monkey patch.
commit `0246e91f107c8dbb82eb0b421d0aeb0f5fe21ecf`, the pushed integration
head of ContextualWisdomLab/contextual-orchestrator#791. That PR combines
the temperature negotiation in #779 with the orchestrator-owned embedding
selection in #789 and remains stacked on #765. The pin remains explicit and
immutable until the protected PRs merge; it is not a moving `main` reference
and it is not a LineageWeave monkey patch.

The runtime contract is:

Expand All @@ -33,6 +38,13 @@ The runtime contract is:
reconciliation prompt; independent VISION worker evidence is retained instead.
- A provider 4xx is reported as a failed orchestration attempt, never as a
successful empty semantic result.
- When HTTP 400/422 explicitly proves that `temperature` is unsupported, the
same endpoint is retried once with only that optional field omitted. Invalid
values and unrelated 4xx responses remain fail-closed.
- Embedding requests may omit `model`; the orchestrator selects only an enabled
embedding-capable agent and returns the resolved model for provenance.
- An omitted embedding model fails with `embedding_unavailable` when no eligible
agent exists; LineageWeave never supplies a heuristic or provider fallback.
- An empty seed model is expanded from the configured gateway `/v1/models`
endpoint; embedding-only rows are not added to the chat agent pool.
- `json_object`, `json_schema`, and Responses JSON formats run conduct plus
Expand All @@ -44,4 +56,5 @@ The runtime contract is:
implementation.
- Rebuilding the image is required after the upstream pin changes.
- Protected-branch review and merge remain external gates; this pin does not
bypass PR #765.
bypass ContextualWisdomLab/contextual-orchestrator#765 or
ContextualWisdomLab/contextual-orchestrator#779, #789, or #791.
49 changes: 49 additions & 0 deletions docs/adr/0118-orchestrator-owned-embedding-model-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# ADR 0118: Orchestrator-owned embedding model discovery and provenance

- Status: Accepted
- Date: 2026-08-20
- Scope: LineageWeave embedding ingestion and post-content backfill
- Figma: N/A

## Context

LineageWeave must send embedding work through contextual-orchestrator. The
orchestrator can select an embedding-capable agent when the request omits a
model, but LineageWeave still read `LLM_GATEWAY_EMBEDDING_MODEL` and treated a
local model string as a prerequisite. That made the consumer contradict the
paper-grounded model boundary in ADR 0076 and the shared agent contract.

## Decision

1. LineageWeave does not read or expose a provider embedding model selector.
2. The embedding request omits `model` when no explicit compatibility value is
supplied; contextual-orchestrator resolves the capability and returns the
served model code.
3. Every persisted unit or visual-region embedding stores that returned model
code in the normalized embedding table. If the boundary returns no model
code, the vector is not persisted as provenance-bearing evidence and the
ingestion job remains incomplete for retry.
4. Completeness checks in automatic mode require an embedding for every
embeddable unit and described image region, regardless of model code. An
explicit legacy model code, when supplied by a lower-level caller, retains
exact-model completeness matching but is not a runtime configuration path.
5. The upstream contextual-orchestrator capability-selection contract is a
dependency of this consumer change. No monkey patch, provider SDK, local
model ranking, or sentinel model name is permitted.

## Consequences

- Runtime configuration contains only the internal orchestrator boundary; the
provider credentials remain in `~/.env` at the orchestrator service boundary.
- Existing rows retain their actual `embedding_model_code`; no backfill uses a
guessed or synthetic model code.
- A temporarily incomplete orchestrator response fails closed for evidence,
while the source body remains available for retry.

## References

ContextualWisdomLab. (2026). *contextual-orchestrator capability selection
contract* (PR #789). https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/789

ContextualWisdomLab. (2026). *ADR 0076: Paper-grounded model policy*.
`docs/adr/0076-paper-grounded-model-policy.md`.
37 changes: 26 additions & 11 deletions lineageweave/embedding_client.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Backward-compat client lacks resolved_model_code and embed_many

OpenAiCompatibleEmbeddingClient (embedding_client.py) still takes model: str and only wraps embed; it exposes neither embed_many nor resolved_model_code. If it were ever used with persist_post_content in auto mode (embedding_model_code=None), getattr(embedding_client, 'resolved_model_code', None) would be None and every embedding would be discarded. No runtime path constructs this class (only ContextualOrchestratorEmbeddingClient is built via orchestrator_embedding_client), so this is currently harmless, but the compat wrapper is now inconsistent with the new provenance flow.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,19 @@ def embed(self, text: str) -> list[float]:


class ContextualOrchestratorEmbeddingClient:
"""Submit embeddings through contextual-orchestrator's batch boundary."""
"""Submit embeddings through contextual-orchestrator's batch boundary.

Model discovery belongs to the orchestrator. The returned model code is
retained for persistence so an automatic route still has provenance.
"""

available = True

def __init__(
self,
base_url: str,
api_key: str,
model: str,
model: str | None,
*,
timeout: float = 60.0,
poll_interval: float = 0.25,
Expand All @@ -71,7 +75,8 @@ def __init__(
if not self._base_url.endswith("/v1"):
self._base_url = f"{self._base_url}/v1"
self._api_key = api_key
self._model = model
self._model = model.strip() if isinstance(model, str) and model.strip() else None
self.resolved_model_code: str | None = self._model
self._timeout = timeout
self._poll_interval = poll_interval

Expand All @@ -84,17 +89,20 @@ def embed_many(self, texts: list[str]) -> list[list[float]]:
if not texts:
return []
headers = {"authorization": f"Bearer {self._api_key}"}
payload: dict[str, object] = {
"inputs": texts,
"endpoint": "/v1/embeddings",
"metadata": {"service": "lineageweave", "channel": "post_content_embedding"},
}
if self._model:
payload["model"] = self._model
Comment on lines 96 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: resolved_model_code reset prevents stale provenance across reused clients

embed_many resets self.resolved_model_code = self._model at the top of each call (embedding_client.py) before any early return. In post_content_persistence.py:189-191 the per-batch model code is read immediately after the awaited embed_many, so provenance is captured per batch. Because the scripts (backfill_post_content.py, import_postgresql_posts.py) reuse a single embedding client across many posts, this reset is what prevents a previous post's resolved model from leaking into a later post whose response omits model (covered by test_orchestrator_embedding_client_does_not_reuse_previous_model_provenance). When the reset falls back to None/empty, the vector is stored with a null model and skipped at insert time (post_content_persistence.py:290-314), keeping the job incomplete for retry — the intended fail-closed path.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

response = post_json(
f"{self._base_url}/batch/embeddings",
{
"model": self._model,
"inputs": texts,
"endpoint": "/v1/embeddings",
"metadata": {"service": "lineageweave", "channel": "post_content_embedding"},
},
payload,
headers=headers,
timeout=self._timeout,
)
self._record_resolved_model(response)
batch_id = response.get("batch_id")
if isinstance(batch_id, str) and batch_id:
deadline = time.monotonic() + self._timeout
Expand All @@ -112,6 +120,7 @@ def embed_many(self, texts: list[str]) -> list[list[float]]:
headers=headers,
timeout=self._timeout,
)
self._record_resolved_model(response)

vectors = self._vectors(response, len(texts))
if vectors is None:
Expand Down Expand Up @@ -139,10 +148,16 @@ def _vectors(response: dict, expected_count: int) -> list[list[float]] | None:
return None
return [vector for vector in ordered if vector is not None]

def _record_resolved_model(self, response: dict) -> None:
"""Keep the model selected by the orchestrator for DB provenance."""
model = response.get("model")
if isinstance(model, str) and model.strip():
self.resolved_model_code = model.strip()
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.


def orchestrator_embedding_client(base_url: str, api_key: str, model: str):
def orchestrator_embedding_client(base_url: str, api_key: str, model: str | None = None):
"""Build the batch embedding channel, or the unavailable null client."""
if not (base_url and api_key and model):
if not (base_url and api_key):
return NullEmbeddingClient()
return ContextualOrchestratorEmbeddingClient(base_url, api_key, model)
Comment on lines +168 to 172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: orchestrator_embedding_client no longer gates on model presence

orchestrator_embedding_client changed its availability gate from if not (base_url and api_key and model) to if not (base_url and api_key) (embedding_client.py). As a result, callers that pass an empty/None model now get a live ContextualOrchestratorEmbeddingClient (available=True) instead of a NullEmbeddingClient. I confirmed every runtime caller (backend/app/main.py:394, the scripts) either passes real base/key or explicitly checks .available and raises, so the gating now correctly depends only on the orchestrator boundary credentials. This matches the PR intent (orchestrator owns model discovery); noting it because it is a semantic contract change to a shared factory whose future callers will get a live client where they previously got Null.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


Expand Down
Loading