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.
- pydantic_ai_rlm/__init__.py +32 -0
- pydantic_ai_rlm/agent.py +161 -0
- pydantic_ai_rlm/dependencies.py +47 -0
- pydantic_ai_rlm/logging.py +274 -0
- pydantic_ai_rlm/prompts.py +118 -0
- pydantic_ai_rlm/py.typed +0 -0
- pydantic_ai_rlm/repl.py +481 -0
- pydantic_ai_rlm/toolset.py +168 -0
- pydantic_ai_rlm/utils.py +47 -0
- pydantic_ai_rlm-0.1.0.dist-info/METADATA +344 -0
- pydantic_ai_rlm-0.1.0.dist-info/RECORD +13 -0
- pydantic_ai_rlm-0.1.0.dist-info/WHEEL +4 -0
- pydantic_ai_rlm-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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"
|
pydantic_ai_rlm/agent.py
ADDED
|
@@ -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
|
pydantic_ai_rlm/py.typed
ADDED
|
File without changes
|