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.
- {satif_ai-0.2.2 → satif_ai-0.2.4}/PKG-INFO +1 -1
- {satif_ai-0.2.2 → satif_ai-0.2.4}/pyproject.toml +2 -2
- satif_ai-0.2.4/satif_ai/code_builders/transformation.py +229 -0
- satif_ai-0.2.2/satif_ai/code_builders/transformation.py +0 -152
- {satif_ai-0.2.2 → satif_ai-0.2.4}/LICENSE +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/README.md +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/__init__.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/adapters/__init__.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/adapters/tidy.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/code_builders/__init__.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/code_builders/adaptation.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/plot_builders/__init__.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/plot_builders/agent.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/plot_builders/prompt.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/plot_builders/tool.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/standardizers/__init__.py +0 -0
- {satif_ai-0.2.2 → satif_ai-0.2.4}/satif_ai/standardizers/ai_csv.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "satif-ai"
|
3
|
-
version = "0.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
|
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
|