Skip to content

Commit fd6a8ca

Browse files
committed
feat: Integrate Phase 3 Context Encoder with Phase 2 Reasoner
- Add use_deep_context parameter to Reasoner.generate_refactor_plan() - Support dual context modes: * Phase 2: Fast markdown serializer (<50k tokens) * Phase 3: Jamba deep compression (200k+ tokens) - Add _context_to_raw_string() helper for graphstring conversion - Create 7 integration tests (all passing) - Backward compatible with existing Phase 2 workflows Architecture: Reasoner can now use either: - ContextSerializer (fast, existing) - ContextEncoder (deep, Jamba-based) Tests: 7/7 integration tests passing
1 parent 36b3f57 commit fd6a8ca

2 files changed

Lines changed: 330 additions & 5 deletions

File tree

src/reasoner/reasoner.py

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from src.librarian.retriever import GraphRetriever
2424
from src.librarian.context_serializer import ContextSerializer, CompressedContextBlock
2525
from src.architect.schemas import RefactorPlan, ValidationResult
26+
from src.context_encoder import ContextEncoder
2627

2728

2829
logger = logging.getLogger(__name__)
@@ -63,7 +64,11 @@ def __init__(self, config: Optional[ReasonerConfig] = None):
6364
# Database connections
6465
self.db = OuroborosGraphDB()
6566
self.retriever = GraphRetriever(self.db)
66-
self.serializer = ContextSerializer(format="markdown") # More token-efficient
67+
68+
# Context handling (Phase 2: Serializer, Phase 3: Encoder)
69+
self.serializer = ContextSerializer(format="markdown") # Fast, for <50k tokens
70+
self.encoder = ContextEncoder() # Phase 3: Jamba compression for 200k+ tokens
71+
6772
self.dependency_analyzer = DependencyAnalyzer(self.db)
6873

6974
logger.info(f"Reasoner initialized with provider: {self.config.provider}")
@@ -75,6 +80,7 @@ def generate_refactor_plan(
7580
target_symbol: Optional[str] = None,
7681
context_files: Optional[List[str]] = None,
7782
max_context_tokens: int = 100_000,
83+
use_deep_context: bool = False,
7884
) -> RefactorPlan:
7985
"""
8086
Generate a validated refactor plan for the given task.
@@ -85,6 +91,7 @@ def generate_refactor_plan(
8591
target_symbol: Specific symbol to refactor (optional)
8692
context_files: Additional files to include in context
8793
max_context_tokens: Maximum tokens for context (respects LLM limits)
94+
use_deep_context: Use Phase 3 Jamba encoder for massive context (200k+ tokens)
8895
8996
Returns:
9097
Validated RefactorPlan
@@ -99,7 +106,8 @@ def generate_refactor_plan(
99106
context_blocks = self._retrieve_context(
100107
target_file=target_file,
101108
context_files=context_files,
102-
max_tokens=max_context_tokens
109+
max_tokens=max_context_tokens,
110+
use_deep_context=use_deep_context
103111
)
104112

105113
if not context_blocks:
@@ -187,9 +195,16 @@ def _retrieve_context(
187195
self,
188196
target_file: Optional[str],
189197
context_files: Optional[List[str]],
190-
max_tokens: int
198+
max_tokens: int,
199+
use_deep_context: bool = False
191200
) -> List[CompressedContextBlock]:
192-
"""Retrieve and serialize context from graph."""
201+
"""
202+
Retrieve and serialize context from graph.
203+
204+
Uses either:
205+
- Phase 2: ContextSerializer (markdown, fast, <50k tokens)
206+
- Phase 3: ContextEncoder (Jamba compression, 200k+ tokens)
207+
"""
193208

194209
context_blocks = []
195210
total_tokens = 0
@@ -198,7 +213,30 @@ def _retrieve_context(
198213
if target_file:
199214
context = self.retriever.get_file_context(target_file)
200215
if context:
201-
block = self.serializer.serialize_file_context(context)
216+
if use_deep_context:
217+
# Phase 3: Use Jamba for deep compression
218+
raw_content = self._context_to_raw_string(context)
219+
compressed = self.encoder.compress(
220+
codebase_context=raw_content,
221+
target_files=[target_file],
222+
metadata={"task": "retrieve_context"}
223+
)
224+
# Wrap in CompressedContextBlock for compatibility
225+
block = CompressedContextBlock(
226+
block_id=f"deep_{target_file}",
227+
block_type="file",
228+
content=compressed.summary,
229+
format="markdown",
230+
token_count=compressed.tokens_out
231+
)
232+
logger.info(
233+
f"Deep context compression: {compressed.tokens_in} → "
234+
f"{compressed.tokens_out} tokens (ratio: {compressed.compression_ratio:.1f}x)"
235+
)
236+
else:
237+
# Phase 2: Use fast markdown serializer
238+
block = self.serializer.serialize_file_context(context)
239+
202240
context_blocks.append(block)
203241
total_tokens += block.token_count
204242

@@ -291,6 +329,54 @@ def _is_using_fallback(self) -> bool:
291329
"""Check if currently using fallback provider."""
292330
return self.config.provider == self.config.fallback_provider
293331

332+
def _context_to_raw_string(self, context: Dict[str, Any]) -> str:
333+
"""
334+
Convert graph context to raw string for Phase 3 encoding.
335+
336+
Args:
337+
context: Context dict from GraphRetriever
338+
339+
Returns:
340+
Raw string representation of the context
341+
"""
342+
parts = []
343+
344+
# Add file header
345+
if "file_path" in context:
346+
parts.append(f"# File: {context['file_path']}\n")
347+
348+
# Add imports
349+
if "imports" in context and context["imports"]:
350+
parts.append("## Imports")
351+
for imp in context["imports"]:
352+
parts.append(f"- {imp}")
353+
parts.append("")
354+
355+
# Add classes
356+
if "classes" in context:
357+
for cls in context["classes"]:
358+
parts.append(f"## Class: {cls.get('name', 'Unknown')}")
359+
if "methods" in cls:
360+
for method in cls["methods"]:
361+
parts.append(f" - {method.get('name', 'unknown')}()")
362+
parts.append("")
363+
364+
# Add functions
365+
if "functions" in context:
366+
parts.append("## Functions")
367+
for func in context["functions"]:
368+
parts.append(f"- {func.get('name', 'unknown')}()")
369+
parts.append("")
370+
371+
# Add full content if available
372+
if "content" in context:
373+
parts.append("## Full Content")
374+
parts.append("```")
375+
parts.append(context["content"])
376+
parts.append("```")
377+
378+
return "\n".join(parts)
379+
294380
def estimate_cost(
295381
self,
296382
task_description: str,
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""
2+
Integration test for Phase 2 Reasoner + Phase 3 Context Encoder
3+
=================================================================
4+
5+
Tests the integration between Reasoner and ContextEncoder.
6+
"""
7+
8+
import pytest
9+
from unittest.mock import Mock, patch
10+
from src.reasoner import Reasoner, ReasonerConfig
11+
from src.reasoner.config import LLMProvider
12+
from src.librarian.context_serializer import CompressedContextBlock
13+
14+
15+
@pytest.fixture
16+
def mock_reasoner():
17+
"""Create a reasoner with mocked LLM client."""
18+
config = ReasonerConfig(provider=LLMProvider.MOCK)
19+
reasoner = Reasoner(config)
20+
return reasoner
21+
22+
23+
@pytest.fixture
24+
def mock_graph_context():
25+
"""Mock context from GraphRetriever."""
26+
return {
27+
"file_path": "src/auth/login.ts",
28+
"imports": ["{ User } from '../models/user'", "{ Firebase } from '../lib/firebase'"],
29+
"classes": [
30+
{
31+
"name": "LoginService",
32+
"methods": [
33+
{"name": "loginWithFirebase"},
34+
{"name": "logout"}
35+
]
36+
}
37+
],
38+
"functions": [],
39+
"content": """
40+
export class LoginService {
41+
async loginWithFirebase(email: string, password: string) {
42+
// Implementation
43+
}
44+
}
45+
"""
46+
}
47+
48+
49+
def test_reasoner_has_encoder(mock_reasoner):
50+
"""Test that Reasoner has Context Encoder initialized."""
51+
assert mock_reasoner.encoder is not None
52+
assert hasattr(mock_reasoner.encoder, 'compress')
53+
54+
55+
def test_reasoner_has_serializer(mock_reasoner):
56+
"""Test that Reasoner still has serializer for backward compatibility."""
57+
assert mock_reasoner.serializer is not None
58+
59+
60+
def test_context_to_raw_string(mock_reasoner, mock_graph_context):
61+
"""Test conversion of graph context to raw string."""
62+
raw_string = mock_reasoner._context_to_raw_string(mock_graph_context)
63+
64+
assert "# File: src/auth/login.ts" in raw_string
65+
assert "{ User }" in raw_string
66+
assert "LoginService" in raw_string
67+
assert "loginWithFirebase" in raw_string
68+
69+
70+
@patch.object(Reasoner, '_retrieve_context')
71+
def test_generate_refactor_plan_without_deep_context(mock_retrieve, mock_reasoner):
72+
"""Test standard refactor plan generation (Phase 2 only)."""
73+
# Mock context retrieval
74+
mock_retrieve.return_value = [
75+
CompressedContextBlock(
76+
block_id="mock_1",
77+
block_type="file",
78+
content="# Mock context",
79+
format="markdown",
80+
token_count=100
81+
)
82+
]
83+
84+
# Mock LLM response
85+
mock_reasoner.llm_client.generate = Mock(return_value=Mock(
86+
content='{"plan_id": "test", "files": [], "dependencies": [], "estimated_impact": "low"}',
87+
output_tokens=50,
88+
cost_usd=0.001
89+
))
90+
91+
# Mock parser
92+
from src.architect.schemas import RefactorPlan
93+
mock_plan = RefactorPlan(
94+
plan_id="test",
95+
description="Test refactor",
96+
files=[],
97+
primary_changes=[],
98+
dependencies=[],
99+
estimated_impact="low"
100+
)
101+
mock_reasoner.plan_parser.parse = Mock(return_value=(mock_plan, Mock(is_valid=True, errors=[], warnings=[])))
102+
mock_reasoner.plan_validator.validate_plan = Mock(return_value=Mock(is_valid=True, errors=[], warnings=[]))
103+
104+
# Generate plan (without deep context)
105+
plan = mock_reasoner.generate_refactor_plan(
106+
task_description="Refactor auth",
107+
target_file="auth.ts",
108+
use_deep_context=False # Use Phase 2 serializer
109+
)
110+
111+
assert plan is not None
112+
assert plan.plan_id == "test"
113+
114+
# Verify _retrieve_context was called with use_deep_context=False
115+
mock_retrieve.assert_called_once()
116+
call_kwargs = mock_retrieve.call_args[1]
117+
assert call_kwargs['use_deep_context'] == False
118+
119+
120+
@patch.object(Reasoner, '_retrieve_context')
121+
def test_generate_refactor_plan_with_deep_context(mock_retrieve, mock_reasoner):
122+
"""Test deep context refactor plan generation (Phase 3 encoder)."""
123+
# Mock context retrieval with deep context
124+
mock_retrieve.return_value = [
125+
CompressedContextBlock(
126+
block_id="mock_2",
127+
block_type="file",
128+
content="# Compressed context from Jamba",
129+
format="markdown",
130+
token_count=500
131+
)
132+
]
133+
134+
# Mock LLM response
135+
mock_reasoner.llm_client.generate = Mock(return_value=Mock(
136+
content='{"plan_id": "test_deep", "files": [], "dependencies": [], "estimated_impact": "medium"}',
137+
output_tokens=100,
138+
cost_usd=0.002
139+
))
140+
141+
# Mock parser
142+
from src.architect.schemas import RefactorPlan
143+
mock_plan = RefactorPlan(
144+
plan_id="test_deep",
145+
description="Test deep refactor",
146+
files=[],
147+
primary_changes=[],
148+
dependencies=[],
149+
estimated_impact="medium"
150+
)
151+
mock_reasoner.plan_parser.parse = Mock(return_value=(mock_plan, Mock(is_valid=True, errors=[], warnings=[])))
152+
mock_reasoner.plan_validator.validate_plan = Mock(return_value=Mock(is_valid=True, errors=[], warnings=[]))
153+
154+
# Generate plan (with deep context)
155+
plan = mock_reasoner.generate_refactor_plan(
156+
task_description="Refactor entire auth system",
157+
target_file="auth.ts",
158+
use_deep_context=True # Use Phase 3 encoder
159+
)
160+
161+
assert plan is not None
162+
assert plan.plan_id == "test_deep"
163+
164+
# Verify _retrieve_context was called with use_deep_context=True
165+
mock_retrieve.assert_called_once()
166+
call_kwargs = mock_retrieve.call_args[1]
167+
assert call_kwargs['use_deep_context'] == True
168+
169+
170+
@patch('src.reasoner.reasoner.GraphRetriever')
171+
def test_retrieve_context_with_encoder(mock_retriever_class, mock_reasoner, mock_graph_context):
172+
"""Test that _retrieve_context uses encoder when use_deep_context=True."""
173+
# Mock retriever
174+
mock_retriever = Mock()
175+
mock_retriever.get_file_context.return_value = mock_graph_context
176+
mock_reasoner.retriever = mock_retriever
177+
178+
# Mock encoder
179+
from src.context_encoder.encoder import CompressedContext
180+
mock_compressed = CompressedContext(
181+
summary="# Technical Summary\nCompressed context",
182+
file_references=["src/auth/login.ts"],
183+
tokens_in=1000,
184+
tokens_out=200,
185+
compression_ratio=5.0,
186+
metadata={"model": "jamba"}
187+
)
188+
mock_reasoner.encoder.compress = Mock(return_value=mock_compressed)
189+
190+
# Retrieve context with deep encoding
191+
blocks = mock_reasoner._retrieve_context(
192+
target_file="src/auth/login.ts",
193+
context_files=None,
194+
max_tokens=100_000,
195+
use_deep_context=True
196+
)
197+
198+
# Verify encoder was called
199+
mock_reasoner.encoder.compress.assert_called_once()
200+
201+
# Verify block was created
202+
assert len(blocks) == 1
203+
assert blocks[0].content == "# Technical Summary\nCompressed context"
204+
assert blocks[0].token_count == 200
205+
206+
207+
@patch('src.reasoner.reasoner.GraphRetriever')
208+
def test_retrieve_context_without_encoder(mock_retriever_class, mock_reasoner, mock_graph_context):
209+
"""Test that _retrieve_context uses serializer when use_deep_context=False."""
210+
# Mock retriever
211+
mock_retriever = Mock()
212+
mock_retriever.get_file_context.return_value = mock_graph_context
213+
mock_reasoner.retriever = mock_retriever
214+
215+
# Mock serializer
216+
mock_block = CompressedContextBlock(
217+
block_id="mock_3",
218+
block_type="file",
219+
content="# Markdown context",
220+
format="markdown",
221+
token_count=150
222+
)
223+
mock_reasoner.serializer.serialize_file_context = Mock(return_value=mock_block)
224+
225+
# Retrieve context without deep encoding
226+
blocks = mock_reasoner._retrieve_context(
227+
target_file="src/auth/login.ts",
228+
context_files=None,
229+
max_tokens=100_000,
230+
use_deep_context=False
231+
)
232+
233+
# Verify serializer was called
234+
mock_reasoner.serializer.serialize_file_context.assert_called_once()
235+
236+
# Verify block was created
237+
assert len(blocks) == 1
238+
assert blocks[0].content == "# Markdown context"
239+
assert blocks[0].token_count == 150

0 commit comments

Comments
 (0)