Skip to content

Commit b21c10e

Browse files
author
AgentPatterns
committed
feat(examples/python): add rag-agent runnable example
1 parent 78bf050 commit b21c10e

7 files changed

Lines changed: 834 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# RAG Agent - Python Implementation
2+
3+
Runnable implementation of a RAG agent that plans retrieval, validates retrieval
4+
intent via policy, grounds answers in approved documents, and returns citations.
5+
6+
---
7+
8+
## Quick start
9+
10+
```bash
11+
# (optional) create venv
12+
python -m venv .venv && source .venv/bin/activate
13+
14+
# install dependencies
15+
pip install -r requirements.txt
16+
17+
# set API key
18+
export OPENAI_API_KEY="sk-..."
19+
20+
# run the agent
21+
python main.py
22+
```
23+
24+
## Full walkthrough
25+
26+
Read the complete implementation guide:
27+
https://agentpatterns.tech/en/agent-patterns/rag-agent
28+
29+
## What's inside
30+
31+
- Retrieval intent planning (`kind=retrieve`)
32+
- Policy boundary for retrieval shape and source allowlist
33+
- Execution boundary for runtime source access
34+
- Deterministic retriever and context packing
35+
- Grounded answer synthesis with citation checks
36+
- Fallback path when no grounded context is available
37+
- Trace and history for auditability
38+
39+
## Project layout
40+
41+
```text
42+
examples/
43+
agent-patterns/
44+
rag-agent/
45+
python/
46+
README.md
47+
main.py
48+
llm.py
49+
gateway.py
50+
retriever.py
51+
kb.py
52+
requirements.txt
53+
```
54+
55+
## Notes
56+
57+
- Code and README are English-only by design.
58+
- The website provides multilingual explanations and theory.
59+
60+
## License
61+
62+
MIT
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Any
5+
6+
from retriever import build_context_pack, retrieve_candidates
7+
8+
9+
class StopRun(Exception):
10+
def __init__(self, reason: str):
11+
super().__init__(reason)
12+
self.reason = reason
13+
14+
15+
@dataclass(frozen=True)
16+
class Budget:
17+
max_query_chars: int = 240
18+
max_top_k: int = 6
19+
max_context_chunks: int = 3
20+
max_context_chars: int = 2200
21+
min_chunk_score: float = 0.2
22+
max_seconds: int = 20
23+
24+
25+
26+
def validate_retrieval_intent(
27+
raw: Any,
28+
*,
29+
allowed_sources_policy: set[str],
30+
max_top_k: int,
31+
) -> dict[str, Any]:
32+
if not isinstance(raw, dict):
33+
raise StopRun("invalid_intent:not_object")
34+
35+
if raw.get("kind") != "retrieve":
36+
raise StopRun("invalid_intent:kind")
37+
38+
query = raw.get("query")
39+
if not isinstance(query, str) or not query.strip():
40+
raise StopRun("invalid_intent:query")
41+
42+
top_k = raw.get("top_k", 4)
43+
if not isinstance(top_k, int) or not (1 <= top_k <= max_top_k):
44+
raise StopRun("invalid_intent:top_k")
45+
46+
sources_raw = raw.get("sources")
47+
normalized_sources: list[str] = []
48+
if sources_raw is not None:
49+
if not isinstance(sources_raw, list) or not sources_raw:
50+
raise StopRun("invalid_intent:sources")
51+
for source in sources_raw:
52+
if not isinstance(source, str) or not source.strip():
53+
raise StopRun("invalid_intent:source_item")
54+
source_name = source.strip()
55+
if source_name not in allowed_sources_policy:
56+
raise StopRun(f"invalid_intent:source_not_allowed:{source_name}")
57+
normalized_sources.append(source_name)
58+
59+
# Ignore unknown keys and keep only contract fields.
60+
payload = {
61+
"kind": "retrieve",
62+
"query": query.strip(),
63+
"top_k": top_k,
64+
}
65+
if normalized_sources:
66+
payload["sources"] = normalized_sources
67+
return payload
68+
69+
70+
class RetrievalGateway:
71+
def __init__(
72+
self,
73+
*,
74+
documents: list[dict[str, Any]],
75+
budget: Budget,
76+
allow_execution_sources: set[str],
77+
):
78+
self.documents = documents
79+
self.budget = budget
80+
self.allow_execution_sources = set(allow_execution_sources)
81+
82+
def run(self, intent: dict[str, Any]) -> dict[str, Any]:
83+
query = intent["query"]
84+
if len(query) > self.budget.max_query_chars:
85+
raise StopRun("invalid_intent:query_too_long")
86+
87+
requested_sources = set(intent.get("sources") or self.allow_execution_sources)
88+
denied = sorted(requested_sources - self.allow_execution_sources)
89+
if denied:
90+
raise StopRun(f"source_denied:{denied[0]}")
91+
92+
candidates = retrieve_candidates(
93+
query=query,
94+
documents=self.documents,
95+
top_k=intent["top_k"],
96+
allowed_sources=requested_sources,
97+
)
98+
99+
context_pack = build_context_pack(
100+
candidates=candidates,
101+
min_score=self.budget.min_chunk_score,
102+
max_chunks=self.budget.max_context_chunks,
103+
max_chars=self.budget.max_context_chars,
104+
)
105+
106+
return {
107+
"query": query,
108+
"requested_sources": sorted(requested_sources),
109+
"candidates": candidates,
110+
"context_chunks": context_pack["chunks"],
111+
"context_total_chars": context_pack["total_chars"],
112+
"rejected_low_score": context_pack["rejected_low_score"],
113+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
KB_DOCUMENTS: list[dict[str, Any]] = [
6+
{
7+
"id": "doc_sla_enterprise_v3",
8+
"source": "support_policy",
9+
"title": "Support Policy",
10+
"section": "Enterprise SLA",
11+
"updated_at": "2026-01-15",
12+
"text": (
13+
"Enterprise plan includes 99.95% monthly uptime SLA. "
14+
"For P1 incidents, first response target is 15 minutes, 24/7. "
15+
"For P2 incidents, first response target is 1 hour."
16+
),
17+
},
18+
{
19+
"id": "doc_sla_standard_v2",
20+
"source": "support_policy",
21+
"title": "Support Policy",
22+
"section": "Standard SLA",
23+
"updated_at": "2025-11-10",
24+
"text": (
25+
"Standard plan includes 99.5% monthly uptime SLA. "
26+
"For P1 incidents, first response target is 1 hour during business hours."
27+
),
28+
},
29+
{
30+
"id": "doc_security_incident_v2",
31+
"source": "security_policy",
32+
"title": "Security Incident Playbook",
33+
"section": "Escalation",
34+
"updated_at": "2026-01-20",
35+
"text": (
36+
"For enterprise customers, security-related P1 incidents require immediate escalation "
37+
"to the on-call incident commander and customer success lead."
38+
),
39+
},
40+
{
41+
"id": "doc_refund_policy_v4",
42+
"source": "billing_policy",
43+
"title": "Billing and Refund Policy",
44+
"section": "Refund Eligibility",
45+
"updated_at": "2025-12-01",
46+
"text": (
47+
"Annual enterprise subscriptions may receive a prorated refund within 14 days "
48+
"under approved exception flow."
49+
),
50+
},
51+
{
52+
"id": "doc_onboarding_checklist_v1",
53+
"source": "operations_notes",
54+
"title": "Enterprise Onboarding Checklist",
55+
"section": "Launch Prep",
56+
"updated_at": "2025-09-02",
57+
"text": (
58+
"Checklist for onboarding includes SSO setup, domain verification, and success plan kickoff."
59+
),
60+
},
61+
]

0 commit comments

Comments
 (0)