lm-deluge 0.0.87__py3-none-any.whl → 0.0.89__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.
@@ -52,6 +52,7 @@ async def _build_gemini_request(
52
52
 
53
53
  # Handle reasoning models (thinking)
54
54
  is_gemini_3 = "gemini-3" in model.name.lower()
55
+ is_gemini_3_flash = "gemini-3-flash" in model.name.lower()
55
56
  if is_gemini_3:
56
57
  # gemini3 MUST think
57
58
  if not sampling_params.reasoning_effort:
@@ -62,13 +63,24 @@ async def _build_gemini_request(
62
63
  if effort_key == "xhigh":
63
64
  maybe_warn("WARN_XHIGH_TO_HIGH", model_name=model.name)
64
65
  effort_key = "high"
65
- level_map = {
66
- "none": "low",
67
- "minimal": "low",
68
- "low": "low",
69
- "medium": "high", # change when supported
70
- "high": "high",
71
- }
66
+ if is_gemini_3_flash:
67
+ # Flash supports minimal, low, medium, high
68
+ level_map = {
69
+ "none": "low",
70
+ "minimal": "minimal",
71
+ "low": "low",
72
+ "medium": "medium",
73
+ "high": "high",
74
+ }
75
+ else:
76
+ # Pro only supports low, high
77
+ level_map = {
78
+ "none": "low",
79
+ "minimal": "low",
80
+ "low": "low",
81
+ "medium": "high",
82
+ "high": "high",
83
+ }
72
84
  effort = level_map[effort_key]
73
85
  thinking_config = {"thinkingLevel": effort}
74
86
  request_json["generationConfig"]["thinkingConfig"] = thinking_config
@@ -153,6 +153,19 @@ GOOGLE_MODELS = {
153
153
  # Note: >200k tokens pricing is $4/$18 per million
154
154
  "reasoning_model": True,
155
155
  },
156
+ "gemini-3-flash-preview": {
157
+ "id": "gemini-3-flash-preview",
158
+ "name": "gemini-3-flash-preview",
159
+ "api_base": "https://generativelanguage.googleapis.com/v1alpha",
160
+ "api_key_env_var": "GEMINI_API_KEY",
161
+ "supports_json": True,
162
+ "supports_logprobs": False,
163
+ "api_spec": "gemini",
164
+ "input_cost": 0.5,
165
+ "cached_input_cost": 0.125, # estimated
166
+ "output_cost": 3.0,
167
+ "reasoning_model": True,
168
+ },
156
169
  # Gemini 2.5 Computer Use model
157
170
  "gemini-2.5-computer-use": {
158
171
  "id": "gemini-2.5-computer-use",
@@ -7,18 +7,23 @@ from .filesystem import (
7
7
  from .batch_tool import BatchTool
8
8
  from .tool_search import ToolSearchTool
9
9
  from .otc import ToolComposer
10
- from .sandbox import DaytonaSandbox, ModalSandbox
10
+ from .rlm import RLMManager, RLMPipeline, RLMResult
11
+ from .sandbox import DaytonaSandbox, DockerSandbox, FargateSandbox, ModalSandbox
11
12
  from .docs import DocsManager
12
13
  from .sheets import SheetsManager
13
14
  from .random import RandomTools
14
15
  from .subagents import SubAgentManager
15
16
  from .todos import TodoItem, TodoManager, TodoPriority, TodoStatus
16
17
  from .email import EmailManager
18
+ from .full_text_search import FullTextSearchManager
17
19
 
18
20
  __all__ = [
19
21
  "BatchTool",
20
22
  "ToolSearchTool",
21
23
  "ToolComposer",
24
+ "RLMManager",
25
+ "RLMPipeline",
26
+ "RLMResult",
22
27
  "TodoItem",
23
28
  "TodoManager",
24
29
  "TodoPriority",
@@ -30,8 +35,11 @@ __all__ = [
30
35
  "WorkspaceBackend",
31
36
  "ModalSandbox",
32
37
  "DaytonaSandbox",
38
+ "DockerSandbox",
39
+ "FargateSandbox",
33
40
  "DocsManager",
34
41
  "SheetsManager",
35
42
  "RandomTools",
36
43
  "EmailManager",
44
+ "FullTextSearchManager",
37
45
  ]
@@ -0,0 +1,285 @@
1
+ """Full text search prefab tool using Tantivy."""
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import Annotated, Any
7
+
8
+ from lm_deluge.tool import Tool
9
+
10
+ from .tantivy_index import SearchResult, TantivySearch
11
+
12
+
13
+ class FullTextSearchManager:
14
+ """
15
+ Full-text search tools using Tantivy.
16
+
17
+ Provides two tools:
18
+ - search: Search the corpus and get document IDs + previews
19
+ - fetch: Get the full contents of specific documents by ID
20
+
21
+ Args:
22
+ corpus: List of document dicts to index. Each dict must have an "id" field.
23
+ search_fields: List of field names to search. If None, searches all fields.
24
+ preview_fields: Fields to include in search result previews.
25
+ index_path: Path to store the Tantivy index. If None, uses a temp directory.
26
+ search_tool_name: Name for the search tool (default: "search")
27
+ fetch_tool_name: Name for the fetch tool (default: "fetch")
28
+ max_results: Maximum number of search results to return (default: 10)
29
+ include_fields: Fields to include in the index (searchable). If None, includes all.
30
+ exclude_fields: Fields to exclude from the index (not searchable).
31
+
32
+ Example:
33
+ ```python
34
+ corpus = [
35
+ {"id": "1", "title": "Hello World", "content": "This is a test document."},
36
+ {"id": "2", "title": "Another Doc", "content": "More content here."},
37
+ ]
38
+ manager = FullTextSearchManager(
39
+ corpus=corpus,
40
+ search_fields=["title", "content"],
41
+ preview_fields=["title"],
42
+ )
43
+ tools = manager.get_tools()
44
+ ```
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ corpus: list[dict[str, Any]],
50
+ *,
51
+ search_fields: list[str] | None = None,
52
+ preview_fields: list[str] | None = None,
53
+ index_path: str | Path | None = None,
54
+ search_tool_name: str = "search",
55
+ fetch_tool_name: str = "fetch",
56
+ max_results: int = 10,
57
+ include_fields: list[str] | None = None,
58
+ exclude_fields: list[str] | None = None,
59
+ deduplicate_by: str | None = None,
60
+ ):
61
+ # Initialize _temp_dir early to avoid __del__ issues
62
+ self._temp_dir: str | None = None
63
+
64
+ self.corpus = corpus
65
+ self.search_fields = search_fields
66
+ self.preview_fields = preview_fields
67
+ self.search_tool_name = search_tool_name
68
+ self.fetch_tool_name = fetch_tool_name
69
+ self.max_results = max_results
70
+ self._tools: list[Tool] | None = None
71
+
72
+ # Validate corpus
73
+ if not corpus:
74
+ raise ValueError("Corpus cannot be empty")
75
+
76
+ # Ensure all documents have an id field
77
+ for i, doc in enumerate(corpus):
78
+ if "id" not in doc:
79
+ raise ValueError(f"Document at index {i} is missing 'id' field")
80
+
81
+ # Set up index path
82
+ if index_path is None:
83
+ self._temp_dir = tempfile.mkdtemp(prefix="tantivy_")
84
+ self._index_path = Path(self._temp_dir)
85
+ else:
86
+ self._temp_dir = None
87
+ self._index_path = Path(index_path)
88
+
89
+ # Determine search fields from corpus if not provided
90
+ if search_fields is None:
91
+ # Use all string fields except 'id'
92
+ sample = corpus[0]
93
+ self.search_fields = [
94
+ k for k, v in sample.items() if k != "id" and isinstance(v, str)
95
+ ]
96
+ else:
97
+ self.search_fields = search_fields
98
+
99
+ # Initialize Tantivy index
100
+ self._index = TantivySearch(
101
+ index_path=str(self._index_path),
102
+ include_fields=include_fields,
103
+ exclude_fields=exclude_fields,
104
+ )
105
+
106
+ # Build the index
107
+ self._index.build_index(
108
+ records=corpus,
109
+ deduplicate_by=deduplicate_by,
110
+ )
111
+
112
+ # Cache documents for efficient fetch
113
+ self._index.cache_documents_for_fetch(corpus, id_column="id")
114
+
115
+ def _format_preview(self, result: SearchResult) -> dict[str, Any]:
116
+ """Format a search result for preview."""
117
+ preview: dict[str, Any] = {
118
+ "id": result.id,
119
+ "score": round(result.score, 4),
120
+ }
121
+
122
+ # Add preview fields
123
+ if self.preview_fields:
124
+ for field in self.preview_fields:
125
+ if field in result.content:
126
+ value = result.content[field]
127
+ # Truncate long values
128
+ if isinstance(value, str) and len(value) > 200:
129
+ value = value[:200] + "..."
130
+ preview[field] = value
131
+ else:
132
+ # Include all fields with truncation
133
+ for field, value in result.content.items():
134
+ if field == "id":
135
+ continue
136
+ if isinstance(value, str) and len(value) > 200:
137
+ value = value[:200] + "..."
138
+ preview[field] = value
139
+
140
+ return preview
141
+
142
+ def _search(
143
+ self,
144
+ query: Annotated[str, "Search query to find relevant documents"],
145
+ limit: Annotated[int, "Maximum number of results to return"] = 10,
146
+ ) -> str:
147
+ """
148
+ Search the corpus for documents matching the query.
149
+
150
+ Returns a list of document previews with IDs and scores.
151
+ Use the fetch tool to get full document contents.
152
+ """
153
+ try:
154
+ # Use the search fields
155
+ assert self.search_fields is not None
156
+ results = self._index.search(
157
+ queries=[query],
158
+ fields=self.search_fields,
159
+ limit=min(limit, self.max_results),
160
+ escape=True,
161
+ )
162
+
163
+ previews = [self._format_preview(r) for r in results]
164
+
165
+ return json.dumps(
166
+ {
167
+ "status": "success",
168
+ "query": query,
169
+ "num_results": len(previews),
170
+ "results": previews,
171
+ },
172
+ indent=2,
173
+ )
174
+
175
+ except Exception as e:
176
+ return json.dumps({"status": "error", "error": str(e)})
177
+
178
+ def _fetch(
179
+ self,
180
+ document_ids: Annotated[
181
+ list[str], "List of document IDs to fetch full contents for"
182
+ ],
183
+ ) -> str:
184
+ """
185
+ Fetch the full contents of documents by their IDs.
186
+
187
+ Use search first to find relevant document IDs.
188
+ """
189
+ try:
190
+ documents, found_ids, missing_ids = self._index.fetch(document_ids)
191
+
192
+ result: dict[str, Any] = {
193
+ "status": "success",
194
+ "found": len(found_ids),
195
+ "missing": len(missing_ids),
196
+ "documents": documents,
197
+ }
198
+
199
+ if missing_ids:
200
+ result["missing_ids"] = missing_ids
201
+
202
+ return json.dumps(result, indent=2)
203
+
204
+ except Exception as e:
205
+ return json.dumps({"status": "error", "error": str(e)})
206
+
207
+ def get_tools(self) -> list[Tool]:
208
+ """Return the search and fetch tools."""
209
+ if self._tools is not None:
210
+ return self._tools
211
+
212
+ search_tool = Tool.from_function(self._search, name=self.search_tool_name)
213
+ fetch_tool = Tool.from_function(self._fetch, name=self.fetch_tool_name)
214
+
215
+ # Update descriptions for clarity
216
+ search_tool = search_tool.model_copy(
217
+ update={
218
+ "description": (
219
+ "Search the document corpus for relevant results. "
220
+ "Returns document IDs, relevance scores, and previews. "
221
+ "Use the fetch tool to get full document contents."
222
+ )
223
+ }
224
+ )
225
+
226
+ fetch_tool = fetch_tool.model_copy(
227
+ update={
228
+ "description": (
229
+ "Fetch the full contents of documents by their IDs. "
230
+ "Use after searching to get complete document text."
231
+ )
232
+ }
233
+ )
234
+
235
+ self._tools = [search_tool, fetch_tool]
236
+ return self._tools
237
+
238
+ def search(
239
+ self, query: str, limit: int = 10, fields: list[str] | None = None
240
+ ) -> list[SearchResult]:
241
+ """
242
+ Direct search method for programmatic use.
243
+
244
+ Args:
245
+ query: Search query string
246
+ limit: Maximum number of results
247
+ fields: Fields to search (defaults to self.search_fields)
248
+
249
+ Returns:
250
+ List of SearchResult objects
251
+ """
252
+ search_fields = fields or self.search_fields
253
+ assert search_fields is not None
254
+ return self._index.search(
255
+ queries=[query],
256
+ fields=search_fields,
257
+ limit=min(limit, self.max_results),
258
+ escape=True,
259
+ )
260
+
261
+ def fetch(self, document_ids: list[str]) -> list[dict[str, Any]]:
262
+ """
263
+ Direct fetch method for programmatic use.
264
+
265
+ Args:
266
+ document_ids: List of document IDs to fetch
267
+
268
+ Returns:
269
+ List of document dicts
270
+ """
271
+ documents, _, _ = self._index.fetch(document_ids)
272
+ return documents
273
+
274
+ def __del__(self):
275
+ """Clean up temp directory if used."""
276
+ if self._temp_dir is not None:
277
+ import shutil
278
+
279
+ try:
280
+ shutil.rmtree(self._temp_dir, ignore_errors=True)
281
+ except Exception:
282
+ pass
283
+
284
+
285
+ __all__ = ["FullTextSearchManager", "SearchResult"]