mrmd-ai 0.1.0__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 (52) hide show
  1. mrmd_ai-0.1.2/LICENSE +21 -0
  2. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/PKG-INFO +2 -1
  3. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/pyproject.toml +1 -1
  4. mrmd_ai-0.1.2/src/mrmd_ai/custom_programs.py +215 -0
  5. mrmd_ai-0.1.2/src/mrmd_ai/juice.py +758 -0
  6. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/__init__.py +11 -0
  7. mrmd_ai-0.1.2/src/mrmd_ai/modules/edit.py +102 -0
  8. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/server.py +275 -23
  9. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/__init__.py +15 -0
  10. mrmd_ai-0.1.2/src/mrmd_ai/signatures/edit.py +173 -0
  11. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/uv.lock +2 -7
  12. mrmd_ai-0.1.0/src/mrmd_ai/juice.py +0 -416
  13. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/.gitignore +0 -0
  14. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/README.md +0 -0
  15. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/dspy.config.yaml +0 -0
  16. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/AddTypeHintsPredict.log +0 -0
  17. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/CorrectAndFinishLinePredict.log +0 -0
  18. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/DocumentCodePredict.log +0 -0
  19. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/ExplainCodePredict.log +0 -0
  20. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/FinishCodeLinePredict.log +0 -0
  21. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/FinishCodeSectionPredict.log +0 -0
  22. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/FinishParagraphPredict.log +0 -0
  23. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/FinishSentencePredict.log +0 -0
  24. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/FixGrammarPredict.log +0 -0
  25. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/FixTranscriptionPredict.log +0 -0
  26. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/FormatCodePredict.log +0 -0
  27. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/GetSynonymsPredict.log +0 -0
  28. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/IdentifyReplacementPredict.log +0 -0
  29. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/ImproveNamesPredict.log +0 -0
  30. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/RefactorCodePredict.log +0 -0
  31. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/ReformatMarkdownPredict.log +0 -0
  32. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/logs/server.log +0 -0
  33. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/openapi.json +0 -0
  34. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/__init__.py +0 -0
  35. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/metrics/__init__.py +0 -0
  36. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/code.py +0 -0
  37. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/correct.py +0 -0
  38. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/document.py +0 -0
  39. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/finish.py +0 -0
  40. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/fix.py +0 -0
  41. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/notebook.py +0 -0
  42. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/modules/text.py +0 -0
  43. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/optimizers/__init__.py +0 -0
  44. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/code.py +0 -0
  45. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/correct.py +0 -0
  46. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/document.py +0 -0
  47. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/finish.py +0 -0
  48. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/fix.py +0 -0
  49. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/notebook.py +0 -0
  50. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/signatures/text.py +0 -0
  51. {mrmd_ai-0.1.0 → mrmd_ai-0.1.2}/src/mrmd_ai/utils/__init__.py +0 -0
  52. {mrmd_ai-0.1.0 → 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.
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mrmd-ai
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: AI programs for MRMD editor - completions, fixes, and corrections
5
+ License-File: LICENSE
5
6
  Requires-Python: >=3.11
6
7
  Requires-Dist: dspy>=2.6
7
8
  Requires-Dist: fastapi>=0.115
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mrmd-ai"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "AI programs for MRMD editor - completions, fixes, and corrections"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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)