mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-26 17:06:22 +02:00
Enhance retrieval pipelines: 4-stage GraphRAG, DocRAG grounding (#697)
Enhance retrieval pipelines: 4-stage GraphRAG, DocRAG grounding, consistent PROV-O GraphRAG: - Split retrieval into 4 prompt stages: extract-concepts, kg-edge-scoring, kg-edge-reasoning, kg-synthesis (was single-stage) - Add concept extraction (grounding) for per-concept embedding - Filter main query to default graph, ignoring provenance/explainability edges - Add source document edges to knowledge graph DocumentRAG: - Add grounding step with concept extraction, matching GraphRAG's pattern: Question → Grounding → Exploration → Synthesis - Per-concept embedding and chunk retrieval with deduplication Cross-pipeline: - Make PROV-O derivation links consistent: wasGeneratedBy for first entity from Activity, wasDerivedFrom for entity-to-entity chains - Update CLIs (tg-invoke-agent, tg-invoke-graph-rag, tg-invoke-document-rag) for new explainability structure - Fix all affected unit and integration tests
This commit is contained in:
parent
29b4300808
commit
a115ec06ab
25 changed files with 1537 additions and 1008 deletions
|
|
@ -15,10 +15,11 @@ from trustgraph.provenance.agent import (
|
|||
|
||||
from trustgraph.provenance.namespaces import (
|
||||
RDF_TYPE, RDFS_LABEL,
|
||||
PROV_ACTIVITY, PROV_ENTITY, PROV_WAS_DERIVED_FROM, PROV_STARTED_AT_TIME,
|
||||
TG_QUERY, TG_THOUGHT, TG_ACTION, TG_ARGUMENTS, TG_OBSERVATION, TG_ANSWER,
|
||||
PROV_ACTIVITY, PROV_ENTITY, PROV_WAS_DERIVED_FROM,
|
||||
PROV_WAS_GENERATED_BY, PROV_STARTED_AT_TIME,
|
||||
TG_QUERY, TG_THOUGHT, TG_ACTION, TG_ARGUMENTS, TG_OBSERVATION,
|
||||
TG_QUESTION, TG_ANALYSIS, TG_CONCLUSION, TG_DOCUMENT,
|
||||
TG_THOUGHT_DOCUMENT, TG_OBSERVATION_DOCUMENT,
|
||||
TG_ANSWER_TYPE, TG_REFLECTION_TYPE, TG_THOUGHT_TYPE, TG_OBSERVATION_TYPE,
|
||||
TG_AGENT_QUESTION,
|
||||
)
|
||||
|
||||
|
|
@ -110,84 +111,107 @@ class TestAgentSessionTriples:
|
|||
class TestAgentIterationTriples:
|
||||
|
||||
ITER_URI = "urn:trustgraph:agent:test-session/i1"
|
||||
PARENT_URI = "urn:trustgraph:agent:test-session"
|
||||
SESSION_URI = "urn:trustgraph:agent:test-session"
|
||||
PREV_URI = "urn:trustgraph:agent:test-session/i0"
|
||||
|
||||
def test_iteration_types(self):
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
thought="thinking", action="search", observation="found it",
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="search",
|
||||
)
|
||||
assert has_type(triples, self.ITER_URI, PROV_ENTITY)
|
||||
assert has_type(triples, self.ITER_URI, TG_ANALYSIS)
|
||||
|
||||
def test_iteration_derived_from_parent(self):
|
||||
def test_first_iteration_generated_by_question(self):
|
||||
"""First iteration uses wasGeneratedBy to link to question activity."""
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="search",
|
||||
)
|
||||
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.ITER_URI)
|
||||
assert gen is not None
|
||||
assert gen.o.iri == self.SESSION_URI
|
||||
# Should NOT have wasDerivedFrom
|
||||
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.ITER_URI)
|
||||
assert derived is None
|
||||
|
||||
def test_subsequent_iteration_derived_from_previous(self):
|
||||
"""Subsequent iterations use wasDerivedFrom to link to previous iteration."""
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, previous_uri=self.PREV_URI,
|
||||
action="search",
|
||||
)
|
||||
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.ITER_URI)
|
||||
assert derived is not None
|
||||
assert derived.o.iri == self.PARENT_URI
|
||||
assert derived.o.iri == self.PREV_URI
|
||||
# Should NOT have wasGeneratedBy
|
||||
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.ITER_URI)
|
||||
assert gen is None
|
||||
|
||||
def test_iteration_label_includes_action(self):
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="graph-rag-query",
|
||||
)
|
||||
label = find_triple(triples, RDFS_LABEL, self.ITER_URI)
|
||||
assert label is not None
|
||||
assert "graph-rag-query" in label.o.value
|
||||
|
||||
def test_iteration_thought_inline(self):
|
||||
def test_iteration_thought_sub_entity(self):
|
||||
"""Thought is a sub-entity with Reflection and Thought types."""
|
||||
thought_uri = "urn:trustgraph:agent:test-session/i1/thought"
|
||||
thought_doc = "urn:doc:thought-1"
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
thought="I need to search for info",
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="search",
|
||||
thought_uri=thought_uri,
|
||||
thought_document_id=thought_doc,
|
||||
)
|
||||
thought = find_triple(triples, TG_THOUGHT, self.ITER_URI)
|
||||
assert thought is not None
|
||||
assert thought.o.value == "I need to search for info"
|
||||
# Iteration links to thought sub-entity
|
||||
thought_link = find_triple(triples, TG_THOUGHT, self.ITER_URI)
|
||||
assert thought_link is not None
|
||||
assert thought_link.o.iri == thought_uri
|
||||
# Thought has correct types
|
||||
assert has_type(triples, thought_uri, TG_REFLECTION_TYPE)
|
||||
assert has_type(triples, thought_uri, TG_THOUGHT_TYPE)
|
||||
# Thought was generated by iteration
|
||||
gen = find_triple(triples, PROV_WAS_GENERATED_BY, thought_uri)
|
||||
assert gen is not None
|
||||
assert gen.o.iri == self.ITER_URI
|
||||
# Thought has document reference
|
||||
doc = find_triple(triples, TG_DOCUMENT, thought_uri)
|
||||
assert doc is not None
|
||||
assert doc.o.iri == thought_doc
|
||||
|
||||
def test_iteration_thought_document_preferred(self):
|
||||
"""When thought_document_id is provided, inline thought is not stored."""
|
||||
def test_iteration_observation_sub_entity(self):
|
||||
"""Observation is a sub-entity with Reflection and Observation types."""
|
||||
obs_uri = "urn:trustgraph:agent:test-session/i1/observation"
|
||||
obs_doc = "urn:doc:obs-1"
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
thought="inline thought",
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="search",
|
||||
thought_document_id="urn:doc:thought-1",
|
||||
observation_uri=obs_uri,
|
||||
observation_document_id=obs_doc,
|
||||
)
|
||||
thought_doc = find_triple(triples, TG_THOUGHT_DOCUMENT, self.ITER_URI)
|
||||
assert thought_doc is not None
|
||||
assert thought_doc.o.iri == "urn:doc:thought-1"
|
||||
thought_inline = find_triple(triples, TG_THOUGHT, self.ITER_URI)
|
||||
assert thought_inline is None
|
||||
|
||||
def test_iteration_observation_inline(self):
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
action="search",
|
||||
observation="Found 3 results",
|
||||
)
|
||||
obs = find_triple(triples, TG_OBSERVATION, self.ITER_URI)
|
||||
assert obs is not None
|
||||
assert obs.o.value == "Found 3 results"
|
||||
|
||||
def test_iteration_observation_document_preferred(self):
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
action="search",
|
||||
observation="inline obs",
|
||||
observation_document_id="urn:doc:obs-1",
|
||||
)
|
||||
obs_doc = find_triple(triples, TG_OBSERVATION_DOCUMENT, self.ITER_URI)
|
||||
assert obs_doc is not None
|
||||
assert obs_doc.o.iri == "urn:doc:obs-1"
|
||||
obs_inline = find_triple(triples, TG_OBSERVATION, self.ITER_URI)
|
||||
assert obs_inline is None
|
||||
# Iteration links to observation sub-entity
|
||||
obs_link = find_triple(triples, TG_OBSERVATION, self.ITER_URI)
|
||||
assert obs_link is not None
|
||||
assert obs_link.o.iri == obs_uri
|
||||
# Observation has correct types
|
||||
assert has_type(triples, obs_uri, TG_REFLECTION_TYPE)
|
||||
assert has_type(triples, obs_uri, TG_OBSERVATION_TYPE)
|
||||
# Observation was generated by iteration
|
||||
gen = find_triple(triples, PROV_WAS_GENERATED_BY, obs_uri)
|
||||
assert gen is not None
|
||||
assert gen.o.iri == self.ITER_URI
|
||||
# Observation has document reference
|
||||
doc = find_triple(triples, TG_DOCUMENT, obs_uri)
|
||||
assert doc is not None
|
||||
assert doc.o.iri == obs_doc
|
||||
|
||||
def test_iteration_action_recorded(self):
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="graph-rag-query",
|
||||
)
|
||||
action = find_triple(triples, TG_ACTION, self.ITER_URI)
|
||||
|
|
@ -197,7 +221,7 @@ class TestAgentIterationTriples:
|
|||
def test_iteration_arguments_json_encoded(self):
|
||||
args = {"query": "test query", "limit": 10}
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="search",
|
||||
arguments=args,
|
||||
)
|
||||
|
|
@ -208,7 +232,7 @@ class TestAgentIterationTriples:
|
|||
|
||||
def test_iteration_default_arguments_empty_dict(self):
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="search",
|
||||
)
|
||||
arguments = find_triple(triples, TG_ARGUMENTS, self.ITER_URI)
|
||||
|
|
@ -219,7 +243,7 @@ class TestAgentIterationTriples:
|
|||
def test_iteration_no_thought_or_observation(self):
|
||||
"""Minimal iteration with just action — no thought or observation triples."""
|
||||
triples = agent_iteration_triples(
|
||||
self.ITER_URI, self.PARENT_URI,
|
||||
self.ITER_URI, question_uri=self.SESSION_URI,
|
||||
action="noop",
|
||||
)
|
||||
thought = find_triple(triples, TG_THOUGHT, self.ITER_URI)
|
||||
|
|
@ -228,19 +252,19 @@ class TestAgentIterationTriples:
|
|||
assert obs is None
|
||||
|
||||
def test_iteration_chaining(self):
|
||||
"""Second iteration derives from first iteration, not session."""
|
||||
"""First iteration uses wasGeneratedBy, second uses wasDerivedFrom."""
|
||||
iter1_uri = "urn:trustgraph:agent:sess/i1"
|
||||
iter2_uri = "urn:trustgraph:agent:sess/i2"
|
||||
|
||||
triples1 = agent_iteration_triples(
|
||||
iter1_uri, self.PARENT_URI, action="step1",
|
||||
iter1_uri, question_uri=self.SESSION_URI, action="step1",
|
||||
)
|
||||
triples2 = agent_iteration_triples(
|
||||
iter2_uri, iter1_uri, action="step2",
|
||||
iter2_uri, previous_uri=iter1_uri, action="step2",
|
||||
)
|
||||
|
||||
derived1 = find_triple(triples1, PROV_WAS_DERIVED_FROM, iter1_uri)
|
||||
assert derived1.o.iri == self.PARENT_URI
|
||||
gen1 = find_triple(triples1, PROV_WAS_GENERATED_BY, iter1_uri)
|
||||
assert gen1.o.iri == self.SESSION_URI
|
||||
|
||||
derived2 = find_triple(triples2, PROV_WAS_DERIVED_FROM, iter2_uri)
|
||||
assert derived2.o.iri == iter1_uri
|
||||
|
|
@ -253,42 +277,50 @@ class TestAgentIterationTriples:
|
|||
class TestAgentFinalTriples:
|
||||
|
||||
FINAL_URI = "urn:trustgraph:agent:test-session/final"
|
||||
PARENT_URI = "urn:trustgraph:agent:test-session/i3"
|
||||
PREV_URI = "urn:trustgraph:agent:test-session/i3"
|
||||
SESSION_URI = "urn:trustgraph:agent:test-session"
|
||||
|
||||
def test_final_types(self):
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, self.PARENT_URI, answer="42"
|
||||
self.FINAL_URI, previous_uri=self.PREV_URI,
|
||||
)
|
||||
assert has_type(triples, self.FINAL_URI, PROV_ENTITY)
|
||||
assert has_type(triples, self.FINAL_URI, TG_CONCLUSION)
|
||||
assert has_type(triples, self.FINAL_URI, TG_ANSWER_TYPE)
|
||||
|
||||
def test_final_derived_from_parent(self):
|
||||
def test_final_derived_from_previous(self):
|
||||
"""Conclusion with iterations uses wasDerivedFrom."""
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, self.PARENT_URI, answer="42"
|
||||
self.FINAL_URI, previous_uri=self.PREV_URI,
|
||||
)
|
||||
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.FINAL_URI)
|
||||
assert derived is not None
|
||||
assert derived.o.iri == self.PARENT_URI
|
||||
assert derived.o.iri == self.PREV_URI
|
||||
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.FINAL_URI)
|
||||
assert gen is None
|
||||
|
||||
def test_final_generated_by_question_when_no_iterations(self):
|
||||
"""When agent answers immediately, final uses wasGeneratedBy."""
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, question_uri=self.SESSION_URI,
|
||||
)
|
||||
gen = find_triple(triples, PROV_WAS_GENERATED_BY, self.FINAL_URI)
|
||||
assert gen is not None
|
||||
assert gen.o.iri == self.SESSION_URI
|
||||
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.FINAL_URI)
|
||||
assert derived is None
|
||||
|
||||
def test_final_label(self):
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, self.PARENT_URI, answer="42"
|
||||
self.FINAL_URI, previous_uri=self.PREV_URI,
|
||||
)
|
||||
label = find_triple(triples, RDFS_LABEL, self.FINAL_URI)
|
||||
assert label is not None
|
||||
assert label.o.value == "Conclusion"
|
||||
|
||||
def test_final_inline_answer(self):
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, self.PARENT_URI, answer="The answer is 42"
|
||||
)
|
||||
answer = find_triple(triples, TG_ANSWER, self.FINAL_URI)
|
||||
assert answer is not None
|
||||
assert answer.o.value == "The answer is 42"
|
||||
|
||||
def test_final_document_reference(self):
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, self.PARENT_URI,
|
||||
self.FINAL_URI, previous_uri=self.PREV_URI,
|
||||
document_id="urn:trustgraph:agent:sess/answer",
|
||||
)
|
||||
doc = find_triple(triples, TG_DOCUMENT, self.FINAL_URI)
|
||||
|
|
@ -296,29 +328,9 @@ class TestAgentFinalTriples:
|
|||
assert doc.o.type == IRI
|
||||
assert doc.o.iri == "urn:trustgraph:agent:sess/answer"
|
||||
|
||||
def test_final_document_takes_precedence(self):
|
||||
def test_final_no_document(self):
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, self.PARENT_URI,
|
||||
answer="inline",
|
||||
document_id="urn:doc:123",
|
||||
self.FINAL_URI, previous_uri=self.PREV_URI,
|
||||
)
|
||||
doc = find_triple(triples, TG_DOCUMENT, self.FINAL_URI)
|
||||
assert doc is not None
|
||||
answer = find_triple(triples, TG_ANSWER, self.FINAL_URI)
|
||||
assert answer is None
|
||||
|
||||
def test_final_no_answer_or_document(self):
|
||||
triples = agent_final_triples(self.FINAL_URI, self.PARENT_URI)
|
||||
answer = find_triple(triples, TG_ANSWER, self.FINAL_URI)
|
||||
doc = find_triple(triples, TG_DOCUMENT, self.FINAL_URI)
|
||||
assert answer is None
|
||||
assert doc is None
|
||||
|
||||
def test_final_derives_from_session_when_no_iterations(self):
|
||||
"""When agent answers immediately, final derives from session."""
|
||||
session_uri = "urn:trustgraph:agent:test-session"
|
||||
triples = agent_final_triples(
|
||||
self.FINAL_URI, session_uri, answer="direct answer"
|
||||
)
|
||||
derived = find_triple(triples, PROV_WAS_DERIVED_FROM, self.FINAL_URI)
|
||||
assert derived.o.iri == session_uri
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue