pydantic-ai-rlm 0.1.1__tar.gz → 0.1.2__tar.gz

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.
Files changed (19) hide show
  1. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/PKG-INFO +33 -1
  2. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/README.md +32 -0
  3. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/examples/needle_in_haystack.py +1 -0
  4. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/examples/semantic_search.py +1 -0
  5. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/pyproject.toml +1 -1
  6. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/__init__.py +4 -0
  7. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/agent.py +138 -13
  8. pydantic_ai_rlm-0.1.2/src/pydantic_ai_rlm/models.py +23 -0
  9. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/prompts.py +61 -3
  10. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/repl.py +4 -0
  11. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/.github/workflows/publish.yml +0 -0
  12. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/.gitignore +0 -0
  13. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/.pre-commit-config.yaml +0 -0
  14. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/LICENSE +0 -0
  15. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/dependencies.py +0 -0
  16. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/logging.py +0 -0
  17. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/py.typed +0 -0
  18. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/toolset.py +0 -0
  19. {pydantic_ai_rlm-0.1.1 → pydantic_ai_rlm-0.1.2}/src/pydantic_ai_rlm/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-rlm
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Recursive Language Model (RLM) toolset for Pydantic AI - handle extremely large contexts
5
5
  Author: Pydantic AI RLM Contributors
6
6
  License-Expression: MIT
@@ -52,6 +52,8 @@ Description-Content-Type: text/markdown
52
52
   • 
53
53
  <b>Sub-Model Delegation</b>
54
54
  &nbsp;•&nbsp;
55
+ <b>Grounded Citations</b>
56
+ &nbsp;•&nbsp;
55
57
  <b>Fully Type-Safe</b>
56
58
  </p>
57
59
 
@@ -203,6 +205,30 @@ result = await agent.run(
203
205
  )
204
206
  ```
205
207
 
208
+ ### Grounded Responses with Citations
209
+
210
+ Get answers with traceable citations back to the source:
211
+
212
+ ```python
213
+ from pydantic_ai_rlm import run_rlm_analysis
214
+
215
+ # Enable grounding for citation tracking
216
+ result = await run_rlm_analysis(
217
+ context=financial_report,
218
+ query="What were the key revenue changes?",
219
+ model="openai:gpt-5",
220
+ grounded=True, # Returns GroundedResponse instead of str
221
+ )
222
+
223
+ # Response contains citation markers
224
+ print(result.info)
225
+ # "Revenue increased [1] primarily due to [2]"
226
+
227
+ # Grounding maps markers to exact quotes from the source
228
+ print(result.grounding)
229
+ # {"1": "by 45% year-over-year", "2": "expansion into Asian markets"}
230
+ ```
231
+
206
232
  ---
207
233
 
208
234
  ## API Reference
@@ -217,6 +243,7 @@ agent = create_rlm_agent(
217
243
  sub_model="openai:gpt-5-mini", # Model for llm_query() (optional)
218
244
  code_timeout=60.0, # Timeout for code execution
219
245
  custom_instructions="...", # Additional instructions
246
+ grounded=True, # Return GroundedResponse with citations
220
247
  )
221
248
  ```
222
249
 
@@ -241,6 +268,11 @@ answer = await run_rlm_analysis(context, query, model="openai:gpt-5")
241
268
 
242
269
  # Sync
243
270
  answer = run_rlm_analysis_sync(context, query, model="openai:gpt-5")
271
+
272
+ # With grounding (returns GroundedResponse)
273
+ result = await run_rlm_analysis(context, query, grounded=True)
274
+ print(result.info) # Text with [N] markers
275
+ print(result.grounding) # {"1": "exact quote", ...}
244
276
  ```
245
277
 
246
278
  ### `RLMDependencies`
@@ -24,6 +24,8 @@
24
24
  &nbsp;•&nbsp;
25
25
  <b>Sub-Model Delegation</b>
26
26
  &nbsp;•&nbsp;
27
+ <b>Grounded Citations</b>
28
+ &nbsp;•&nbsp;
27
29
  <b>Fully Type-Safe</b>
28
30
  </p>
29
31
 
@@ -175,6 +177,30 @@ result = await agent.run(
175
177
  )
176
178
  ```
177
179
 
180
+ ### Grounded Responses with Citations
181
+
182
+ Get answers with traceable citations back to the source:
183
+
184
+ ```python
185
+ from pydantic_ai_rlm import run_rlm_analysis
186
+
187
+ # Enable grounding for citation tracking
188
+ result = await run_rlm_analysis(
189
+ context=financial_report,
190
+ query="What were the key revenue changes?",
191
+ model="openai:gpt-5",
192
+ grounded=True, # Returns GroundedResponse instead of str
193
+ )
194
+
195
+ # Response contains citation markers
196
+ print(result.info)
197
+ # "Revenue increased [1] primarily due to [2]"
198
+
199
+ # Grounding maps markers to exact quotes from the source
200
+ print(result.grounding)
201
+ # {"1": "by 45% year-over-year", "2": "expansion into Asian markets"}
202
+ ```
203
+
178
204
  ---
179
205
 
180
206
  ## API Reference
@@ -189,6 +215,7 @@ agent = create_rlm_agent(
189
215
  sub_model="openai:gpt-5-mini", # Model for llm_query() (optional)
190
216
  code_timeout=60.0, # Timeout for code execution
191
217
  custom_instructions="...", # Additional instructions
218
+ grounded=True, # Return GroundedResponse with citations
192
219
  )
193
220
  ```
194
221
 
@@ -213,6 +240,11 @@ answer = await run_rlm_analysis(context, query, model="openai:gpt-5")
213
240
 
214
241
  # Sync
215
242
  answer = run_rlm_analysis_sync(context, query, model="openai:gpt-5")
243
+
244
+ # With grounding (returns GroundedResponse)
245
+ result = await run_rlm_analysis(context, query, grounded=True)
246
+ print(result.info) # Text with [N] markers
247
+ print(result.grounding) # {"1": "exact quote", ...}
216
248
  ```
217
249
 
218
250
  ### `RLMDependencies`
@@ -65,6 +65,7 @@ def main():
65
65
  query=query,
66
66
  model="openai:gpt-5",
67
67
  sub_model="openai:gpt-5-mini",
68
+ grounded=True,
68
69
  )
69
70
 
70
71
  print(f"\nResult: {result}")
@@ -142,6 +142,7 @@ def main():
142
142
  query=query,
143
143
  model="openai:gpt-5",
144
144
  sub_model="openai:gpt-5-mini",
145
+ grounded=True,
145
146
  )
146
147
 
147
148
  print(f"\nResult: {result}")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic-ai-rlm"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Recursive Language Model (RLM) toolset for Pydantic AI - handle extremely large contexts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,7 +1,9 @@
1
1
  from .agent import create_rlm_agent, run_rlm_analysis, run_rlm_analysis_sync
2
2
  from .dependencies import ContextType, RLMConfig, RLMDependencies
3
3
  from .logging import configure_logging
4
+ from .models import GroundedResponse
4
5
  from .prompts import (
6
+ GROUNDING_INSTRUCTIONS,
5
7
  LLM_QUERY_INSTRUCTIONS,
6
8
  RLM_INSTRUCTIONS,
7
9
  build_rlm_instructions,
@@ -13,9 +15,11 @@ from .toolset import (
13
15
  )
14
16
 
15
17
  __all__ = [
18
+ "GROUNDING_INSTRUCTIONS",
16
19
  "LLM_QUERY_INSTRUCTIONS",
17
20
  "RLM_INSTRUCTIONS",
18
21
  "ContextType",
22
+ "GroundedResponse",
19
23
  "REPLEnvironment",
20
24
  "REPLResult",
21
25
  "RLMConfig",
@@ -1,21 +1,45 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ from typing import Any, Literal, overload
4
4
 
5
5
  from pydantic_ai import Agent, UsageLimits
6
6
 
7
7
  from .dependencies import ContextType, RLMConfig, RLMDependencies
8
+ from .models import GroundedResponse
8
9
  from .prompts import build_rlm_instructions
9
10
  from .toolset import create_rlm_toolset
10
11
 
11
12
 
13
+ @overload
12
14
  def create_rlm_agent(
13
15
  model: str = "openai:gpt-5",
14
16
  sub_model: str | None = None,
15
17
  code_timeout: float = 60.0,
16
- include_example_instructions: bool = True,
17
18
  custom_instructions: str | None = None,
18
- ) -> Agent[RLMDependencies, str]:
19
+ *,
20
+ grounded: Literal[False] = False,
21
+ ) -> Agent[RLMDependencies, str]: ...
22
+
23
+
24
+ @overload
25
+ def create_rlm_agent(
26
+ model: str = "openai:gpt-5",
27
+ sub_model: str | None = None,
28
+ code_timeout: float = 60.0,
29
+ custom_instructions: str | None = None,
30
+ *,
31
+ grounded: Literal[True],
32
+ ) -> Agent[RLMDependencies, GroundedResponse]: ...
33
+
34
+
35
+ def create_rlm_agent(
36
+ model: str = "openai:gpt-5",
37
+ sub_model: str | None = None,
38
+ code_timeout: float = 60.0,
39
+ custom_instructions: str | None = None,
40
+ *,
41
+ grounded: bool = False,
42
+ ) -> Agent[RLMDependencies, str] | Agent[RLMDependencies, GroundedResponse]:
19
43
  """
20
44
  Create a Pydantic AI agent with REPL code execution capabilities.
21
45
 
@@ -26,11 +50,12 @@ def create_rlm_agent(
26
50
  available in the REPL, allowing the agent to delegate sub-queries.
27
51
  Example: "openai:gpt-5-mini" or "anthropic:claude-3-haiku-20240307"
28
52
  code_timeout: Timeout for code execution in seconds
29
- include_example_instructions: Include detailed examples in instructions
30
53
  custom_instructions: Additional instructions to append
54
+ grounded: If True, return a GroundedResponse with citation markers
31
55
 
32
56
  Returns:
33
- Configured Agent instance
57
+ Configured Agent instance. Returns Agent[RLMDependencies, GroundedResponse]
58
+ when grounded=True, otherwise Agent[RLMDependencies, str].
34
59
 
35
60
  Example:
36
61
  ```python
@@ -48,19 +73,28 @@ def create_rlm_agent(
48
73
  )
49
74
  result = await agent.run("What are the main themes?", deps=deps)
50
75
  print(result.output)
76
+
77
+ # Create grounded agent
78
+ grounded_agent = create_rlm_agent(model="openai:gpt-5", grounded=True)
79
+ result = await grounded_agent.run("What happened?", deps=deps)
80
+ print(result.output.info) # Response with [N] markers
81
+ print(result.output.grounding) # {"1": "exact quote", ...}
51
82
  ```
52
83
  """
53
84
  toolset = create_rlm_toolset(code_timeout=code_timeout, sub_model=sub_model)
54
85
 
55
86
  instructions = build_rlm_instructions(
56
87
  include_llm_query=sub_model is not None,
88
+ include_grounding=grounded,
57
89
  custom_suffix=custom_instructions,
58
90
  )
59
91
 
60
- agent: Agent[RLMDependencies, str] = Agent(
92
+ output_type: type[str] | type[GroundedResponse] = GroundedResponse if grounded else str
93
+
94
+ agent: Agent[RLMDependencies, Any] = Agent(
61
95
  model,
62
96
  deps_type=RLMDependencies,
63
- output_type=str,
97
+ output_type=output_type,
64
98
  toolsets=[toolset],
65
99
  instructions=instructions,
66
100
  )
@@ -68,6 +102,34 @@ def create_rlm_agent(
68
102
  return agent
69
103
 
70
104
 
105
+ @overload
106
+ async def run_rlm_analysis(
107
+ context: ContextType,
108
+ query: str,
109
+ model: str = "openai:gpt-5",
110
+ sub_model: str | None = None,
111
+ config: RLMConfig | None = None,
112
+ max_tool_calls: int = 50,
113
+ *,
114
+ grounded: Literal[False] = False,
115
+ **agent_kwargs: Any,
116
+ ) -> str: ...
117
+
118
+
119
+ @overload
120
+ async def run_rlm_analysis(
121
+ context: ContextType,
122
+ query: str,
123
+ model: str = "openai:gpt-5",
124
+ sub_model: str | None = None,
125
+ config: RLMConfig | None = None,
126
+ max_tool_calls: int = 50,
127
+ *,
128
+ grounded: Literal[True],
129
+ **agent_kwargs: Any,
130
+ ) -> GroundedResponse: ...
131
+
132
+
71
133
  async def run_rlm_analysis(
72
134
  context: ContextType,
73
135
  query: str,
@@ -75,8 +137,10 @@ async def run_rlm_analysis(
75
137
  sub_model: str | None = None,
76
138
  config: RLMConfig | None = None,
77
139
  max_tool_calls: int = 50,
140
+ *,
141
+ grounded: bool = False,
78
142
  **agent_kwargs: Any,
79
- ) -> str:
143
+ ) -> str | GroundedResponse:
80
144
  """
81
145
  Convenience function to run RLM analysis on a context.
82
146
 
@@ -89,25 +153,36 @@ async def run_rlm_analysis(
89
153
  available in the REPL, allowing the agent to delegate sub-queries.
90
154
  config: Optional RLMConfig for customization
91
155
  max_tool_calls: Maximum tool calls allowed
156
+ grounded: If True, return a GroundedResponse with citation markers
92
157
  **agent_kwargs: Additional arguments passed to create_rlm_agent()
93
158
 
94
159
  Returns:
95
- The agent's final answer as a string
160
+ The agent's final answer. Returns GroundedResponse when grounded=True,
161
+ otherwise returns str.
96
162
 
97
163
  Example:
98
164
  ```python
99
165
  from pydantic_ai_rlm import run_rlm_analysis
100
166
 
101
- # With sub-model for llm_query
167
+ # Standard string response
102
168
  answer = await run_rlm_analysis(
103
169
  context=huge_document,
104
170
  query="Find the magic number hidden in the text",
105
171
  sub_model="openai:gpt-5-mini",
106
172
  )
107
173
  print(answer)
174
+
175
+ # Grounded response with citations
176
+ result = await run_rlm_analysis(
177
+ context=document,
178
+ query="What was the revenue change?",
179
+ grounded=True,
180
+ )
181
+ print(result.info) # "Revenue grew [1]..."
182
+ print(result.grounding) # {"1": "increased by 45%", ...}
108
183
  ```
109
184
  """
110
- agent = create_rlm_agent(model=model, sub_model=sub_model, **agent_kwargs)
185
+ agent = create_rlm_agent(model=model, sub_model=sub_model, grounded=grounded, **agent_kwargs)
111
186
 
112
187
  effective_config = config or RLMConfig()
113
188
  if sub_model and not effective_config.sub_model:
@@ -127,6 +202,7 @@ async def run_rlm_analysis(
127
202
  return result.output
128
203
 
129
204
 
205
+ @overload
130
206
  def run_rlm_analysis_sync(
131
207
  context: ContextType,
132
208
  query: str,
@@ -134,14 +210,63 @@ def run_rlm_analysis_sync(
134
210
  sub_model: str | None = None,
135
211
  config: RLMConfig | None = None,
136
212
  max_tool_calls: int = 50,
213
+ *,
214
+ grounded: Literal[False] = False,
137
215
  **agent_kwargs: Any,
138
- ) -> str:
216
+ ) -> str: ...
217
+
218
+
219
+ @overload
220
+ def run_rlm_analysis_sync(
221
+ context: ContextType,
222
+ query: str,
223
+ model: str = "openai:gpt-5",
224
+ sub_model: str | None = None,
225
+ config: RLMConfig | None = None,
226
+ max_tool_calls: int = 50,
227
+ *,
228
+ grounded: Literal[True],
229
+ **agent_kwargs: Any,
230
+ ) -> GroundedResponse: ...
231
+
232
+
233
+ def run_rlm_analysis_sync(
234
+ context: ContextType,
235
+ query: str,
236
+ model: str = "openai:gpt-5",
237
+ sub_model: str | None = None,
238
+ config: RLMConfig | None = None,
239
+ max_tool_calls: int = 50,
240
+ *,
241
+ grounded: bool = False,
242
+ **agent_kwargs: Any,
243
+ ) -> str | GroundedResponse:
139
244
  """
140
245
  Synchronous version of run_rlm_analysis.
141
246
 
142
247
  See run_rlm_analysis() for full documentation.
248
+
249
+ Example:
250
+ ```python
251
+ from pydantic_ai_rlm import run_rlm_analysis_sync
252
+
253
+ # Standard string response
254
+ answer = run_rlm_analysis_sync(
255
+ context=document,
256
+ query="What happened?",
257
+ )
258
+
259
+ # Grounded response with citations
260
+ result = run_rlm_analysis_sync(
261
+ context=document,
262
+ query="What was the revenue change?",
263
+ grounded=True,
264
+ )
265
+ print(result.info) # "Revenue grew [1]..."
266
+ print(result.grounding) # {"1": "increased by 45%", ...}
267
+ ```
143
268
  """
144
- agent = create_rlm_agent(model=model, sub_model=sub_model, **agent_kwargs)
269
+ agent = create_rlm_agent(model=model, sub_model=sub_model, grounded=grounded, **agent_kwargs)
145
270
 
146
271
  effective_config = config or RLMConfig()
147
272
  if sub_model and not effective_config.sub_model:
@@ -0,0 +1,23 @@
1
+ """Pydantic models for structured RLM outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class GroundedResponse(BaseModel):
9
+ """A response with citation markers mapping to exact quotes from source documents.
10
+
11
+ Example:
12
+ ```python
13
+ response = GroundedResponse(
14
+ info="Revenue grew [1] driven by expansion [2]", grounding={"1": "increased by 45%", "2": "new markets in Asia"}
15
+ )
16
+ ```
17
+ """
18
+
19
+ info: str = Field(description="Response text with citation markers like [1]")
20
+ grounding: dict[str, str] = Field(
21
+ default_factory=dict,
22
+ description="Mapping from citation markers to exact quotes from the source",
23
+ )
@@ -50,6 +50,61 @@ print(f"Final answer: {results}")
50
50
  4. **Be thorough** - For needle-in-haystack, search the entire context
51
51
  """
52
52
 
53
+ GROUNDING_INSTRUCTIONS = """
54
+
55
+ ## Grounding Requirements
56
+
57
+ Your response MUST include grounded citations. This means:
58
+
59
+ 1. **Citation Format**: Use markers like `[1]`, `[2]`, etc. in your response text
60
+ 2. **Exact Quotes**: Each marker must map to an EXACT quote from the source context (verbatim, no paraphrasing)
61
+ 3. **Quote Length**: Each quote should be 10-200 characters - enough to be meaningful but not too long
62
+ 4. **Consecutive Numbering**: Number citations consecutively starting from 1
63
+
64
+ ### Output Format
65
+
66
+ Your final answer must be valid JSON with this structure:
67
+ ```json
68
+ {
69
+ "info": "The document states that X Y Z [1]. Additionally, A B C [2]",
70
+ "grounding": {
71
+ "1": "exact quote from source",
72
+ "2": "another exact quote from source"
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### Example
78
+
79
+ If the context contains: "The company's revenue increased by 45% in Q3 2024, driven by expansion into new markets in Asia."
80
+
81
+ Your response should look like:
82
+ ```json
83
+ {
84
+ "info": "Revenue showed strong growth [1] with geographic expansion being a key driver [2].",
85
+ "grounding": {
86
+ "1": "revenue increased by 45% in Q3 2024",
87
+ "2": "driven by expansion into new markets in Asia"
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### Finding Quotes in Code
93
+
94
+ Use this approach to find and verify exact quotes:
95
+ ```python
96
+ # Find a specific phrase in context
97
+ search_term = "revenue"
98
+ idx = context.lower().find(search_term)
99
+ if idx != -1:
100
+ # Extract surrounding context for the quote
101
+ quote = context[max(0, idx):idx+100]
102
+ print(f"Found: {quote}")
103
+ ```
104
+
105
+ **Important**: Every citation marker in your `info` field MUST have a corresponding entry in `grounding`. Only output the JSON object, no additional text.
106
+ """
107
+
53
108
  LLM_QUERY_INSTRUCTIONS = """
54
109
 
55
110
  ## Sub-LLM Queries
@@ -93,14 +148,15 @@ print(result)
93
148
 
94
149
  def build_rlm_instructions(
95
150
  include_llm_query: bool = False,
151
+ include_grounding: bool = False,
96
152
  custom_suffix: str | None = None,
97
153
  ) -> str:
98
154
  """
99
155
  Build RLM instructions with optional customization.
100
156
 
101
157
  Args:
102
- include_examples: Whether to include detailed examples
103
158
  include_llm_query: Whether to include llm_query() documentation
159
+ include_grounding: Whether to include grounding/citation instructions
104
160
  custom_suffix: Additional instructions to append
105
161
 
106
162
  Returns:
@@ -109,8 +165,10 @@ def build_rlm_instructions(
109
165
  base = RLM_INSTRUCTIONS
110
166
 
111
167
  if include_llm_query:
112
- llm_docs = LLM_QUERY_INSTRUCTIONS
113
- base = f"{base}{llm_docs}"
168
+ base = f"{base}{LLM_QUERY_INSTRUCTIONS}"
169
+
170
+ if include_grounding:
171
+ base = f"{base}{GROUNDING_INSTRUCTIONS}"
114
172
 
115
173
  if custom_suffix:
116
174
  base = f"{base}\n\n## Additional Instructions\n\n{custom_suffix}"
@@ -7,6 +7,7 @@ import os
7
7
  import shutil
8
8
  import sys
9
9
  import tempfile
10
+ import textwrap
10
11
  import threading
11
12
  import time
12
13
  from contextlib import contextmanager
@@ -328,6 +329,9 @@ with open(r'{context_path}', 'r', encoding='utf-8') as f:
328
329
  Returns:
329
330
  REPLResult with stdout, stderr, locals, and timing
330
331
  """
332
+ # Normalize code: remove common leading whitespace and strip
333
+ code = textwrap.dedent(code).strip()
334
+
331
335
  start_time = time.time()
332
336
  success = True
333
337
  stdout_content = ""
File without changes