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