Skip to content

Commit b716d7d

Browse files
Add Search/RAG integration for grounded verification (Issue #2)
- Add PerplexityAdapter for RAG-native models (sonar, sonar-pro, etc.) with built-in web search and citations support - Add SearchBackend interface with implementations: - BraveSearchBackend (Brave Search API) - SerpAPIBackend (Google Search via SerpAPI) - TavilySearchBackend (AI-optimized search) - Add SearchRegistry for managing multiple search backends - Add verify_claim_with_search() for RAG-grounded verification - Add verify_claim_grounded() to AICP protocol - Add citations field to Response schema - Add source_citations and retrieved_sources to VerificationResult - Add Perplexity model capabilities to router (4 models) - Add 40 new tests (Perplexity adapter + search backends) - Update README with Perplexity and RAG verification documentation
1 parent 846b1d1 commit b716d7d

File tree

10 files changed

+1081
-3
lines changed

10 files changed

+1081
-3
lines changed

README.md

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,52 @@ LITELLM_API_KEY=sk-1234
268268
LITELLM_MODELS=gpt-4o,claude-3-opus,llama3.1
269269
```
270270

271+
### Perplexity (RAG-Native)
272+
273+
i2i integrates with [Perplexity](https://perplexity.ai) for RAG-native models with built-in web search and citations:
274+
275+
```env
276+
PERPLEXITY_API_KEY=pplx-...
277+
```
278+
279+
Perplexity models automatically search the web and return citations:
280+
281+
```python
282+
# Query with automatic web search
283+
result = await protocol.query(
284+
"What is the current stock price of Apple?",
285+
model="perplexity/sonar-pro"
286+
)
287+
print(result.content)
288+
print(result.citations) # ['https://finance.yahoo.com/...', ...]
289+
```
290+
291+
Available models: `sonar`, `sonar-pro`, `sonar-deep-research`, `sonar-reasoning-pro`
292+
293+
### Search-Grounded Verification (RAG)
294+
295+
i2i provides RAG-grounded verification that retrieves external sources before verifying claims:
296+
297+
```python
298+
# Verify a claim with search grounding
299+
result = await protocol.verify_claim_grounded(
300+
"The Eiffel Tower is 330 meters tall",
301+
search_backend="brave" # or "serpapi", "tavily"
302+
)
303+
print(f"Verified: {result.verified}")
304+
print(f"Confidence: {result.confidence}")
305+
print(f"Sources: {result.source_citations}")
306+
print(f"Retrieved: {result.retrieved_sources}")
307+
```
308+
309+
Configure search backends:
310+
```env
311+
# Choose one or more (first configured is used as fallback)
312+
BRAVE_API_KEY=BSA... # https://brave.com/search/api/
313+
SERPAPI_API_KEY=... # https://serpapi.com/
314+
TAVILY_API_KEY=tvly-... # https://tavily.com/
315+
```
316+
271317
### Configuring Default Models
272318

273319
Models are **not hardcoded**. Configure via `config.json`, environment variables, or CLI:
@@ -759,6 +805,7 @@ protocol = AICP()
759805
| **Cohere** | Command A, Command A Reasoning | ✅ Supported |
760806
| **Ollama** | Llama 3.2, Mistral, CodeLlama, Phi-3, Gemma 2, etc. | ✅ Supported (Local) |
761807
| **LiteLLM** | 100+ models via unified proxy | ✅ Supported |
808+
| **Perplexity** | Sonar, Sonar Pro, Deep Research, Reasoning Pro | ✅ Supported (RAG) |
762809

763810
---
764811

@@ -798,9 +845,9 @@ The RFC defines:
798845
│ ┌────────┐ ┌──────────┐ ┌────────┐ ┌────────┐ ┌───────────┐ │
799846
│ │ OpenAI │ │Anthropic │ │ Google │ │Mistral │ │Groq/Llama │ │
800847
│ └────────┘ └──────────┘ └────────┘ └────────┘ └───────────┘ │
801-
│ ┌────────┐ ┌──────────┐
802-
│ │ Ollama │ │ LiteLLM │ ← Local & Proxy providers
803-
│ └────────┘ └──────────┘
848+
│ ┌────────┐ ┌──────────┐ ┌────────────┐
849+
│ │ Ollama │ │ LiteLLM │ │ Perplexity │ ← Local/Proxy/RAG
850+
│ └────────┘ └──────────┘ └────────────┘
804851
└─────────────────────────────────────────────────────────────────┘
805852
```
806853

i2i/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@
3737
get_epistemic_models,
3838
DEFAULTS,
3939
)
40+
from .search import (
41+
SearchBackend,
42+
SearchResult,
43+
SearchRegistry,
44+
BraveSearchBackend,
45+
SerpAPIBackend,
46+
TavilySearchBackend,
47+
)
4048

4149
__version__ = "0.1.0"
4250
__all__ = [
@@ -71,4 +79,11 @@
7179
"get_synthesis_models",
7280
"get_verification_models",
7381
"get_epistemic_models",
82+
# Search/RAG
83+
"SearchBackend",
84+
"SearchResult",
85+
"SearchRegistry",
86+
"BraveSearchBackend",
87+
"SerpAPIBackend",
88+
"TavilySearchBackend",
7489
]

i2i/protocol.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,42 @@ async def verify_claim(
215215
context=context,
216216
)
217217

218+
async def verify_claim_grounded(
219+
self,
220+
claim: str,
221+
verifiers: Optional[List[str]] = None,
222+
search_backend: Optional[str] = None,
223+
num_sources: int = 5,
224+
original_source: Optional[str] = None,
225+
) -> VerificationResult:
226+
"""
227+
Verify a claim with search-grounded evidence (RAG verification).
228+
229+
This method retrieves relevant sources from the web before
230+
asking verification models to evaluate the claim. The result
231+
includes source citations for transparency.
232+
233+
Args:
234+
claim: The claim/statement to verify
235+
verifiers: Models to use for verification
236+
search_backend: Search backend to use (brave, serpapi, tavily)
237+
num_sources: Number of sources to retrieve (default: 5)
238+
original_source: Model that originally made the claim
239+
240+
Returns:
241+
VerificationResult with source_citations and retrieved_sources
242+
"""
243+
if verifiers is None:
244+
verifiers = self._get_default_models()[:2]
245+
246+
return await self.verification_engine.verify_claim_with_search(
247+
claim=claim,
248+
verifier_models=verifiers,
249+
search_backend=search_backend,
250+
num_sources=num_sources,
251+
original_source=original_source,
252+
)
253+
218254
async def challenge_response(
219255
self,
220256
response: Response,

i2i/providers.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,93 @@ def _extract_confidence(self, content: str) -> ConfidenceLevel:
656656
return ConfidenceLevel.MEDIUM
657657

658658

659+
class PerplexityAdapter(ProviderAdapter):
660+
"""
661+
Adapter for Perplexity AI.
662+
663+
Perplexity provides RAG-native models with built-in web search and citations.
664+
Uses OpenAI-compatible API at https://api.perplexity.ai
665+
"""
666+
667+
MODELS = ["sonar", "sonar-pro", "sonar-deep-research", "sonar-reasoning-pro"]
668+
669+
def __init__(self):
670+
self.api_key = os.getenv("PERPLEXITY_API_KEY")
671+
self._client = None
672+
673+
@property
674+
def provider_name(self) -> str:
675+
return "perplexity"
676+
677+
@property
678+
def available_models(self) -> List[str]:
679+
return self.MODELS
680+
681+
def is_configured(self) -> bool:
682+
return bool(self.api_key)
683+
684+
@property
685+
def client(self):
686+
if self._client is None:
687+
from openai import AsyncOpenAI
688+
self._client = AsyncOpenAI(
689+
api_key=self.api_key,
690+
base_url="https://api.perplexity.ai"
691+
)
692+
return self._client
693+
694+
async def query(self, message: Message, model: str) -> Response:
695+
start_time = time.time()
696+
697+
# Build messages
698+
messages = []
699+
if message.context:
700+
for ctx_msg in message.context:
701+
role = "assistant" if ctx_msg.sender else "user"
702+
messages.append({"role": role, "content": ctx_msg.content})
703+
messages.append({"role": "user", "content": message.content})
704+
705+
response = await self.client.chat.completions.create(
706+
model=model,
707+
messages=messages,
708+
temperature=0.2, # Lower for factual queries
709+
)
710+
711+
latency = (time.time() - start_time) * 1000
712+
content = response.choices[0].message.content
713+
714+
# Extract citations from response (Perplexity includes search_results)
715+
citations = None
716+
if hasattr(response, 'search_results') and response.search_results:
717+
citations = [r.get('url') for r in response.search_results if r.get('url')]
718+
719+
return Response(
720+
message_id=message.id,
721+
model=f"perplexity/{model}",
722+
content=content,
723+
confidence=self._extract_confidence(content),
724+
citations=citations,
725+
input_tokens=response.usage.prompt_tokens if response.usage else None,
726+
output_tokens=response.usage.completion_tokens if response.usage else None,
727+
latency_ms=latency,
728+
)
729+
730+
def _extract_confidence(self, content: str) -> ConfidenceLevel:
731+
"""Heuristically extract confidence from response content."""
732+
content_lower = content.lower()
733+
if any(phrase in content_lower for phrase in ["i'm certain", "definitely", "absolutely", "i'm confident"]):
734+
return ConfidenceLevel.VERY_HIGH
735+
elif any(phrase in content_lower for phrase in ["i believe", "likely", "probably"]):
736+
return ConfidenceLevel.HIGH
737+
elif any(phrase in content_lower for phrase in ["i think", "possibly", "might"]):
738+
return ConfidenceLevel.MEDIUM
739+
elif any(phrase in content_lower for phrase in ["i'm not sure", "uncertain", "hard to say"]):
740+
return ConfidenceLevel.LOW
741+
elif any(phrase in content_lower for phrase in ["i don't know", "impossible to determine"]):
742+
return ConfidenceLevel.VERY_LOW
743+
return ConfidenceLevel.MEDIUM
744+
745+
659746
class ProviderRegistry:
660747
"""
661748
Registry of all available AI providers.
@@ -677,6 +764,7 @@ def __init__(self):
677764
self._register_adapter(CohereAdapter())
678765
self._register_adapter(OllamaAdapter())
679766
self._register_adapter(LiteLLMAdapter())
767+
self._register_adapter(PerplexityAdapter())
680768

681769
def _register_adapter(self, adapter: ProviderAdapter):
682770
"""Register a provider adapter."""

i2i/router.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,94 @@ class RoutingResult(BaseModel):
953953
instruction_following=79,
954954
factual_accuracy=73,
955955
),
956+
957+
# ==================== Perplexity (RAG-native) ====================
958+
# Sonar - Lightweight search model
959+
"perplexity/sonar": ModelCapability(
960+
model_id="perplexity/sonar",
961+
provider="perplexity",
962+
task_scores={
963+
TaskType.FACTUAL_QA: 90,
964+
TaskType.RESEARCH: 92,
965+
TaskType.SUMMARIZATION: 85,
966+
TaskType.CHAT: 80,
967+
},
968+
avg_latency_ms=800,
969+
cost_per_1k_tokens=0.001,
970+
context_window=127000,
971+
max_output_tokens=4096,
972+
supports_function_calling=False,
973+
supports_json_mode=False,
974+
reasoning_depth=75,
975+
creativity_score=60,
976+
instruction_following=80,
977+
factual_accuracy=92,
978+
),
979+
# Sonar Pro - Advanced search with citations
980+
"perplexity/sonar-pro": ModelCapability(
981+
model_id="perplexity/sonar-pro",
982+
provider="perplexity",
983+
task_scores={
984+
TaskType.FACTUAL_QA: 95,
985+
TaskType.RESEARCH: 97,
986+
TaskType.SUMMARIZATION: 90,
987+
TaskType.ANALYTICAL: 88,
988+
TaskType.CHAT: 85,
989+
},
990+
avg_latency_ms=1500,
991+
cost_per_1k_tokens=0.005,
992+
context_window=200000,
993+
max_output_tokens=8192,
994+
supports_function_calling=False,
995+
supports_json_mode=False,
996+
reasoning_depth=85,
997+
creativity_score=65,
998+
instruction_following=85,
999+
factual_accuracy=96,
1000+
),
1001+
# Sonar Deep Research - Exhaustive multi-step research
1002+
"perplexity/sonar-deep-research": ModelCapability(
1003+
model_id="perplexity/sonar-deep-research",
1004+
provider="perplexity",
1005+
task_scores={
1006+
TaskType.RESEARCH: 99,
1007+
TaskType.FACTUAL_QA: 97,
1008+
TaskType.ANALYTICAL: 95,
1009+
TaskType.SUMMARIZATION: 92,
1010+
},
1011+
avg_latency_ms=5000,
1012+
cost_per_1k_tokens=0.02,
1013+
context_window=200000,
1014+
max_output_tokens=16384,
1015+
supports_function_calling=False,
1016+
supports_json_mode=False,
1017+
reasoning_depth=95,
1018+
creativity_score=60,
1019+
instruction_following=88,
1020+
factual_accuracy=98,
1021+
),
1022+
# Sonar Reasoning Pro - Premier reasoning with search
1023+
"perplexity/sonar-reasoning-pro": ModelCapability(
1024+
model_id="perplexity/sonar-reasoning-pro",
1025+
provider="perplexity",
1026+
task_scores={
1027+
TaskType.LOGICAL_REASONING: 94,
1028+
TaskType.RESEARCH: 95,
1029+
TaskType.FACTUAL_QA: 93,
1030+
TaskType.ANALYTICAL: 94,
1031+
TaskType.SCIENTIFIC: 90,
1032+
},
1033+
avg_latency_ms=3000,
1034+
cost_per_1k_tokens=0.01,
1035+
context_window=200000,
1036+
max_output_tokens=16384,
1037+
supports_function_calling=False,
1038+
supports_json_mode=False,
1039+
reasoning_depth=94,
1040+
creativity_score=65,
1041+
instruction_following=88,
1042+
factual_accuracy=95,
1043+
),
9561044
}
9571045

9581046

i2i/schema.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ class Response(BaseModel):
9393
output_tokens: Optional[int] = None
9494
latency_ms: Optional[float] = None
9595

96+
# RAG/Search citations
97+
citations: Optional[List[str]] = None # Source URLs from RAG providers
98+
9699

97100
class ConsensusResult(BaseModel):
98101
"""
@@ -132,6 +135,10 @@ class VerificationResult(BaseModel):
132135
issues_found: List[str] = Field(default_factory=list)
133136
corrections: Optional[str] = None # Suggested corrections if not verified
134137

138+
# RAG/Search grounding
139+
source_citations: List[str] = Field(default_factory=list) # Source URLs used
140+
retrieved_sources: List[Dict[str, Any]] = Field(default_factory=list) # Full source info
141+
135142
metadata: Dict[str, Any] = Field(default_factory=dict)
136143

137144

0 commit comments

Comments
 (0)