Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
62 changes: 59 additions & 3 deletions codenib/wiki/media_grounding.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import re
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Iterable, Mapping
from typing import Any, Callable, Iterable, Mapping

from ..repository_filters import walk_repository_files
from ..repository_source_selection import (
Expand Down Expand Up @@ -44,6 +44,9 @@
r"\b(?:class|def|function|const|let|var|interface|type|struct|enum)\s+([A-Za-z_][A-Za-z0-9_]*)"
)
_CAMEL_RE = re.compile(r"\b[A-Z][A-Za-z0-9_]{2,}\b")
VisualGroundingScorer = Callable[
[Mapping[str, Any], Mapping[str, Any]], Mapping[str, Any] | None
]


@dataclass(frozen=True)
Expand Down Expand Up @@ -159,8 +162,14 @@ def ground_visual_facts_to_sources(
source_candidates: Iterable[Mapping[str, Any]],
*,
max_bindings_per_entity: int = _MAX_BINDINGS_PER_ENTITY,
scorer: VisualGroundingScorer | None = None,
) -> dict[str, Any]:
"""Ground visual entities to a source inventory using deterministic scoring."""
"""Ground visual entities to a source inventory.

The default scorer is deterministic and lexical. Callers can pass a scorer
backed by BM25, embeddings, CodeGraph, LSP facts, or FactQueryIndex without
changing the visual-code binding manifest schema.
"""

candidates = [
_candidate_from_mapping(candidate)
Expand Down Expand Up @@ -188,11 +197,13 @@ def ground_visual_facts_to_sources(
scored = [
binding
for binding in (
_score_candidate(
_score_with_optional_scorer(
artifact_path=artifact_path,
entity=entity,
entity_name=entity_name,
hints=hints,
candidate=candidate,
scorer=scorer,
)
for candidate in candidates
)
Expand Down Expand Up @@ -229,6 +240,40 @@ def ground_visual_facts_to_sources(
return manifest.to_dict()


def _score_with_optional_scorer(
*,
artifact_path: str,
entity: Mapping[str, Any],
entity_name: str,
hints: Iterable[str],
candidate: SourceSymbolCandidate,
scorer: VisualGroundingScorer | None,
) -> VisualCodeBinding | None:
if scorer is None:
return _score_candidate(
artifact_path=artifact_path,
entity_name=entity_name,
hints=hints,
candidate=candidate,
)
raw = scorer(entity, candidate.to_dict())
if not isinstance(raw, Mapping):
return None
score = _confidence(raw.get("score"))
if score <= 0:
return None
return VisualCodeBinding(
artifact_path=artifact_path,
entity_name=entity_name,
source_path=candidate.path,
symbol=candidate.symbol,
kind=candidate.kind,
line=candidate.line,
score=round(score, 4),
evidence=_safe_text(raw.get("evidence") or "custom scorer"),
)


def _score_candidate(
*,
artifact_path: str,
Expand Down Expand Up @@ -324,6 +369,16 @@ def _positive_int(value: Any) -> int:
return max(0, number)


def _confidence(value: Any) -> float:
if isinstance(value, bool):
return 0.0
try:
confidence = float(value)
except (TypeError, ValueError):
return 0.0
return min(1.0, max(0.0, confidence))


def _sha256_json(payload: Mapping[str, Any]) -> str:
encoded = json.dumps(
payload,
Expand All @@ -343,6 +398,7 @@ def _safe_text(value: Any) -> str:
"MEDIA_GROUNDING_SCHEMA",
"MEDIA_GROUNDING_VERSION",
"SourceSymbolCandidate",
"VisualGroundingScorer",
"VisualCodeBinding",
"VisualGroundingManifest",
"discover_source_symbol_candidates",
Expand Down
4 changes: 3 additions & 1 deletion docs/experiments/multimodal_repository_knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ keeps the multimodal knowledge pipeline independent of a specific model family.
files and symbols. The first implementation uses deterministic lexical scoring
against a bounded source-symbol inventory. Later versions can replace the
scorer with BM25, embeddings, CodeGraph, LSP facts, or `FactQueryIndex` /
`FactBatch`.
`FactBatch`. The `ground_visual_facts_to_sources(..., scorer=...)` hook already
accepts a custom scorer, so graph/fact-backed ranking can be added without
changing the binding manifest schema.

### MultimodalKnowledgeView

Expand Down
43 changes: 43 additions & 0 deletions test/wiki/test_media_grounding.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,46 @@ def test_ground_visual_facts_to_sources_deduplicates_bindings():
manifest = ground_visual_facts_to_sources(visual_facts, [source, source])

assert len(manifest["bindings"]) == 1


def test_ground_visual_facts_to_sources_accepts_custom_scorer():
visual_facts = {
"manifest_sha256": "visual-facts-hash",
"facts": [
{
"artifact_path": "docs/architecture.svg",
"entities": [{"name": "DiagramBox"}],
}
],
}

def scorer(entity, candidate):
if entity["name"] == "DiagramBox" and candidate["symbol"] == "WikiService":
return {"score": 0.88, "evidence": "graph scorer match"}
return None

manifest = ground_visual_facts_to_sources(
visual_facts,
[
{
"path": "codenib/wiki/service.py",
"symbol": "WikiService",
"kind": "symbol",
"line": 17,
}
],
scorer=scorer,
)

assert manifest["bindings"] == [
{
"artifact_path": "docs/architecture.svg",
"entity_name": "DiagramBox",
"source_path": "codenib/wiki/service.py",
"symbol": "WikiService",
"kind": "symbol",
"line": 17,
"score": 0.88,
"evidence": "graph scorer match",
}
]
Loading