mrmd-ai 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.
- mrmd_ai-0.1.2/LICENSE +21 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/PKG-INFO +2 -1
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/pyproject.toml +1 -1
- mrmd_ai-0.1.2/src/mrmd_ai/custom_programs.py +215 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/juice.py +92 -7
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/server.py +182 -8
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/uv.lock +2 -7
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/.gitignore +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/README.md +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/dspy.config.yaml +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/AddTypeHintsPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/CorrectAndFinishLinePredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/DocumentCodePredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/ExplainCodePredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/FinishCodeLinePredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/FinishCodeSectionPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/FinishParagraphPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/FinishSentencePredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/FixGrammarPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/FixTranscriptionPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/FormatCodePredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/GetSynonymsPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/IdentifyReplacementPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/ImproveNamesPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/RefactorCodePredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/ReformatMarkdownPredict.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/logs/server.log +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/openapi.json +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/__init__.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/metrics/__init__.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/__init__.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/code.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/correct.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/document.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/edit.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/finish.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/fix.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/notebook.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/text.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/optimizers/__init__.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/__init__.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/code.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/correct.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/document.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/edit.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/finish.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/fix.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/notebook.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/text.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/src/mrmd_ai/utils/__init__.py +0 -0
- {mrmd_ai-0.1.1 → mrmd_ai-0.1.2}/tests/__init__.py +0 -0
mrmd_ai-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Maxime Rivest
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Program Factory - Generate DSPy modules from user-defined templates.
|
|
3
|
+
|
|
4
|
+
This module allows users to create custom AI commands without writing code.
|
|
5
|
+
Users define their commands with:
|
|
6
|
+
- name: Display name for the command
|
|
7
|
+
- inputType: What text to process (selection, cursor, fullDoc)
|
|
8
|
+
- outputType: What to do with result (replace, insert)
|
|
9
|
+
- instructions: Natural language instructions for the AI
|
|
10
|
+
|
|
11
|
+
The factory generates DSPy Signature and Module classes dynamically.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
import dspy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_custom_signature(
|
|
19
|
+
name: str,
|
|
20
|
+
instructions: str,
|
|
21
|
+
input_type: str = "selection",
|
|
22
|
+
output_type: str = "replace",
|
|
23
|
+
) -> type:
|
|
24
|
+
"""Create a DSPy Signature class from user configuration.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: Command name (used for class naming)
|
|
28
|
+
instructions: User's instructions for the AI
|
|
29
|
+
input_type: "selection" | "cursor" | "fullDoc"
|
|
30
|
+
output_type: "replace" | "insert"
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A DSPy Signature class
|
|
34
|
+
"""
|
|
35
|
+
# Build the docstring from user instructions
|
|
36
|
+
docstring = f"""{instructions}
|
|
37
|
+
|
|
38
|
+
IMPORTANT RULES:
|
|
39
|
+
- Output ONLY the result text, no explanations or meta-commentary
|
|
40
|
+
- Maintain appropriate formatting (markdown, code style, etc.)
|
|
41
|
+
- Be concise and direct
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Define input fields based on input type
|
|
45
|
+
if input_type == "selection":
|
|
46
|
+
input_fields = {
|
|
47
|
+
"text": dspy.InputField(desc="The selected text to process"),
|
|
48
|
+
"local_context": dspy.InputField(desc="Text surrounding the selection for context"),
|
|
49
|
+
"document_context": dspy.InputField(desc="Broader document context"),
|
|
50
|
+
}
|
|
51
|
+
elif input_type == "cursor":
|
|
52
|
+
input_fields = {
|
|
53
|
+
"text_before_cursor": dspy.InputField(desc="Text before the cursor position"),
|
|
54
|
+
"local_context": dspy.InputField(desc="Text surrounding the cursor for context"),
|
|
55
|
+
"document_context": dspy.InputField(desc="Broader document context"),
|
|
56
|
+
}
|
|
57
|
+
else: # fullDoc
|
|
58
|
+
input_fields = {
|
|
59
|
+
"document_context": dspy.InputField(desc="The full document content"),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Define output field
|
|
63
|
+
output_field_name = "result"
|
|
64
|
+
output_fields = {
|
|
65
|
+
output_field_name: dspy.OutputField(desc="The processed result text. Output ONLY the result.")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Create the Signature class dynamically
|
|
69
|
+
# Clean name for class naming (remove spaces, special chars)
|
|
70
|
+
class_name = "".join(c for c in name if c.isalnum()) + "Signature"
|
|
71
|
+
|
|
72
|
+
signature_class = type(
|
|
73
|
+
class_name,
|
|
74
|
+
(dspy.Signature,),
|
|
75
|
+
{
|
|
76
|
+
"__doc__": docstring,
|
|
77
|
+
"__annotations__": {
|
|
78
|
+
**{k: str for k in input_fields},
|
|
79
|
+
**{k: str for k in output_fields},
|
|
80
|
+
},
|
|
81
|
+
**input_fields,
|
|
82
|
+
**output_fields,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return signature_class
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create_custom_module(
|
|
90
|
+
name: str,
|
|
91
|
+
instructions: str,
|
|
92
|
+
input_type: str = "selection",
|
|
93
|
+
output_type: str = "replace",
|
|
94
|
+
) -> type:
|
|
95
|
+
"""Create a DSPy Module class from user configuration.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
name: Command name
|
|
99
|
+
instructions: User's instructions for the AI
|
|
100
|
+
input_type: "selection" | "cursor" | "fullDoc"
|
|
101
|
+
output_type: "replace" | "insert"
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A DSPy Module class (not instance)
|
|
105
|
+
"""
|
|
106
|
+
signature = create_custom_signature(name, instructions, input_type, output_type)
|
|
107
|
+
|
|
108
|
+
# Clean name for class naming
|
|
109
|
+
class_name = "".join(c for c in name if c.isalnum()) + "Predict"
|
|
110
|
+
|
|
111
|
+
class CustomModule(dspy.Module):
|
|
112
|
+
"""Dynamically generated custom command module."""
|
|
113
|
+
|
|
114
|
+
def __init__(self):
|
|
115
|
+
super().__init__()
|
|
116
|
+
self.predictor = dspy.Predict(signature)
|
|
117
|
+
self._input_type = input_type
|
|
118
|
+
self._output_type = output_type
|
|
119
|
+
|
|
120
|
+
def forward(self, **kwargs) -> Any:
|
|
121
|
+
return self.predictor(**kwargs)
|
|
122
|
+
|
|
123
|
+
# Set the class name
|
|
124
|
+
CustomModule.__name__ = class_name
|
|
125
|
+
CustomModule.__qualname__ = class_name
|
|
126
|
+
|
|
127
|
+
return CustomModule
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CustomProgramRegistry:
|
|
131
|
+
"""Registry for user-defined custom programs.
|
|
132
|
+
|
|
133
|
+
Manages creation and caching of custom DSPy modules.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self):
|
|
137
|
+
self._modules: dict[str, type] = {}
|
|
138
|
+
self._configs: dict[str, dict] = {}
|
|
139
|
+
|
|
140
|
+
def register(self, program_id: str, config: dict) -> type:
|
|
141
|
+
"""Register a custom program from configuration.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
program_id: Unique identifier for this program
|
|
145
|
+
config: Dict with name, instructions, inputType, outputType
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
The generated Module class
|
|
149
|
+
"""
|
|
150
|
+
module_class = create_custom_module(
|
|
151
|
+
name=config.get("name", program_id),
|
|
152
|
+
instructions=config.get("instructions", "Process this text."),
|
|
153
|
+
input_type=config.get("inputType", "selection"),
|
|
154
|
+
output_type=config.get("outputType", "replace"),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self._modules[program_id] = module_class
|
|
158
|
+
self._configs[program_id] = config
|
|
159
|
+
|
|
160
|
+
return module_class
|
|
161
|
+
|
|
162
|
+
def get(self, program_id: str) -> type | None:
|
|
163
|
+
"""Get a registered module class by ID."""
|
|
164
|
+
return self._modules.get(program_id)
|
|
165
|
+
|
|
166
|
+
def get_config(self, program_id: str) -> dict | None:
|
|
167
|
+
"""Get the configuration for a registered program."""
|
|
168
|
+
return self._configs.get(program_id)
|
|
169
|
+
|
|
170
|
+
def unregister(self, program_id: str) -> bool:
|
|
171
|
+
"""Remove a program from the registry."""
|
|
172
|
+
if program_id in self._modules:
|
|
173
|
+
del self._modules[program_id]
|
|
174
|
+
del self._configs[program_id]
|
|
175
|
+
return True
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def clear(self):
|
|
179
|
+
"""Clear all registered programs."""
|
|
180
|
+
self._modules.clear()
|
|
181
|
+
self._configs.clear()
|
|
182
|
+
|
|
183
|
+
def list_programs(self) -> list[str]:
|
|
184
|
+
"""List all registered program IDs."""
|
|
185
|
+
return list(self._modules.keys())
|
|
186
|
+
|
|
187
|
+
def is_registered(self, program_id: str) -> bool:
|
|
188
|
+
"""Check if a program is registered."""
|
|
189
|
+
return program_id in self._modules
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Global registry instance
|
|
193
|
+
custom_registry = CustomProgramRegistry()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def register_custom_programs(commands: list[dict]) -> None:
|
|
197
|
+
"""Register multiple custom programs from a list of command configs.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
commands: List of command configurations, each with:
|
|
201
|
+
- id or program: Unique identifier
|
|
202
|
+
- name: Display name
|
|
203
|
+
- instructions: AI instructions
|
|
204
|
+
- inputType: selection | cursor | fullDoc
|
|
205
|
+
- outputType: replace | insert
|
|
206
|
+
"""
|
|
207
|
+
for cmd in commands:
|
|
208
|
+
program_id = cmd.get("program") or cmd.get("id")
|
|
209
|
+
if program_id:
|
|
210
|
+
custom_registry.register(program_id, cmd)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_custom_program(program_id: str) -> type | None:
|
|
214
|
+
"""Get a custom program module class by ID."""
|
|
215
|
+
return custom_registry.get(program_id)
|
|
@@ -213,15 +213,73 @@ SYNTHESIZER_MODEL = ModelConfig(
|
|
|
213
213
|
)
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def get_api_key_for_model(model: str, api_keys: dict | None) -> str | None:
|
|
217
|
+
"""Get the appropriate API key for a model based on its provider.
|
|
218
|
+
|
|
219
|
+
Uses LiteLLM model naming convention: provider/model-name
|
|
220
|
+
Supports any provider that the user has configured in settings.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
model: Model identifier (e.g., "anthropic/claude-sonnet-4-5")
|
|
224
|
+
api_keys: Dict of provider -> API key
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
API key string or None if not found/provided.
|
|
228
|
+
"""
|
|
229
|
+
if not api_keys:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
model_lower = model.lower()
|
|
233
|
+
|
|
234
|
+
# Extract provider from model name (LiteLLM format: provider/model-name)
|
|
235
|
+
if "/" in model:
|
|
236
|
+
provider = model.split("/")[0].lower()
|
|
237
|
+
# Check for direct provider match
|
|
238
|
+
if provider in api_keys and api_keys[provider]:
|
|
239
|
+
return api_keys[provider]
|
|
240
|
+
|
|
241
|
+
# Fallback: Check for known provider patterns in model name
|
|
242
|
+
# This handles cases like "claude-3-sonnet" without prefix
|
|
243
|
+
provider_patterns = {
|
|
244
|
+
"anthropic": ["anthropic/", "claude"],
|
|
245
|
+
"openai": ["openai/", "gpt-", "o1-", "o3-"],
|
|
246
|
+
"groq": ["groq/"],
|
|
247
|
+
"gemini": ["gemini/", "gemini-"],
|
|
248
|
+
"openrouter": ["openrouter/"],
|
|
249
|
+
"together_ai": ["together_ai/", "together/"],
|
|
250
|
+
"fireworks_ai": ["fireworks_ai/", "fireworks/"],
|
|
251
|
+
"mistral": ["mistral/"],
|
|
252
|
+
"cohere": ["cohere/"],
|
|
253
|
+
"deepseek": ["deepseek/"],
|
|
254
|
+
"ollama": ["ollama/"],
|
|
255
|
+
"azure": ["azure/"],
|
|
256
|
+
"bedrock": ["bedrock/"],
|
|
257
|
+
"vertex_ai": ["vertex_ai/"],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for provider, patterns in provider_patterns.items():
|
|
261
|
+
for pattern in patterns:
|
|
262
|
+
if pattern in model_lower:
|
|
263
|
+
if provider in api_keys and api_keys[provider]:
|
|
264
|
+
return api_keys[provider]
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
216
270
|
def get_lm(
|
|
217
271
|
juice: JuiceLevel | int = JuiceLevel.QUICK,
|
|
218
|
-
reasoning: ReasoningLevel | int | None = None
|
|
272
|
+
reasoning: ReasoningLevel | int | None = None,
|
|
273
|
+
api_keys: dict | None = None,
|
|
274
|
+
model_override: str | None = None,
|
|
219
275
|
) -> dspy.LM:
|
|
220
276
|
"""Get a dspy.LM configured for the specified juice and reasoning levels.
|
|
221
277
|
|
|
222
278
|
Args:
|
|
223
279
|
juice: Juice level (0-3). Level 4 (ULTIMATE) requires special handling.
|
|
224
280
|
reasoning: Optional reasoning level (0-5). If None, uses juice level's default.
|
|
281
|
+
api_keys: Optional dict of provider -> API key. If provided, overrides env vars.
|
|
282
|
+
model_override: Optional model to use instead of the default for this juice level.
|
|
225
283
|
|
|
226
284
|
Returns:
|
|
227
285
|
Configured dspy.LM instance.
|
|
@@ -235,6 +293,15 @@ def get_lm(
|
|
|
235
293
|
config = JUICE_MODELS[juice]
|
|
236
294
|
kwargs = config.to_lm_kwargs()
|
|
237
295
|
|
|
296
|
+
# Apply model override if provided
|
|
297
|
+
if model_override:
|
|
298
|
+
kwargs["model"] = model_override
|
|
299
|
+
|
|
300
|
+
# Get API key for this model's provider
|
|
301
|
+
api_key = get_api_key_for_model(kwargs["model"], api_keys)
|
|
302
|
+
if api_key:
|
|
303
|
+
kwargs["api_key"] = api_key
|
|
304
|
+
|
|
238
305
|
# Apply reasoning level overrides if specified AND model supports reasoning
|
|
239
306
|
if reasoning is not None and config.supports_reasoning:
|
|
240
307
|
if isinstance(reasoning, int):
|
|
@@ -313,7 +380,9 @@ class JuicedProgram:
|
|
|
313
380
|
program: dspy.Module,
|
|
314
381
|
juice: JuiceLevel | int = JuiceLevel.QUICK,
|
|
315
382
|
reasoning: ReasoningLevel | int | None = None,
|
|
316
|
-
progress_callback: Callable[[str, dict], None] | None = None
|
|
383
|
+
progress_callback: Callable[[str, dict], None] | None = None,
|
|
384
|
+
api_keys: dict | None = None,
|
|
385
|
+
model_override: str | None = None,
|
|
317
386
|
):
|
|
318
387
|
"""Initialize a juiced program.
|
|
319
388
|
|
|
@@ -326,11 +395,15 @@ class JuicedProgram:
|
|
|
326
395
|
- "status": General status update
|
|
327
396
|
- "model_start": A model is starting (ultimate mode)
|
|
328
397
|
- "model_complete": A model finished (ultimate mode)
|
|
398
|
+
api_keys: Optional dict of provider -> API key. Overrides env vars.
|
|
399
|
+
model_override: Optional model to use instead of the default for this juice level.
|
|
329
400
|
"""
|
|
330
401
|
self.program = program
|
|
331
402
|
self.juice = JuiceLevel(juice) if isinstance(juice, int) else juice
|
|
332
403
|
self.reasoning = ReasoningLevel(reasoning) if isinstance(reasoning, int) else reasoning
|
|
333
404
|
self.progress_callback = progress_callback
|
|
405
|
+
self.api_keys = api_keys
|
|
406
|
+
self.model_override = model_override
|
|
334
407
|
|
|
335
408
|
def _emit(self, event_type: str, data: dict):
|
|
336
409
|
"""Emit a progress event if callback is set."""
|
|
@@ -347,7 +420,10 @@ class JuicedProgram:
|
|
|
347
420
|
def _run_single(self, **kwargs) -> Any:
|
|
348
421
|
"""Run with a single model at the specified juice level."""
|
|
349
422
|
config = JUICE_MODELS[self.juice]
|
|
350
|
-
|
|
423
|
+
|
|
424
|
+
# Use model override if provided, otherwise use default for juice level
|
|
425
|
+
actual_model = self.model_override if self.model_override else config.model
|
|
426
|
+
model_name = actual_model.split("/")[-1]
|
|
351
427
|
|
|
352
428
|
reasoning_desc = ""
|
|
353
429
|
if self.reasoning is not None:
|
|
@@ -356,11 +432,11 @@ class JuicedProgram:
|
|
|
356
432
|
self._emit("status", {
|
|
357
433
|
"step": "calling_model",
|
|
358
434
|
"model": model_name,
|
|
359
|
-
"model_full":
|
|
435
|
+
"model_full": actual_model,
|
|
360
436
|
"reasoning_level": self.reasoning.value if self.reasoning else None,
|
|
361
437
|
})
|
|
362
438
|
|
|
363
|
-
lm = get_lm(self.juice, self.reasoning)
|
|
439
|
+
lm = get_lm(self.juice, self.reasoning, api_keys=self.api_keys, model_override=self.model_override)
|
|
364
440
|
with dspy.context(lm=lm):
|
|
365
441
|
result = self.program(**kwargs)
|
|
366
442
|
|
|
@@ -416,6 +492,11 @@ class JuicedProgram:
|
|
|
416
492
|
if reasoning_config["reasoning_effort"] is not None:
|
|
417
493
|
lm_kwargs["reasoning_effort"] = reasoning_config["reasoning_effort"]
|
|
418
494
|
|
|
495
|
+
# Apply API key for this model's provider
|
|
496
|
+
api_key = get_api_key_for_model(config.model, self.api_keys)
|
|
497
|
+
if api_key:
|
|
498
|
+
lm_kwargs["api_key"] = api_key
|
|
499
|
+
|
|
419
500
|
lm = dspy.LM(**lm_kwargs)
|
|
420
501
|
model_name = config.model.split("/")[-1]
|
|
421
502
|
|
|
@@ -536,8 +617,12 @@ class JuicedProgram:
|
|
|
536
617
|
# Create synthesized result
|
|
537
618
|
merged = {}
|
|
538
619
|
|
|
539
|
-
# Configure synthesizer LM
|
|
540
|
-
|
|
620
|
+
# Configure synthesizer LM with API key if provided
|
|
621
|
+
synth_kwargs = SYNTHESIZER_MODEL.to_lm_kwargs()
|
|
622
|
+
api_key = get_api_key_for_model(SYNTHESIZER_MODEL.model, self.api_keys)
|
|
623
|
+
if api_key:
|
|
624
|
+
synth_kwargs["api_key"] = api_key
|
|
625
|
+
synth_lm = dspy.LM(**synth_kwargs)
|
|
541
626
|
|
|
542
627
|
# Synthesize each output field
|
|
543
628
|
for field_name, base_value in base_store.items():
|
|
@@ -24,6 +24,7 @@ import json
|
|
|
24
24
|
_executor = ThreadPoolExecutor(max_workers=10)
|
|
25
25
|
|
|
26
26
|
from .juice import JuiceLevel, ReasoningLevel, JuicedProgram, get_lm, JUICE_MODELS, REASONING_DESCRIPTIONS
|
|
27
|
+
from .custom_programs import custom_registry, register_custom_programs
|
|
27
28
|
from .modules import (
|
|
28
29
|
# Finish
|
|
29
30
|
FinishSentencePredict,
|
|
@@ -105,16 +106,91 @@ PROGRAMS = {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
# Cached program instances per juice level and reasoning level
|
|
109
|
+
# NOTE: Cache is only used when no custom API keys are provided
|
|
108
110
|
_program_cache: dict[tuple[str, int, int | None], JuicedProgram] = {}
|
|
109
111
|
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
# API key header names
|
|
114
|
+
API_KEY_HEADERS = {
|
|
115
|
+
"anthropic": "X-Api-Key-Anthropic",
|
|
116
|
+
"openai": "X-Api-Key-Openai",
|
|
117
|
+
"groq": "X-Api-Key-Groq",
|
|
118
|
+
"gemini": "X-Api-Key-Gemini",
|
|
119
|
+
"openrouter": "X-Api-Key-Openrouter",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def extract_api_keys(request: Request) -> dict | None:
|
|
124
|
+
"""Extract API keys from request headers.
|
|
125
|
+
|
|
126
|
+
Headers:
|
|
127
|
+
X-Api-Key-Anthropic: Anthropic API key
|
|
128
|
+
X-Api-Key-Openai: OpenAI API key
|
|
129
|
+
X-Api-Key-Groq: Groq API key
|
|
130
|
+
X-Api-Key-Gemini: Google Gemini API key
|
|
131
|
+
X-Api-Key-Openrouter: OpenRouter API key
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dict of provider -> key if any keys are provided, None otherwise.
|
|
135
|
+
"""
|
|
136
|
+
api_keys = {}
|
|
137
|
+
for provider, header in API_KEY_HEADERS.items():
|
|
138
|
+
key = request.headers.get(header)
|
|
139
|
+
if key:
|
|
140
|
+
api_keys[provider] = key
|
|
141
|
+
|
|
142
|
+
return api_keys if api_keys else None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_program(
|
|
146
|
+
name: str,
|
|
147
|
+
juice: int = 0,
|
|
148
|
+
reasoning: int | None = None,
|
|
149
|
+
api_keys: dict | None = None,
|
|
150
|
+
model_override: str | None = None,
|
|
151
|
+
) -> JuicedProgram:
|
|
152
|
+
"""Get a JuicedProgram instance for the given program configuration.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
name: Program name (can be built-in or custom)
|
|
156
|
+
juice: Juice level (0-4)
|
|
157
|
+
reasoning: Optional reasoning level (0-5)
|
|
158
|
+
api_keys: Optional dict of provider -> API key
|
|
159
|
+
model_override: Optional model to use instead of default
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Configured JuicedProgram instance.
|
|
163
|
+
|
|
164
|
+
Note:
|
|
165
|
+
Programs with custom API keys or model overrides are NOT cached,
|
|
166
|
+
since they need fresh instances with the provided configuration.
|
|
167
|
+
Custom programs are never cached.
|
|
168
|
+
"""
|
|
169
|
+
# Check built-in programs first
|
|
170
|
+
program_class = PROGRAMS.get(name)
|
|
171
|
+
|
|
172
|
+
# If not found, check custom registry
|
|
173
|
+
if program_class is None:
|
|
174
|
+
program_class = custom_registry.get(name)
|
|
175
|
+
|
|
176
|
+
if program_class is None:
|
|
177
|
+
raise ValueError(f"Unknown program: {name}")
|
|
178
|
+
|
|
179
|
+
# Custom programs and those with custom config are never cached
|
|
180
|
+
is_custom = name not in PROGRAMS
|
|
181
|
+
if api_keys or model_override or is_custom:
|
|
182
|
+
program = program_class()
|
|
183
|
+
return JuicedProgram(
|
|
184
|
+
program,
|
|
185
|
+
juice=juice,
|
|
186
|
+
reasoning=reasoning,
|
|
187
|
+
api_keys=api_keys,
|
|
188
|
+
model_override=model_override,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Use cache for standard built-in requests (no custom keys)
|
|
113
192
|
cache_key = (name, juice, reasoning)
|
|
114
193
|
if cache_key not in _program_cache:
|
|
115
|
-
if name not in PROGRAMS:
|
|
116
|
-
raise ValueError(f"Unknown program: {name}")
|
|
117
|
-
program_class = PROGRAMS[name]
|
|
118
194
|
program = program_class()
|
|
119
195
|
_program_cache[cache_key] = JuicedProgram(program, juice=juice, reasoning=reasoning)
|
|
120
196
|
return _program_cache[cache_key]
|
|
@@ -250,6 +326,12 @@ async def run_program(program_name: str, request: Request):
|
|
|
250
326
|
except ValueError:
|
|
251
327
|
reasoning_level = None
|
|
252
328
|
|
|
329
|
+
# Extract API keys from headers (optional)
|
|
330
|
+
api_keys = extract_api_keys(request)
|
|
331
|
+
|
|
332
|
+
# Get model override from header (optional)
|
|
333
|
+
model_override = request.headers.get("X-Model-Override")
|
|
334
|
+
|
|
253
335
|
# Get request body
|
|
254
336
|
try:
|
|
255
337
|
params = await request.json()
|
|
@@ -258,7 +340,13 @@ async def run_program(program_name: str, request: Request):
|
|
|
258
340
|
|
|
259
341
|
# Get program
|
|
260
342
|
try:
|
|
261
|
-
juiced_program = get_program(
|
|
343
|
+
juiced_program = get_program(
|
|
344
|
+
program_name,
|
|
345
|
+
juice_level,
|
|
346
|
+
reasoning_level,
|
|
347
|
+
api_keys=api_keys,
|
|
348
|
+
model_override=model_override,
|
|
349
|
+
)
|
|
262
350
|
except ValueError as e:
|
|
263
351
|
raise HTTPException(status_code=404, detail=str(e))
|
|
264
352
|
|
|
@@ -347,6 +435,12 @@ async def run_program_stream(program_name: str, request: Request):
|
|
|
347
435
|
except ValueError:
|
|
348
436
|
reasoning_level = None
|
|
349
437
|
|
|
438
|
+
# Extract API keys from headers (optional)
|
|
439
|
+
api_keys = extract_api_keys(request)
|
|
440
|
+
|
|
441
|
+
# Get model override from header (optional)
|
|
442
|
+
model_override = request.headers.get("X-Model-Override")
|
|
443
|
+
|
|
350
444
|
# Get request body
|
|
351
445
|
try:
|
|
352
446
|
params = await request.json()
|
|
@@ -390,10 +484,17 @@ async def run_program_stream(program_name: str, request: Request):
|
|
|
390
484
|
def run_with_progress():
|
|
391
485
|
"""Run the program in a thread, emitting progress events."""
|
|
392
486
|
try:
|
|
393
|
-
# Create program with progress callback
|
|
487
|
+
# Create program with progress callback and optional API keys
|
|
394
488
|
program_class = PROGRAMS[program_name]
|
|
395
489
|
program = program_class()
|
|
396
|
-
juiced = JuicedProgram(
|
|
490
|
+
juiced = JuicedProgram(
|
|
491
|
+
program,
|
|
492
|
+
juice=juice_level,
|
|
493
|
+
reasoning=reasoning_level,
|
|
494
|
+
progress_callback=progress_callback,
|
|
495
|
+
api_keys=api_keys,
|
|
496
|
+
model_override=model_override,
|
|
497
|
+
)
|
|
397
498
|
|
|
398
499
|
# Emit starting event
|
|
399
500
|
progress_callback("status", {
|
|
@@ -468,6 +569,79 @@ async def run_program_stream(program_name: str, request: Request):
|
|
|
468
569
|
)
|
|
469
570
|
|
|
470
571
|
|
|
572
|
+
# =============================================================================
|
|
573
|
+
# CUSTOM PROGRAMS API
|
|
574
|
+
# =============================================================================
|
|
575
|
+
|
|
576
|
+
class CustomProgramConfig(BaseModel):
|
|
577
|
+
"""Configuration for a custom program."""
|
|
578
|
+
id: str # Unique program ID (e.g., "Custom_cmd-123")
|
|
579
|
+
name: str
|
|
580
|
+
instructions: str
|
|
581
|
+
inputType: str = "selection" # selection | cursor | fullDoc
|
|
582
|
+
outputType: str = "replace" # replace | insert
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class RegisterCustomProgramsRequest(BaseModel):
|
|
586
|
+
"""Request to register multiple custom programs."""
|
|
587
|
+
programs: list[CustomProgramConfig]
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@app.post("/api/custom-programs/register")
|
|
591
|
+
async def register_custom_programs_endpoint(request: RegisterCustomProgramsRequest):
|
|
592
|
+
"""Register custom programs from frontend configuration.
|
|
593
|
+
|
|
594
|
+
This endpoint is called when the app starts or when settings change.
|
|
595
|
+
It clears existing custom programs and registers the new ones.
|
|
596
|
+
"""
|
|
597
|
+
# Clear existing custom programs
|
|
598
|
+
custom_registry.clear()
|
|
599
|
+
|
|
600
|
+
# Register new programs
|
|
601
|
+
registered = []
|
|
602
|
+
for prog in request.programs:
|
|
603
|
+
try:
|
|
604
|
+
custom_registry.register(
|
|
605
|
+
program_id=prog.id,
|
|
606
|
+
config={
|
|
607
|
+
"name": prog.name,
|
|
608
|
+
"instructions": prog.instructions,
|
|
609
|
+
"inputType": prog.inputType,
|
|
610
|
+
"outputType": prog.outputType,
|
|
611
|
+
}
|
|
612
|
+
)
|
|
613
|
+
registered.append(prog.id)
|
|
614
|
+
print(f"[Custom] Registered: {prog.name} ({prog.id})")
|
|
615
|
+
except Exception as e:
|
|
616
|
+
print(f"[Custom] Failed to register {prog.id}: {e}")
|
|
617
|
+
|
|
618
|
+
return {"registered": registered, "count": len(registered)}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
@app.get("/api/custom-programs")
|
|
622
|
+
async def list_custom_programs():
|
|
623
|
+
"""List all registered custom programs."""
|
|
624
|
+
programs = []
|
|
625
|
+
for program_id in custom_registry.list_programs():
|
|
626
|
+
config = custom_registry.get_config(program_id)
|
|
627
|
+
if config:
|
|
628
|
+
programs.append({
|
|
629
|
+
"id": program_id,
|
|
630
|
+
"name": config.get("name", program_id),
|
|
631
|
+
"inputType": config.get("inputType", "selection"),
|
|
632
|
+
"outputType": config.get("outputType", "replace"),
|
|
633
|
+
})
|
|
634
|
+
return {"programs": programs}
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@app.delete("/api/custom-programs/{program_id}")
|
|
638
|
+
async def unregister_custom_program(program_id: str):
|
|
639
|
+
"""Unregister a specific custom program."""
|
|
640
|
+
if custom_registry.unregister(program_id):
|
|
641
|
+
return {"success": True, "id": program_id}
|
|
642
|
+
raise HTTPException(status_code=404, detail=f"Program not found: {program_id}")
|
|
643
|
+
|
|
644
|
+
|
|
471
645
|
def main():
|
|
472
646
|
"""Run the AI server."""
|
|
473
647
|
import argparse
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 3
|
|
3
3
|
requires-python = ">=3.11"
|
|
4
4
|
resolution-markers = [
|
|
5
5
|
"python_full_version >= '3.14'",
|
|
@@ -584,7 +584,6 @@ wheels = [
|
|
|
584
584
|
{ url = "https://files.pythonhosted.org/packages/1f/cb/48e964c452ca2b92175a9b2dca037a553036cb053ba69e284650ce755f13/greenlet-3.3.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e", size = 274908, upload-time = "2025-12-04T14:23:26.435Z" },
|
|
585
585
|
{ url = "https://files.pythonhosted.org/packages/28/da/38d7bff4d0277b594ec557f479d65272a893f1f2a716cad91efeb8680953/greenlet-3.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62", size = 577113, upload-time = "2025-12-04T14:50:05.493Z" },
|
|
586
586
|
{ url = "https://files.pythonhosted.org/packages/3c/f2/89c5eb0faddc3ff014f1c04467d67dee0d1d334ab81fadbf3744847f8a8a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32", size = 590338, upload-time = "2025-12-04T14:57:41.136Z" },
|
|
587
|
-
{ url = "https://files.pythonhosted.org/packages/80/d7/db0a5085035d05134f8c089643da2b44cc9b80647c39e93129c5ef170d8f/greenlet-3.3.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45", size = 601098, upload-time = "2025-12-04T15:07:11.898Z" },
|
|
588
587
|
{ url = "https://files.pythonhosted.org/packages/dc/a6/e959a127b630a58e23529972dbc868c107f9d583b5a9f878fb858c46bc1a/greenlet-3.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948", size = 590206, upload-time = "2025-12-04T14:26:01.254Z" },
|
|
589
588
|
{ url = "https://files.pythonhosted.org/packages/48/60/29035719feb91798693023608447283b266b12efc576ed013dd9442364bb/greenlet-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794", size = 1550668, upload-time = "2025-12-04T15:04:22.439Z" },
|
|
590
589
|
{ url = "https://files.pythonhosted.org/packages/0a/5f/783a23754b691bfa86bd72c3033aa107490deac9b2ef190837b860996c9f/greenlet-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5", size = 1615483, upload-time = "2025-12-04T14:27:28.083Z" },
|
|
@@ -592,7 +591,6 @@ wheels = [
|
|
|
592
591
|
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
|
593
592
|
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
|
594
593
|
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
|
595
|
-
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
|
596
594
|
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
|
597
595
|
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
|
598
596
|
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
|
@@ -600,7 +598,6 @@ wheels = [
|
|
|
600
598
|
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
|
|
601
599
|
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
|
|
602
600
|
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
|
|
603
|
-
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
|
|
604
601
|
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
|
|
605
602
|
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
|
|
606
603
|
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
|
|
@@ -608,7 +605,6 @@ wheels = [
|
|
|
608
605
|
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
|
|
609
606
|
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
|
|
610
607
|
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
|
|
611
|
-
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
|
|
612
608
|
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
|
|
613
609
|
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
|
|
614
610
|
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
|
|
@@ -616,7 +612,6 @@ wheels = [
|
|
|
616
612
|
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
|
|
617
613
|
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
|
|
618
614
|
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
|
|
619
|
-
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
|
|
620
615
|
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
|
|
621
616
|
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
|
|
622
617
|
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },
|
|
@@ -1026,7 +1021,7 @@ wheels = [
|
|
|
1026
1021
|
|
|
1027
1022
|
[[package]]
|
|
1028
1023
|
name = "mrmd-ai"
|
|
1029
|
-
version = "0.1.
|
|
1024
|
+
version = "0.1.1"
|
|
1030
1025
|
source = { editable = "." }
|
|
1031
1026
|
dependencies = [
|
|
1032
1027
|
{ name = "dspy" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|