pydantic-ai-rlm 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ from .agent import create_rlm_agent, run_rlm_analysis, run_rlm_analysis_sync
2
+ from .dependencies import ContextType, RLMConfig, RLMDependencies
3
+ from .logging import configure_logging
4
+ from .prompts import (
5
+ LLM_QUERY_INSTRUCTIONS,
6
+ RLM_INSTRUCTIONS,
7
+ build_rlm_instructions,
8
+ )
9
+ from .repl import REPLEnvironment, REPLResult
10
+ from .toolset import (
11
+ cleanup_repl_environments,
12
+ create_rlm_toolset,
13
+ )
14
+
15
+ __all__ = [
16
+ "LLM_QUERY_INSTRUCTIONS",
17
+ "RLM_INSTRUCTIONS",
18
+ "ContextType",
19
+ "REPLEnvironment",
20
+ "REPLResult",
21
+ "RLMConfig",
22
+ "RLMDependencies",
23
+ "build_rlm_instructions",
24
+ "cleanup_repl_environments",
25
+ "configure_logging",
26
+ "create_rlm_agent",
27
+ "create_rlm_toolset",
28
+ "run_rlm_analysis",
29
+ "run_rlm_analysis_sync",
30
+ ]
31
+
32
+ __version__ = "0.1.0"
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic_ai import Agent, UsageLimits
6
+
7
+ from .dependencies import ContextType, RLMConfig, RLMDependencies
8
+ from .prompts import build_rlm_instructions
9
+ from .toolset import create_rlm_toolset
10
+
11
+
12
+ def create_rlm_agent(
13
+ model: str = "openai:gpt-5",
14
+ sub_model: str | None = None,
15
+ code_timeout: float = 60.0,
16
+ include_example_instructions: bool = True,
17
+ custom_instructions: str | None = None,
18
+ ) -> Agent[RLMDependencies, str]:
19
+ """
20
+ Create a Pydantic AI agent with REPL code execution capabilities.
21
+
22
+ Args:
23
+ model: Model to use for the main agent
24
+ sub_model: Model to use for llm_query() within the REPL environment.
25
+ If provided, a `llm_query(prompt: str) -> str` function becomes
26
+ available in the REPL, allowing the agent to delegate sub-queries.
27
+ Example: "openai:gpt-5-mini" or "anthropic:claude-3-haiku-20240307"
28
+ code_timeout: Timeout for code execution in seconds
29
+ include_example_instructions: Include detailed examples in instructions
30
+ custom_instructions: Additional instructions to append
31
+
32
+ Returns:
33
+ Configured Agent instance
34
+
35
+ Example:
36
+ ```python
37
+ from pydantic_ai_rlm import create_rlm_agent, RLMDependencies, RLMConfig
38
+
39
+ # Create agent with sub-model for llm_query
40
+ agent = create_rlm_agent(
41
+ model="openai:gpt-5",
42
+ sub_model="openai:gpt-5-mini",
43
+ )
44
+
45
+ deps = RLMDependencies(
46
+ context=very_large_document,
47
+ config=RLMConfig(sub_model="openai:gpt-5-mini"),
48
+ )
49
+ result = await agent.run("What are the main themes?", deps=deps)
50
+ print(result.output)
51
+ ```
52
+ """
53
+ toolset = create_rlm_toolset(code_timeout=code_timeout, sub_model=sub_model)
54
+
55
+ instructions = build_rlm_instructions(
56
+ include_llm_query=sub_model is not None,
57
+ custom_suffix=custom_instructions,
58
+ )
59
+
60
+ agent: Agent[RLMDependencies, str] = Agent(
61
+ model,
62
+ deps_type=RLMDependencies,
63
+ output_type=str,
64
+ toolsets=[toolset],
65
+ instructions=instructions,
66
+ )
67
+
68
+ return agent
69
+
70
+
71
+ async def run_rlm_analysis(
72
+ context: ContextType,
73
+ query: str,
74
+ model: str = "openai:gpt-5",
75
+ sub_model: str | None = None,
76
+ config: RLMConfig | None = None,
77
+ max_tool_calls: int = 50,
78
+ **agent_kwargs: Any,
79
+ ) -> str:
80
+ """
81
+ Convenience function to run RLM analysis on a context.
82
+
83
+ Args:
84
+ context: The large context to analyze (string, dict, or list)
85
+ query: The question to answer about the context
86
+ model: Model to use for the main agent
87
+ sub_model: Model to use for llm_query() within the REPL environment.
88
+ If provided, a `llm_query(prompt: str) -> str` function becomes
89
+ available in the REPL, allowing the agent to delegate sub-queries.
90
+ config: Optional RLMConfig for customization
91
+ max_tool_calls: Maximum tool calls allowed
92
+ **agent_kwargs: Additional arguments passed to create_rlm_agent()
93
+
94
+ Returns:
95
+ The agent's final answer as a string
96
+
97
+ Example:
98
+ ```python
99
+ from pydantic_ai_rlm import run_rlm_analysis
100
+
101
+ # With sub-model for llm_query
102
+ answer = await run_rlm_analysis(
103
+ context=huge_document,
104
+ query="Find the magic number hidden in the text",
105
+ sub_model="openai:gpt-5-mini",
106
+ )
107
+ print(answer)
108
+ ```
109
+ """
110
+ agent = create_rlm_agent(model=model, sub_model=sub_model, **agent_kwargs)
111
+
112
+ effective_config = config or RLMConfig()
113
+ if sub_model and not effective_config.sub_model:
114
+ effective_config.sub_model = sub_model
115
+
116
+ deps = RLMDependencies(
117
+ context=context,
118
+ config=effective_config,
119
+ )
120
+
121
+ result = await agent.run(
122
+ query,
123
+ deps=deps,
124
+ usage_limits=UsageLimits(tool_calls_limit=max_tool_calls),
125
+ )
126
+
127
+ return result.output
128
+
129
+
130
+ def run_rlm_analysis_sync(
131
+ context: ContextType,
132
+ query: str,
133
+ model: str = "openai:gpt-5",
134
+ sub_model: str | None = None,
135
+ config: RLMConfig | None = None,
136
+ max_tool_calls: int = 50,
137
+ **agent_kwargs: Any,
138
+ ) -> str:
139
+ """
140
+ Synchronous version of run_rlm_analysis.
141
+
142
+ See run_rlm_analysis() for full documentation.
143
+ """
144
+ agent = create_rlm_agent(model=model, sub_model=sub_model, **agent_kwargs)
145
+
146
+ effective_config = config or RLMConfig()
147
+ if sub_model and not effective_config.sub_model:
148
+ effective_config.sub_model = sub_model
149
+
150
+ deps = RLMDependencies(
151
+ context=context,
152
+ config=effective_config,
153
+ )
154
+
155
+ result = agent.run_sync(
156
+ query,
157
+ deps=deps,
158
+ usage_limits=UsageLimits(tool_calls_limit=max_tool_calls),
159
+ )
160
+
161
+ return result.output
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ ContextType = str | dict[str, Any] | list[Any]
7
+
8
+
9
+ @dataclass
10
+ class RLMConfig:
11
+ """Configuration for RLM behavior."""
12
+
13
+ code_timeout: float = 60.0
14
+ """Timeout in seconds for code execution."""
15
+
16
+ truncate_output_chars: int = 50_000
17
+ """Maximum characters to return from code execution output."""
18
+
19
+ sub_model: str | None = None
20
+ """
21
+ Model to use for llm_query() within the REPL environment.
22
+
23
+ If set, a `llm_query(prompt: str) -> str` function becomes available
24
+ in the REPL environment, allowing the main LLM to delegate sub-queries
25
+ to another model. This is useful for processing large contexts in chunks.
26
+ """
27
+
28
+
29
+ @dataclass
30
+ class RLMDependencies:
31
+ """
32
+ Dependencies injected into RLM tools via RunContext.
33
+
34
+ This holds the context data and configuration that
35
+ the RLM toolset needs to operate.
36
+ """
37
+
38
+ context: ContextType
39
+ """The context to analyze (string, dict, or list)."""
40
+
41
+ config: RLMConfig = field(default_factory=RLMConfig)
42
+ """RLM configuration options."""
43
+
44
+ def __post_init__(self):
45
+ """Validate dependencies after initialization."""
46
+ if self.context is None:
47
+ raise ValueError("context cannot be None")
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .repl import REPLResult
7
+
8
+ # Check if rich is available
9
+ try:
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.syntax import Syntax
13
+ from rich.text import Text
14
+
15
+ RICH_AVAILABLE = True
16
+ except ImportError:
17
+ RICH_AVAILABLE = False
18
+
19
+
20
+ class RLMLogger:
21
+ """
22
+ Pretty logger for RLM code execution.
23
+
24
+ Uses rich for fancy terminal output with syntax highlighting and styled panels.
25
+ Falls back to plain text if rich is not installed.
26
+ """
27
+
28
+ def __init__(self, enabled: bool = True):
29
+ self.enabled = enabled
30
+ if RICH_AVAILABLE:
31
+ self.console = Console()
32
+ else:
33
+ self.console = None
34
+
35
+ def log_code_execution(self, code: str) -> None:
36
+ """Log the code being executed."""
37
+ if not self.enabled:
38
+ return
39
+
40
+ if RICH_AVAILABLE and self.console:
41
+ syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
42
+ panel = Panel(
43
+ syntax,
44
+ title="[bold cyan]Code Execution[/bold cyan]",
45
+ border_style="cyan",
46
+ padding=(0, 1),
47
+ )
48
+ self.console.print(panel)
49
+ else:
50
+ print(f"\n{'='*50}")
51
+ print("CODE EXECUTION")
52
+ print("=" * 50)
53
+ print(code)
54
+ print("=" * 50)
55
+
56
+ def log_result(self, result: REPLResult) -> None:
57
+ """Log the execution result."""
58
+ if not self.enabled:
59
+ return
60
+
61
+ if RICH_AVAILABLE and self.console:
62
+ self._log_result_rich(result)
63
+ else:
64
+ self._log_result_plain(result)
65
+
66
+ def _log_result_rich(self, result: REPLResult) -> None:
67
+ """Log result using rich formatting."""
68
+ status, border_style = self._get_status_style(result.success)
69
+ content_parts = self._build_content_parts(result)
70
+ user_vars = self._get_user_vars(result.locals)
71
+
72
+ self._print_result_panel(content_parts, status, border_style, user_vars)
73
+
74
+ def _get_status_style(self, success: bool) -> tuple:
75
+ """Get status text and border style based on success."""
76
+ if success:
77
+ return Text("SUCCESS", style="bold green"), "green"
78
+ return Text("ERROR", style="bold red"), "red"
79
+
80
+ def _build_content_parts(self, result: REPLResult) -> list:
81
+ """Build content parts for the result panel."""
82
+ parts = [Text(f"Executed in {result.execution_time:.3f}s", style="dim")]
83
+
84
+ if result.stdout.strip():
85
+ stdout = result.stdout.strip()
86
+ if len(stdout) > 2000:
87
+ stdout = stdout[:2000] + "\n... (truncated)"
88
+ parts.extend([Text("\n"), Text("Output:", style="bold yellow"), Text("\n"), Text(stdout, style="white")])
89
+
90
+ if result.stderr.strip():
91
+ stderr = result.stderr.strip()
92
+ if len(stderr) > 1000:
93
+ stderr = stderr[:1000] + "\n... (truncated)"
94
+ parts.extend([Text("\n"), Text("Errors:", style="bold red"), Text("\n"), Text(stderr, style="red")])
95
+
96
+ return parts
97
+
98
+ def _get_user_vars(self, locals_dict: dict) -> dict:
99
+ """Extract user-defined variables from locals."""
100
+ excluded = ("context", "json", "re", "os", "collections", "math")
101
+ return {k: v for k, v in locals_dict.items() if not k.startswith("_") and k not in excluded}
102
+
103
+ def _print_result_panel(self, content_parts: list, status, border_style: str, user_vars: dict) -> None:
104
+ """Print the result panel and optional variables table."""
105
+ from rich.table import Table
106
+
107
+ if user_vars:
108
+ content_parts.extend([Text("\n"), Text("Variables:", style="bold magenta"), Text("\n")])
109
+ if len(user_vars) > 10:
110
+ content_parts.append(Text(f" ... and {len(user_vars) - 10} more variables\n", style="dim"))
111
+
112
+ combined = Text()
113
+ for part in content_parts:
114
+ combined.append(part)
115
+
116
+ panel = Panel(combined, title=f"[bold]Result: {status}[/bold]", border_style=border_style, padding=(0, 1))
117
+ self.console.print(panel)
118
+
119
+ if user_vars:
120
+ var_table = Table(show_header=True, header_style="bold", box=None, padding=(0, 1))
121
+ var_table.add_column("Name", style="cyan")
122
+ var_table.add_column("Type", style="yellow")
123
+ var_table.add_column("Value", style="white", max_width=60)
124
+
125
+ for name, value in list(user_vars.items())[:10]:
126
+ value_str = self._format_var_value(value)
127
+ var_table.add_row(name, type(value).__name__, value_str)
128
+
129
+ self.console.print(var_table)
130
+
131
+ def _format_var_value(self, value) -> str:
132
+ """Format a variable value for display."""
133
+ try:
134
+ value_str = repr(value)
135
+ if len(value_str) > 60:
136
+ return value_str[:57] + "..."
137
+ return value_str
138
+ except Exception:
139
+ return "<unable to repr>"
140
+
141
+ def _log_result_plain(self, result: REPLResult) -> None:
142
+ """Log result using plain text."""
143
+ status = "SUCCESS" if result.success else "ERROR"
144
+ print(f"\n{'='*50}")
145
+ print(f"RESULT: {status} (executed in {result.execution_time:.3f}s)")
146
+ print("=" * 50)
147
+
148
+ if result.stdout.strip():
149
+ print("\nOutput:")
150
+ stdout = result.stdout.strip()
151
+ if len(stdout) > 2000:
152
+ stdout = stdout[:2000] + "\n... (truncated)"
153
+ print(stdout)
154
+
155
+ if result.stderr.strip():
156
+ print("\nErrors:")
157
+ stderr = result.stderr.strip()
158
+ if len(stderr) > 1000:
159
+ stderr = stderr[:1000] + "\n... (truncated)"
160
+ print(stderr)
161
+
162
+ user_vars = {
163
+ k: v
164
+ for k, v in result.locals.items()
165
+ if not k.startswith("_") and k not in ("context", "json", "re", "os")
166
+ }
167
+ if user_vars:
168
+ print("\nVariables:")
169
+ for name, value in list(user_vars.items())[:10]:
170
+ try:
171
+ value_str = repr(value)
172
+ if len(value_str) > 60:
173
+ value_str = value_str[:57] + "..."
174
+ except Exception:
175
+ value_str = "<unable to repr>"
176
+ print(f" {name} ({type(value).__name__}): {value_str}")
177
+ if len(user_vars) > 10:
178
+ print(f" ... and {len(user_vars) - 10} more variables")
179
+
180
+ print("=" * 50)
181
+
182
+ def log_llm_query(self, prompt: str) -> None:
183
+ """Log an llm_query call."""
184
+ if not self.enabled:
185
+ return
186
+
187
+ if RICH_AVAILABLE and self.console:
188
+ # Truncate long prompts
189
+ display_prompt = prompt
190
+ if len(display_prompt) > 500:
191
+ display_prompt = display_prompt[:500] + "..."
192
+
193
+ panel = Panel(
194
+ Text(display_prompt, style="white"),
195
+ title="[bold blue]LLM Query[/bold blue]",
196
+ border_style="blue",
197
+ padding=(0, 1),
198
+ )
199
+ self.console.print(panel)
200
+ else:
201
+ print(f"\n{'='*50}")
202
+ print("LLM QUERY")
203
+ print("=" * 50)
204
+ display_prompt = prompt
205
+ if len(display_prompt) > 500:
206
+ display_prompt = display_prompt[:500] + "..."
207
+ print(display_prompt)
208
+ print("=" * 50)
209
+
210
+ def log_llm_response(self, response: str) -> None:
211
+ """Log an llm_query response."""
212
+ if not self.enabled:
213
+ return
214
+
215
+ if RICH_AVAILABLE and self.console:
216
+ # Truncate long responses
217
+ display_response = response
218
+ if len(display_response) > 500:
219
+ display_response = display_response[:500] + "..."
220
+
221
+ panel = Panel(
222
+ Text(display_response, style="white"),
223
+ title="[bold blue]LLM Response[/bold blue]",
224
+ border_style="blue",
225
+ padding=(0, 1),
226
+ )
227
+ self.console.print(panel)
228
+ else:
229
+ print(f"\n{'='*50}")
230
+ print("LLM RESPONSE")
231
+ print("=" * 50)
232
+ display_response = response
233
+ if len(display_response) > 500:
234
+ display_response = display_response[:500] + "..."
235
+ print(display_response)
236
+ print("=" * 50)
237
+
238
+
239
+ # Global logger instance
240
+ _logger: RLMLogger | None = None
241
+
242
+
243
+ def get_logger() -> RLMLogger:
244
+ """Get the global RLM logger instance."""
245
+ global _logger
246
+ if _logger is None:
247
+ _logger = RLMLogger(enabled=False) # Disabled by default
248
+ return _logger
249
+
250
+
251
+ def configure_logging(enabled: bool = True) -> RLMLogger:
252
+ """
253
+ Configure RLM logging.
254
+
255
+ Args:
256
+ enabled: Whether to enable logging output
257
+
258
+ Returns:
259
+ The configured logger instance
260
+
261
+ Example:
262
+ ```python
263
+ from pydantic_ai_rlm import configure_logging
264
+
265
+ # Enable fancy logging
266
+ configure_logging(enabled=True)
267
+
268
+ # Run your analysis - you'll see code and output in the terminal
269
+ result = await run_rlm_analysis(context, query)
270
+ ```
271
+ """
272
+ global _logger
273
+ _logger = RLMLogger(enabled=enabled)
274
+ return _logger
@@ -0,0 +1,118 @@
1
+ RLM_INSTRUCTIONS = """You are an AI assistant that analyzes data using Python code execution. You have access to a REPL environment where code persists between executions.
2
+
3
+ ## REPL Environment
4
+
5
+ The REPL environment provides:
6
+ 1. A `context` variable containing your data (string, dict, or list)
7
+ 2. Common modules available via import: `re`, `json`, `collections`, etc.
8
+ 3. Variables persist between code executions
9
+
10
+ ## Strategy for Large Contexts
11
+
12
+ ### Step 1: Explore the Context Structure
13
+ ```python
14
+ print(f"Context type: {type(context)}")
15
+ print(f"Context length: {len(context)}")
16
+ if isinstance(context, str):
17
+ print(f"First 500 chars: {context[:500]}")
18
+ ```
19
+
20
+ ### Step 2: Process the Data
21
+ For structured data:
22
+ ```python
23
+ import re
24
+ sections = re.split(r'### (.+)', context)
25
+ for i in range(1, len(sections), 2):
26
+ header = sections[i]
27
+ content = sections[i+1][:200]
28
+ print(f"{header}: {content}...")
29
+ ```
30
+
31
+ For raw text - search patterns:
32
+ ```python
33
+ import re
34
+ matches = re.findall(r'\\d{4}-\\d{2}-\\d{2}', context)
35
+ print(f"Found {len(matches)} dates: {matches[:10]}")
36
+ ```
37
+
38
+ ### Step 3: Build Your Answer
39
+ ```python
40
+ results = []
41
+ # ... process data ...
42
+ print(f"Final answer: {results}")
43
+ ```
44
+
45
+ ## Guidelines
46
+
47
+ 1. **Always explore first** - Check context type and size before processing
48
+ 2. **Use print() liberally** - See intermediate results
49
+ 3. **Store results in variables** - Build up your answer incrementally
50
+ 4. **Be thorough** - For needle-in-haystack, search the entire context
51
+ """
52
+
53
+ LLM_QUERY_INSTRUCTIONS = """
54
+
55
+ ## Sub-LLM Queries
56
+
57
+ You also have access to `llm_query(prompt: str) -> str` function that allows you to query another LLM from within your REPL code. This is extremely useful for:
58
+ - **Semantic analysis** - Understanding meaning, not just text patterns
59
+ - **Summarization** - Condensing large sections of context
60
+ - **Chunked processing** - Analyzing context in manageable pieces
61
+ - **Complex reasoning** - Delegating sub-tasks that require language understanding
62
+
63
+ ### Example: Chunked Analysis
64
+ ```python
65
+ # Split context into chunks and analyze each with llm_query
66
+ chunk_size = 50000
67
+ chunks = [context[i:i+chunk_size] for i in range(0, len(context), chunk_size)]
68
+
69
+ summaries = []
70
+ for i, chunk in enumerate(chunks):
71
+ summary = llm_query(f"Summarize this section:\\n{chunk}")
72
+ summaries.append(f"Chunk {i+1}: {summary}")
73
+ print(f"Processed chunk {i+1}/{len(chunks)}")
74
+
75
+ # Combine summaries for final answer
76
+ final = llm_query(f"Based on these summaries, answer: What are the main themes?\\n" + "\\n".join(summaries))
77
+ print(final)
78
+ ```
79
+
80
+ ### Example: Semantic Search
81
+ ```python
82
+ # Use llm_query for semantic understanding
83
+ result = llm_query(f"Find any mentions of 'magic number' in this text and return the value:\\n{context[:100000]}")
84
+ print(result)
85
+ ```
86
+
87
+ **Tips:**
88
+ - The sub-LLM can handle ~500K characters per query
89
+ - Use it for semantic analysis that regex/string operations can't do
90
+ - Store sub-LLM results in variables to build up your answer
91
+ """
92
+
93
+
94
+ def build_rlm_instructions(
95
+ include_llm_query: bool = False,
96
+ custom_suffix: str | None = None,
97
+ ) -> str:
98
+ """
99
+ Build RLM instructions with optional customization.
100
+
101
+ Args:
102
+ include_examples: Whether to include detailed examples
103
+ include_llm_query: Whether to include llm_query() documentation
104
+ custom_suffix: Additional instructions to append
105
+
106
+ Returns:
107
+ Complete instructions string
108
+ """
109
+ base = RLM_INSTRUCTIONS
110
+
111
+ if include_llm_query:
112
+ llm_docs = LLM_QUERY_INSTRUCTIONS
113
+ base = f"{base}{llm_docs}"
114
+
115
+ if custom_suffix:
116
+ base = f"{base}\n\n## Additional Instructions\n\n{custom_suffix}"
117
+
118
+ return base
File without changes