satif-ai 0.2.2__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: satif-ai
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: AI Agents for Satif
5
5
  License: MIT
6
6
  Author: Bryan Djafer
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "satif-ai"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "AI Agents for Satif"
5
5
  authors = [
6
6
  {name = "Bryan Djafer", email = "bryan.djafer@syncpulse.fr"}
@@ -20,7 +20,7 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
20
20
  build-backend = "poetry.core.masonry.api"
21
21
 
22
22
  [project.scripts]
23
- satif = "satif.cli:main"
23
+ satif-ai = "satif.cli:main"
24
24
 
25
25
  [tool.poetry.group.dev.dependencies]
26
26
  pytest = "^8.3.5"
@@ -0,0 +1,229 @@
1
+ import base64
2
+ import os
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional, Union
6
+
7
+ from agents import Agent, Runner, function_tool
8
+ from agents.mcp.server import MCPServer
9
+ from mcp import ClientSession
10
+ from satif_core import AsyncCodeBuilder, CodeBuilder, SDIFDatabase
11
+ from satif_sdk.comparators import get_comparator
12
+ from satif_sdk.representers import get_representer
13
+ from satif_sdk.transformers import CodeTransformer
14
+
15
+ # Global variables for transformation
16
+ INPUT_SDIF_PATH: Optional[Path] = None
17
+ OUTPUT_TARGET_FILES: Optional[Dict[Union[str, Path], str]] = None
18
+
19
+
20
+ @function_tool
21
+ async def execute_transformation(code: str) -> str:
22
+ """Executes the transformation code on the input and returns the
23
+ comparison difference between the transformed output and the target output example.
24
+
25
+ Args:
26
+ code: The code to execute on the input.
27
+ """
28
+ if INPUT_SDIF_PATH is None or OUTPUT_TARGET_FILES is None:
29
+ return "Error: Transformation context not initialized"
30
+
31
+ code_transformer = CodeTransformer(function=code)
32
+ generated_output_path = code_transformer.export(INPUT_SDIF_PATH)
33
+
34
+ comparisons = []
35
+
36
+ if os.path.isdir(generated_output_path):
37
+ # If it's a directory, compare each file with its corresponding target
38
+ generated_files = os.listdir(generated_output_path)
39
+
40
+ for (
41
+ output_base_file,
42
+ output_target_file_name,
43
+ ) in OUTPUT_TARGET_FILES.items():
44
+ if output_target_file_name in generated_files:
45
+ generated_file_path = os.path.join(
46
+ generated_output_path, output_target_file_name
47
+ )
48
+ comparator = get_comparator(output_target_file_name.split(".")[-1])
49
+ comparison = comparator.compare(generated_file_path, output_base_file)
50
+ comparisons.append(
51
+ f"Comparison for {generated_file_path} [SOURCE] with {output_target_file_name} [TARGET]: {comparison}"
52
+ )
53
+ else:
54
+ comparisons.append(
55
+ f"Error: {output_target_file_name} not found in the generated output"
56
+ )
57
+ else:
58
+ # If it's a single file, ensure there's only one target and compare
59
+ if len(OUTPUT_TARGET_FILES) == 1:
60
+ output_file = list(OUTPUT_TARGET_FILES.keys())[0]
61
+ output_target_file_name = list(OUTPUT_TARGET_FILES.values())[0]
62
+ comparator = get_comparator(output_file.split(".")[-1])
63
+ comparison = comparator.compare(generated_output_path, output_file)
64
+ comparisons.append(
65
+ f"Comparison for {generated_output_path} [SOURCE] with {output_target_file_name} [TARGET]: {comparison}"
66
+ )
67
+ else:
68
+ comparisons.append(
69
+ "Error: Single output file generated but multiple target files expected"
70
+ )
71
+
72
+ return "\n".join(comparisons)
73
+
74
+
75
+ class TransformationCodeBuilder(CodeBuilder):
76
+ def __init__(self, output_example: Path | List[Path] | Dict[str, Path]):
77
+ self.output_example = output_example
78
+
79
+ def build(
80
+ self,
81
+ sdif: Path | SDIFDatabase,
82
+ instructions: Optional[str] = None,
83
+ ) -> str:
84
+ pass
85
+
86
+
87
+ class TransformationAsyncCodeBuilder(AsyncCodeBuilder):
88
+ """This class is used to build a transformation code that will be used to transform a SDIF database into a set of files following the format of the given output files."""
89
+
90
+ def __init__(
91
+ self,
92
+ mcp_server: MCPServer,
93
+ mcp_session: ClientSession,
94
+ llm_model: str = "o3-mini",
95
+ ):
96
+ self.mcp_server = mcp_server
97
+ self.mcp_session = mcp_session
98
+ self.llm_model = llm_model
99
+
100
+ async def build(
101
+ self,
102
+ sdif: Path, # This will now be relative to project root (MCP server CWD)
103
+ output_target_files: Dict[Union[str, Path], str] | List[Path],
104
+ output_sdif: Optional[Path] = None, # This will now be relative or None
105
+ instructions: Optional[str] = None,
106
+ ) -> str:
107
+ global INPUT_SDIF_PATH, OUTPUT_TARGET_FILES
108
+ # INPUT_SDIF_PATH is used by execute_transformation tool, needs to be accessible from where that tool runs.
109
+ # If execute_transformation runs in the same process as the builder, absolute path is fine.
110
+ # If it were a separate context, this might need adjustment.
111
+ # For now, assume execute_transformation can access absolute paths if needed for its *input SDIF*.
112
+ # However, the sdif for MCP URIs must be relative.
113
+ INPUT_SDIF_PATH = Path(
114
+ sdif
115
+ ).resolve() # Keep this as absolute for the tool's potential direct access.
116
+
117
+ # Paths for MCP URIs are now expected to be relative to MCP server CWD (project root)
118
+ # So, use them directly as strings.
119
+ input_sdif_mcp_uri_path = base64.b64encode(str(sdif).encode()).decode()
120
+ output_sdif_mcp_uri_path = (
121
+ base64.b64encode(str(output_sdif).encode()).decode()
122
+ if output_sdif
123
+ else None
124
+ )
125
+
126
+ input_schema = await self.mcp_session.read_resource(
127
+ f"schema://{input_sdif_mcp_uri_path}"
128
+ )
129
+ input_sample = await self.mcp_session.read_resource(
130
+ f"sample://{input_sdif_mcp_uri_path}"
131
+ )
132
+
133
+ output_schema_text = "N/A"
134
+ output_sample_text = "N/A"
135
+ if output_sdif_mcp_uri_path:
136
+ try:
137
+ output_schema_content = await self.mcp_session.read_resource(
138
+ f"schema://{output_sdif_mcp_uri_path}"
139
+ )
140
+ if output_schema_content.contents:
141
+ output_schema_text = output_schema_content.contents[0].text
142
+ except Exception as e:
143
+ print(
144
+ f"Warning: Could not read schema for output_sdif {output_sdif_mcp_uri_path}: {e}"
145
+ )
146
+
147
+ try:
148
+ output_sample_content = await self.mcp_session.read_resource(
149
+ f"sample://{output_sdif_mcp_uri_path}"
150
+ )
151
+ if output_sample_content.contents:
152
+ output_sample_text = output_sample_content.contents[0].text
153
+ except Exception as e:
154
+ print(
155
+ f"Warning: Could not read sample for output_sdif {output_sdif_mcp_uri_path}: {e}"
156
+ )
157
+
158
+ # OUTPUT_TARGET_FILES keys are absolute paths to original example files for local reading by representers/comparators.
159
+ # Values are agent-facing filenames.
160
+ if isinstance(output_target_files, list):
161
+ OUTPUT_TARGET_FILES = {
162
+ file_path.resolve(): file_path.name for file_path in output_target_files
163
+ }
164
+ elif isinstance(output_target_files, dict):
165
+ temp_map = {}
166
+ for k, v in output_target_files.items():
167
+ if isinstance(k, Path):
168
+ temp_map[k.resolve()] = v
169
+ else:
170
+ temp_map[k] = v
171
+ OUTPUT_TARGET_FILES = temp_map
172
+ else:
173
+ OUTPUT_TARGET_FILES = {}
174
+
175
+ output_representation = {}
176
+ if OUTPUT_TARGET_FILES:
177
+ for file_key_abs_path in list(OUTPUT_TARGET_FILES.keys()):
178
+ agent_facing_name = OUTPUT_TARGET_FILES[file_key_abs_path]
179
+ print(f"Representing {agent_facing_name} from {file_key_abs_path}!!")
180
+ try:
181
+ # Representer uses the absolute path (file_key_abs_path) to read the example file.
182
+ output_representation[agent_facing_name] = get_representer(
183
+ file_key_abs_path
184
+ ).represent(file_key_abs_path)
185
+ except Exception as e:
186
+ print(
187
+ f"Warning: Could not get representation for {agent_facing_name} (path {file_key_abs_path}): {e}"
188
+ )
189
+ output_representation[agent_facing_name] = (
190
+ f"Error representing file: {e}"
191
+ )
192
+
193
+ prompt = await self.mcp_session.get_prompt(
194
+ "create_transformation",
195
+ arguments={
196
+ "input_file": Path(
197
+ input_sdif_mcp_uri_path
198
+ ).name, # Display name for prompt (from relative path)
199
+ "input_schema": input_schema.contents[0].text
200
+ if input_schema.contents
201
+ else "Error reading input schema",
202
+ "input_sample": input_sample.contents[0].text
203
+ if input_sample.contents
204
+ else "Error reading input sample",
205
+ "output_files": str(list(OUTPUT_TARGET_FILES.values())),
206
+ "output_schema": output_schema_text,
207
+ "output_sample": output_sample_text,
208
+ "output_representation": str(
209
+ output_representation
210
+ ), # Representation keyed by agent-facing name
211
+ },
212
+ )
213
+ agent = Agent(
214
+ name="Transformation Builder",
215
+ mcp_servers=[self.mcp_server],
216
+ tools=[execute_transformation],
217
+ model=self.llm_model,
218
+ )
219
+ result = await Runner.run(agent, prompt.messages[0].content.text)
220
+ transformation_code = self.parse_code(result.final_output)
221
+ return transformation_code
222
+
223
+ def parse_code(self, code) -> str:
224
+ match = re.search(r"```(?:python)?(.*?)```", code, re.DOTALL)
225
+ if match:
226
+ return match.group(1).strip()
227
+ else:
228
+ # Handle case where no code block is found
229
+ return code.strip()
@@ -1,152 +0,0 @@
1
- import os
2
- import re
3
- from pathlib import Path
4
- from typing import Dict, List, Optional, Union
5
-
6
- from agents import Agent, Runner, function_tool
7
- from agents.mcp.server import MCPServerStdio
8
- from mcp import ClientSession
9
- from satif_core import AsyncCodeBuilder, CodeBuilder, SDIFDatabase
10
- from satif_sdk.comparators import get_comparator
11
- from satif_sdk.representers import get_representer
12
- from satif_sdk.transformers import CodeTransformer
13
-
14
- # Global variables for transformation
15
- INPUT_SDIF_PATH: Optional[Path] = None
16
- OUTPUT_TARGET_FILES: Optional[Dict[Union[str, Path], str]] = None
17
-
18
-
19
- @function_tool
20
- async def execute_transformation(code: str) -> str:
21
- """Executes the transformation code on the input and returns the
22
- comparison difference between the transformed output and the target output example.
23
-
24
- Args:
25
- code: The code to execute on the input.
26
- """
27
- if INPUT_SDIF_PATH is None or OUTPUT_TARGET_FILES is None:
28
- return "Error: Transformation context not initialized"
29
-
30
- code_transformer = CodeTransformer(function=code)
31
- generated_output_path = code_transformer.export(INPUT_SDIF_PATH)
32
-
33
- comparisons = []
34
-
35
- if os.path.isdir(generated_output_path):
36
- # If it's a directory, compare each file with its corresponding target
37
- generated_files = os.listdir(generated_output_path)
38
-
39
- for (
40
- output_base_file,
41
- output_target_file_name,
42
- ) in OUTPUT_TARGET_FILES.items():
43
- if output_target_file_name in generated_files:
44
- generated_file_path = os.path.join(
45
- generated_output_path, output_target_file_name
46
- )
47
- comparator = get_comparator(output_target_file_name.split(".")[-1])
48
- comparison = comparator.compare(generated_file_path, output_base_file)
49
- comparisons.append(
50
- f"Comparison for {generated_file_path} [SOURCE] with {output_target_file_name} [TARGET]: {comparison}"
51
- )
52
- else:
53
- comparisons.append(
54
- f"Error: {output_target_file_name} not found in the generated output"
55
- )
56
- else:
57
- # If it's a single file, ensure there's only one target and compare
58
- if len(OUTPUT_TARGET_FILES) == 1:
59
- output_file = list(OUTPUT_TARGET_FILES.keys())[0]
60
- output_target_file_name = list(OUTPUT_TARGET_FILES.values())[0]
61
- comparator = get_comparator(output_file.split(".")[-1])
62
- comparison = comparator.compare(generated_output_path, output_file)
63
- comparisons.append(
64
- f"Comparison for {generated_output_path} [SOURCE] with {output_target_file_name} [TARGET]: {comparison}"
65
- )
66
- else:
67
- comparisons.append(
68
- "Error: Single output file generated but multiple target files expected"
69
- )
70
-
71
- return "\n".join(comparisons)
72
-
73
-
74
- class TransformationCodeBuilder(CodeBuilder):
75
- def __init__(self, output_example: Path | List[Path] | Dict[str, Path]):
76
- self.output_example = output_example
77
-
78
- def build(
79
- self,
80
- sdif: Path | SDIFDatabase,
81
- instructions: Optional[str] = None,
82
- ) -> str:
83
- pass
84
-
85
-
86
- class TransformationAsyncCodeBuilder(AsyncCodeBuilder):
87
- """This class is used to build a transformation code that will be used to transform a SDIF database into a set of files following the format of the given output files."""
88
-
89
- def __init__(
90
- self,
91
- mcp_server: MCPServerStdio,
92
- mcp_session: ClientSession,
93
- llm_model: str = "o3-mini",
94
- ):
95
- self.mcp_server = mcp_server
96
- self.mcp_session = mcp_session
97
- self.llm_model = llm_model
98
-
99
- async def build(
100
- self,
101
- sdif: Path,
102
- output_target_files: Dict[Union[str, Path], str] | List[Path],
103
- output_sdif: Optional[Path] = None,
104
- instructions: Optional[str] = None,
105
- ) -> str:
106
- global INPUT_SDIF_PATH, OUTPUT_TARGET_FILES
107
- INPUT_SDIF_PATH = Path(sdif)
108
-
109
- if isinstance(output_target_files, list):
110
- OUTPUT_TARGET_FILES = {file: file.name for file in output_target_files}
111
- else:
112
- OUTPUT_TARGET_FILES = output_target_files
113
-
114
- input_schema = await self.mcp_session.read_resource(f"schema://{sdif}")
115
- input_sample = await self.mcp_session.read_resource(f"sample://{sdif}")
116
-
117
- output_schema = await self.mcp_session.read_resource(f"schema://{output_sdif}")
118
- output_sample = await self.mcp_session.read_resource(f"sample://{output_sdif}")
119
- output_representation = {
120
- file: get_representer(file).represent(file)
121
- for file in list(OUTPUT_TARGET_FILES.keys())
122
- }
123
-
124
- prompt = await self.mcp_session.get_prompt(
125
- "create_transformation",
126
- arguments={
127
- "input_file": INPUT_SDIF_PATH.name,
128
- "input_schema": input_schema.contents[0].text,
129
- "input_sample": input_sample.contents[0].text,
130
- "output_files": str(list(OUTPUT_TARGET_FILES.values())),
131
- "output_schema": output_schema.contents[0].text,
132
- "output_sample": output_sample.contents[0].text,
133
- "output_representation": str(output_representation),
134
- },
135
- )
136
- agent = Agent(
137
- name="Transformation Builder",
138
- mcp_servers=[self.mcp_server],
139
- tools=[execute_transformation],
140
- model=self.llm_model,
141
- )
142
- result = await Runner.run(agent, prompt.messages[0].content.text)
143
- transformation_code = self.parse_code(result.final_output)
144
- return transformation_code
145
-
146
- def parse_code(self, code) -> str:
147
- match = re.search(r"```(?:python)?(.*?)```", code, re.DOTALL)
148
- if match:
149
- return match.group(1).strip()
150
- else:
151
- # Handle case where no code block is found
152
- return code.strip()
File without changes
File without changes
File without changes