pygeai 0.4.0b2__py3-none-any.whl → 0.4.0b4__py3-none-any.whl
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.
Potentially problematic release.
This version of pygeai might be problematic. Click here for more details.
- pygeai/assistant/rag/models.py +1 -1
- pygeai/lab/models.py +7 -3
- pygeai/lab/processes/mappers.py +2 -2
- pygeai/lab/tools/mappers.py +5 -5
- pygeai/tests/integration/chat/__init__.py +0 -0
- pygeai/tests/integration/chat/test_generate_image.py +162 -0
- pygeai/tests/integration/lab/agents/test_create_agent.py +9 -12
- pygeai/tests/integration/lab/agents/test_update_agent.py +6 -14
- pygeai/tests/integration/lab/tools/__init__.py +0 -0
- pygeai/tests/integration/lab/tools/test_create_tool.py +293 -0
- pygeai/tests/integration/lab/tools/test_delete_tool.py +87 -0
- pygeai/tests/integration/lab/tools/test_get_tool.py +91 -0
- pygeai/tests/integration/lab/tools/test_list_tools.py +38 -0
- pygeai/tests/snippets/lab/agents/create_agent_edge_case.py +48 -0
- pygeai/tests/snippets/lab/agents/create_agent_without_instructions.py +48 -0
- pygeai/tests/snippets/lab/tools/create_tool_edge_case.py +50 -0
- {pygeai-0.4.0b2.dist-info → pygeai-0.4.0b4.dist-info}/METADATA +4 -4
- {pygeai-0.4.0b2.dist-info → pygeai-0.4.0b4.dist-info}/RECORD +22 -12
- {pygeai-0.4.0b2.dist-info → pygeai-0.4.0b4.dist-info}/WHEEL +0 -0
- {pygeai-0.4.0b2.dist-info → pygeai-0.4.0b4.dist-info}/entry_points.txt +0 -0
- {pygeai-0.4.0b2.dist-info → pygeai-0.4.0b4.dist-info}/licenses/LICENSE +0 -0
- {pygeai-0.4.0b2.dist-info → pygeai-0.4.0b4.dist-info}/top_level.txt +0 -0
pygeai/assistant/rag/models.py
CHANGED
|
@@ -227,7 +227,7 @@ class SearchOptions(CustomBaseModel):
|
|
|
227
227
|
"ingestion": self.ingestion.to_dict() if self.ingestion else None,
|
|
228
228
|
"options": self.options,
|
|
229
229
|
"rerank": self.rerank,
|
|
230
|
-
"variables": self.variables.
|
|
230
|
+
"variables": self.variables.to_list() if self.variables else None,
|
|
231
231
|
"vectorStore": self.vector_store
|
|
232
232
|
}
|
|
233
233
|
return {k: v for k, v in result.items() if v is not None}
|
pygeai/lab/models.py
CHANGED
|
@@ -94,7 +94,7 @@ class Model(CustomBaseModel):
|
|
|
94
94
|
:param llm_config: Optional[LlmConfig] - Overrides default agent LLM settings.
|
|
95
95
|
:param prompt: Optional[dict] - A tailored prompt specific to this model.
|
|
96
96
|
"""
|
|
97
|
-
name: str = Field(
|
|
97
|
+
name: Optional[str] = Field(None, alias="name")
|
|
98
98
|
llm_config: Optional[LlmConfig] = Field(None, alias="llmConfig")
|
|
99
99
|
prompt: Optional[Dict[str, Any]] = Field(None, alias="prompt")
|
|
100
100
|
|
|
@@ -161,12 +161,13 @@ class Prompt(CustomBaseModel):
|
|
|
161
161
|
:param context: Optional[str] - Background context for the agent # NOT IMPLEMENTED YET
|
|
162
162
|
:param examples: List[PromptExample] - List of example input-output pairs.
|
|
163
163
|
"""
|
|
164
|
-
instructions: str = Field(
|
|
164
|
+
instructions: Optional[str] = Field(None, alias="instructions")
|
|
165
165
|
inputs: Optional[List[str]] = Field(None, alias="inputs")
|
|
166
166
|
outputs: Optional[List[PromptOutput]] = Field([], alias="outputs")
|
|
167
167
|
context: Optional[str] = Field(None, alias="context", description="Background context for the agent")
|
|
168
168
|
examples: Optional[List[PromptExample]] = Field(None, alias="examples")
|
|
169
169
|
|
|
170
|
+
'''
|
|
170
171
|
@field_validator("instructions")
|
|
171
172
|
@classmethod
|
|
172
173
|
def validate_instructions(cls, value: str) -> str:
|
|
@@ -174,6 +175,7 @@ class Prompt(CustomBaseModel):
|
|
|
174
175
|
raise ValueError("instructions cannot be blank")
|
|
175
176
|
|
|
176
177
|
return value
|
|
178
|
+
'''
|
|
177
179
|
|
|
178
180
|
@field_validator("outputs", mode="before")
|
|
179
181
|
@classmethod
|
|
@@ -687,7 +689,7 @@ class Tool(CustomBaseModel):
|
|
|
687
689
|
:param status: Optional[str] - Current status of the tool (e.g., "active"), defaults to None.
|
|
688
690
|
"""
|
|
689
691
|
name: str = Field(..., alias="name", description="The name of the tool")
|
|
690
|
-
description: str = Field(
|
|
692
|
+
description: Optional[str] = Field(None, alias="description", description="Description of the tool's purpose")
|
|
691
693
|
scope: str = Field("builtin", alias="scope", description="The scope of the tool (e.g., 'builtin', 'external', 'api')")
|
|
692
694
|
parameters: Optional[List[ToolParameter]] = Field(None, alias="parameters", description="List of parameters required by the tool")
|
|
693
695
|
access_scope: Optional[str] = Field(None, alias="accessScope", description="The access scope of the tool ('public' or 'private')")
|
|
@@ -730,6 +732,7 @@ class Tool(CustomBaseModel):
|
|
|
730
732
|
raise ValueError("public_name is required if access_scope is 'public'")
|
|
731
733
|
return self
|
|
732
734
|
|
|
735
|
+
'''
|
|
733
736
|
@model_validator(mode="after")
|
|
734
737
|
def validate_api_tool_requirements(self):
|
|
735
738
|
if self.scope == "api" and not (self.open_api or self.open_api_json):
|
|
@@ -739,6 +742,7 @@ class Tool(CustomBaseModel):
|
|
|
739
742
|
if len(param_keys) != len(set(param_keys)):
|
|
740
743
|
raise ValueError("All parameter keys must be unique within the tool")
|
|
741
744
|
return self
|
|
745
|
+
'''
|
|
742
746
|
|
|
743
747
|
@field_validator("parameters", mode="before")
|
|
744
748
|
@classmethod
|
pygeai/lab/processes/mappers.py
CHANGED
pygeai/lab/tools/mappers.py
CHANGED
|
@@ -37,8 +37,8 @@ class ToolMapper:
|
|
|
37
37
|
"""
|
|
38
38
|
return [
|
|
39
39
|
ToolMessage(
|
|
40
|
-
description=msg
|
|
41
|
-
type=msg
|
|
40
|
+
description=msg.get("description"),
|
|
41
|
+
type=msg.get("type")
|
|
42
42
|
)
|
|
43
43
|
for msg in messages_data
|
|
44
44
|
]
|
|
@@ -55,9 +55,9 @@ class ToolMapper:
|
|
|
55
55
|
"""
|
|
56
56
|
tool_data = data.get("tool", data)
|
|
57
57
|
|
|
58
|
-
name = tool_data
|
|
59
|
-
description = tool_data
|
|
60
|
-
scope = tool_data
|
|
58
|
+
name = tool_data.get("name")
|
|
59
|
+
description = tool_data.get("description")
|
|
60
|
+
scope = tool_data.get("scope")
|
|
61
61
|
parameter_data = tool_data.get("parameters")
|
|
62
62
|
parameters = cls._map_parameters(parameter_data) if parameter_data else None
|
|
63
63
|
|
|
File without changes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
from pygeai.chat.clients import ChatClient
|
|
3
|
+
|
|
4
|
+
chat_client: ChatClient
|
|
5
|
+
|
|
6
|
+
class TestChatGenerateImageIntegration(TestCase):
|
|
7
|
+
|
|
8
|
+
def setUp(self):
|
|
9
|
+
self.chat_client = ChatClient(alias="beta")
|
|
10
|
+
self.new_image = self.__load_image()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __load_image(self):
|
|
14
|
+
return {
|
|
15
|
+
"model": "openai/gpt-image-1",
|
|
16
|
+
"prompt": "generate an image of a futuristic city skyline at sunset",
|
|
17
|
+
"n": 1,
|
|
18
|
+
"quality": "high",
|
|
19
|
+
"size": "1024x1536"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __generate_image(self, image = None):
|
|
24
|
+
image = image if image is not None else self.new_image
|
|
25
|
+
return self.chat_client.generate_image(
|
|
26
|
+
model=image["model"],
|
|
27
|
+
prompt=image["prompt"],
|
|
28
|
+
n=image["n"],
|
|
29
|
+
quality=image["quality"],
|
|
30
|
+
size=image["size"],
|
|
31
|
+
aspect_ratio= image["aspect_ratio"] if "aspect_ratio" in image else None
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_generate_image(self):
|
|
36
|
+
created_image = self.__generate_image()
|
|
37
|
+
self.assertEqual(len(created_image["data"]), 1, "Expected an image to be generated")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_generate_image_invalid_model(self):
|
|
41
|
+
self.new_image["model"] = "openai/gpt-image-10",
|
|
42
|
+
created_image = self.__generate_image()
|
|
43
|
+
|
|
44
|
+
self.assertEqual(
|
|
45
|
+
created_image["error"]["code"], 400,
|
|
46
|
+
"Expected a 400 code for invalid model"
|
|
47
|
+
)
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
created_image["error"]["message"],
|
|
50
|
+
'Provider \'["openai\' does not exists.',
|
|
51
|
+
"Expected an error message when model does not exists"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_generate_image_no_model(self):
|
|
56
|
+
self.new_image["model"] = ""
|
|
57
|
+
created_image = self.__generate_image()
|
|
58
|
+
|
|
59
|
+
self.assertEqual(
|
|
60
|
+
created_image["error"]["code"], 400,
|
|
61
|
+
"Expected a 400 code for no model"
|
|
62
|
+
)
|
|
63
|
+
self.assertEqual(
|
|
64
|
+
created_image["error"]["message"],
|
|
65
|
+
"Invalid 'model' name. Must follow pattern {provider}/{modelName}",
|
|
66
|
+
"Expected an error message when no model is provided"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_generate_image_no_prompt(self):
|
|
71
|
+
self.new_image["prompt"] = ""
|
|
72
|
+
created_image = self.__generate_image()
|
|
73
|
+
|
|
74
|
+
self.assertEqual(
|
|
75
|
+
created_image["error"]["type"],
|
|
76
|
+
"invalid_request_error",
|
|
77
|
+
"Expected a 400 code for no model"
|
|
78
|
+
)
|
|
79
|
+
self.assertEqual(
|
|
80
|
+
created_image["error"]["param"], "prompt",
|
|
81
|
+
"Expected an error message when no model is provided"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_generate_image_specific_n(self):
|
|
86
|
+
self.new_image["n"] = 2
|
|
87
|
+
|
|
88
|
+
created_image = self.__generate_image()
|
|
89
|
+
self.assertEqual(len(created_image["data"]), 2, "Expected two images to be generated")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_generate_image_no_n(self):
|
|
93
|
+
self.new_image["n"] = None # default is 1
|
|
94
|
+
|
|
95
|
+
created_image = self.__generate_image()
|
|
96
|
+
self.assertEqual(len(created_image["data"]), 1, "Expected an image to be generated")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_generate_image_no_supported_n(self):
|
|
100
|
+
self.new_image["model"] = "openai/dall-e-3"
|
|
101
|
+
self.new_image["n"] = 5
|
|
102
|
+
|
|
103
|
+
created_image = self.__generate_image()
|
|
104
|
+
self.assertIn(
|
|
105
|
+
"Invalid 'n': integer above maximum value",
|
|
106
|
+
created_image["error"]["message"],
|
|
107
|
+
"Expected an error message when n is not supported by the model"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_generate_image_no_quality(self):
|
|
112
|
+
self.new_image["quality"] = ""
|
|
113
|
+
|
|
114
|
+
created_image = self.__generate_image()
|
|
115
|
+
self.assertIn(
|
|
116
|
+
"Invalid value: ''. Supported values are: 'low', 'medium', 'high', and 'auto'",
|
|
117
|
+
created_image["error"]["message"],
|
|
118
|
+
"Expected an error message when quality is not provided"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_generate_image_no_supported_quality(self):
|
|
123
|
+
self.new_image["model"] = "openai/dall-e-3"
|
|
124
|
+
|
|
125
|
+
created_image = self.__generate_image()
|
|
126
|
+
self.assertIn(
|
|
127
|
+
"Invalid value: 'high'. Supported values are: 'standard' and 'hd'",
|
|
128
|
+
created_image["error"]["message"],
|
|
129
|
+
"Expected an error message when quality is not supported by the model"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_generate_image_no_size(self):
|
|
134
|
+
self.new_image["size"] = ""
|
|
135
|
+
|
|
136
|
+
created_image = self.__generate_image()
|
|
137
|
+
self.assertIn(
|
|
138
|
+
"Invalid value: ''. Supported values are: '1024x1024', '1024x1536', '1536x1024', and 'auto'",
|
|
139
|
+
created_image["error"]["message"],
|
|
140
|
+
"Expected an error message when no size is provided"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_generate_image_no_supported_size(self):
|
|
145
|
+
self.new_image["size"] = 1024
|
|
146
|
+
self.new_image["quality"] = None
|
|
147
|
+
|
|
148
|
+
created_image = self.__generate_image()
|
|
149
|
+
self.assertIn(
|
|
150
|
+
"Invalid type for 'size': expected one of '1024x1024', '1024x1536', '1536x1024', or 'auto', but got an integer instead",
|
|
151
|
+
created_image["error"]["message"],
|
|
152
|
+
"Expected an error message when no size is provided"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_generate_image_with_aspect_ratio(self):
|
|
157
|
+
self.new_image["model"] = "vertex_ai/imagen-3.0-generate-001"
|
|
158
|
+
self.new_image["aspect_ratio"] = "4:3"
|
|
159
|
+
self.new_image["quality"] = None
|
|
160
|
+
|
|
161
|
+
created_image = self.__generate_image()
|
|
162
|
+
self.assertEqual(len(created_image["data"]), 1, "Expected an image to be generated")
|
|
@@ -287,21 +287,18 @@ class TestAILabCreateAgentIntegration(TestCase):
|
|
|
287
287
|
)
|
|
288
288
|
|
|
289
289
|
|
|
290
|
-
@unittest.skip("Agent is getting created regardless of the prompt instructions being empty")
|
|
291
290
|
def test_create_agent_no_prompt_instructions(self):
|
|
292
291
|
self.new_agent.agent_data.prompt.instructions = ""
|
|
293
|
-
|
|
294
|
-
self.__create_agent()
|
|
295
|
-
self.assertIn(
|
|
296
|
-
"instructions",
|
|
297
|
-
str(exception.exception),
|
|
298
|
-
"Expected a validation error about allowed values for instructions"
|
|
299
|
-
)
|
|
292
|
+
self.created_agent = self.__create_agent()
|
|
300
293
|
|
|
301
|
-
self.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
294
|
+
self.assertTrue(
|
|
295
|
+
isinstance(self.created_agent, Agent),
|
|
296
|
+
"Expected a created agent"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
self.assertIsNone(
|
|
300
|
+
self.created_agent.agent_data.prompt.instructions,
|
|
301
|
+
"Expected the created agent to not have prompt instructions"
|
|
305
302
|
)
|
|
306
303
|
|
|
307
304
|
|
|
@@ -207,28 +207,20 @@ class TestAILabUpdateAgentIntegration(TestCase):
|
|
|
207
207
|
f"Expected a validation error about allowed values for instructions when autopublish is {'enabled' if auto_publish else 'disabled'}"
|
|
208
208
|
)
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
# TODO: Change validation when API behavior is fixed
|
|
211
211
|
def test_update_agent_no_model(self):
|
|
212
212
|
test_params = [ True, False ]
|
|
213
213
|
self.agent_to_update.agent_data.models[0].name = ""
|
|
214
214
|
|
|
215
215
|
for auto_publish in test_params:
|
|
216
216
|
with self.subTest(input=auto_publish):
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
|
|
218
|
+
# If the agent is not published, the API returns a warning message for invalid model name. However, the sdk mapping is not returning it.
|
|
219
|
+
if auto_publish == False:
|
|
220
|
+
updated_agent = self.__update_agent(automatic_publish=auto_publish)
|
|
220
221
|
error_msg = str(exception.exception)
|
|
221
222
|
|
|
222
|
-
self.
|
|
223
|
-
"name",
|
|
224
|
-
error_msg,
|
|
225
|
-
"Expected a validation error about empty model name"
|
|
226
|
-
)
|
|
227
|
-
self.assertIn(
|
|
228
|
-
"Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]",
|
|
229
|
-
error_msg,
|
|
230
|
-
"Expected a validation error about empty model name"
|
|
231
|
-
)
|
|
223
|
+
self.assertTrue(self.isInstance(updated_agent), Agent)
|
|
232
224
|
else:
|
|
233
225
|
with self.assertRaises(APIError) as exception:
|
|
234
226
|
self.__update_agent(automatic_publish=auto_publish)
|
|
File without changes
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
import unittest
|
|
3
|
+
import uuid
|
|
4
|
+
from pygeai.lab.managers import AILabManager
|
|
5
|
+
from pygeai.lab.models import Agent, AgentData, Prompt, LlmConfig, Model, Tool, ToolParameter,Sampling, PromptExample, PromptOutput
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
from pygeai.core.common.exceptions import APIError, InvalidAPIResponseException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestAILabCreateToolIntegration(TestCase):
|
|
11
|
+
def setUp(self):
|
|
12
|
+
"""
|
|
13
|
+
Set up the test environment.
|
|
14
|
+
"""
|
|
15
|
+
self.ai_lab_manager = AILabManager(alias="beta")
|
|
16
|
+
self.new_tool = self.__load_tool()
|
|
17
|
+
self.created_tool: Agent = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def tearDown(self):
|
|
21
|
+
"""
|
|
22
|
+
Clean up after each test if necessary.
|
|
23
|
+
This can be used to delete the created tool
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(self.created_tool, Tool):
|
|
26
|
+
|
|
27
|
+
self.ai_lab_manager.delete_tool(self.created_tool.id)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def __load_tool(self):
|
|
31
|
+
return Tool(
|
|
32
|
+
name=str(uuid.uuid4()),
|
|
33
|
+
description="Tool created for sdk testing purposes",
|
|
34
|
+
scope="builtin",
|
|
35
|
+
openApi="https://raw.usercontent.com//openapi.json",
|
|
36
|
+
openApiJson={"openapi": "3.0.0","info": {"title": "Simple API overview","version": "2.0.0"}},
|
|
37
|
+
accessScope="private",
|
|
38
|
+
reportEvents="None",
|
|
39
|
+
parameters=[{"key": "param", "description": "param description", "type":"app", "value":"param value", "data_type": "String", "isRequired": False}],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __create_tool(self, tool=None, automatic_publish=False):
|
|
44
|
+
"""
|
|
45
|
+
Helper to create a tool using ai_lab_manager.
|
|
46
|
+
"""
|
|
47
|
+
return self.ai_lab_manager.create_tool(
|
|
48
|
+
tool=self.new_tool if tool is None else tool,
|
|
49
|
+
automatic_publish=automatic_publish
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_create_tool_full_data(self):
|
|
54
|
+
self.created_tool = self.__create_tool()
|
|
55
|
+
created_tool = self.created_tool
|
|
56
|
+
tool = self.new_tool
|
|
57
|
+
self.assertTrue(isinstance(created_tool, Tool), "Expected a created tool")
|
|
58
|
+
|
|
59
|
+
# Assert the main fields of the created tool
|
|
60
|
+
self.assertIsNotNone(created_tool.id)
|
|
61
|
+
self.assertEqual(created_tool.name, tool.name)
|
|
62
|
+
self.assertEqual(created_tool.description, tool.description)
|
|
63
|
+
self.assertEqual(created_tool.scope, tool.scope)
|
|
64
|
+
self.assertEqual(created_tool.access_scope, tool.access_scope)
|
|
65
|
+
self.assertEqual(created_tool.open_api, tool.open_api)
|
|
66
|
+
self.assertEqual(created_tool.status, "active")
|
|
67
|
+
|
|
68
|
+
# Assert agentData fields
|
|
69
|
+
tool_param = created_tool.parameters[0]
|
|
70
|
+
self.assertTrue(isinstance(tool_param, ToolParameter), "Expected parameters to be of type ToolParameter")
|
|
71
|
+
self.assertEqual(tool_param.key, tool.parameters[0].key)
|
|
72
|
+
self.assertEqual(tool_param.data_type, tool.parameters[0].data_type)
|
|
73
|
+
self.assertEqual(tool_param.description, tool.parameters[0].description)
|
|
74
|
+
self.assertEqual(tool_param.is_required, tool.parameters[0].is_required)
|
|
75
|
+
self.assertEqual(tool_param.type, tool.parameters[0].type)
|
|
76
|
+
self.assertEqual(tool_param.value, tool.parameters[0].value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_create_tool_minimum_required_data(self):
|
|
80
|
+
self.new_tool = Tool(
|
|
81
|
+
name=str(uuid.uuid4()),
|
|
82
|
+
description="Tool created for sdk testing purposes",
|
|
83
|
+
scope="builtin"
|
|
84
|
+
)
|
|
85
|
+
self.created_tool = self.__create_tool()
|
|
86
|
+
tool = self.new_tool
|
|
87
|
+
|
|
88
|
+
self.assertIsNotNone(self.created_tool.id)
|
|
89
|
+
self.assertEqual(self.created_tool.name, tool.name)
|
|
90
|
+
self.assertEqual(self.created_tool.description, tool.description)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_create_tool_without_required_data(self):
|
|
94
|
+
test_params = [ True, False ]
|
|
95
|
+
|
|
96
|
+
for auto_publish in test_params:
|
|
97
|
+
|
|
98
|
+
with self.subTest(input=auto_publish):
|
|
99
|
+
with self.assertRaises(ValidationError) as context:
|
|
100
|
+
self.new_tool = Tool(
|
|
101
|
+
name=str(uuid.uuid4())
|
|
102
|
+
)
|
|
103
|
+
self.__create_tool(automatic_publish=auto_publish)
|
|
104
|
+
|
|
105
|
+
self.assertIn("description", str(context.exception))
|
|
106
|
+
self.assertIn("Field required", str(context.exception))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_create_tool_no_name(self):
|
|
110
|
+
test_params = [ True, False ]
|
|
111
|
+
|
|
112
|
+
for auto_publish in test_params:
|
|
113
|
+
with self.subTest(input=auto_publish):
|
|
114
|
+
self.new_tool.name = ""
|
|
115
|
+
with self.assertRaises(APIError) as exception:
|
|
116
|
+
self.__create_tool(automatic_publish=auto_publish)
|
|
117
|
+
|
|
118
|
+
self.assertIn(
|
|
119
|
+
"Tool name cannot be empty.",
|
|
120
|
+
str(exception.exception),
|
|
121
|
+
f"Expected an error about the missing tool name with autopublish {'enabled' if auto_publish else 'disabled'}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_create_tool_duplicated_name(self):
|
|
126
|
+
test_params = [ True, False ]
|
|
127
|
+
|
|
128
|
+
for auto_publish in test_params:
|
|
129
|
+
|
|
130
|
+
with self.subTest(input=auto_publish):
|
|
131
|
+
self.new_tool.name = "sdk_project_gemini_tool"
|
|
132
|
+
with self.assertRaises(APIError) as exception:
|
|
133
|
+
self.__create_tool(automatic_publish=auto_publish)
|
|
134
|
+
self.assertIn(
|
|
135
|
+
"Tool already exists [name=sdk_project_gemini_tool]..",
|
|
136
|
+
str(exception.exception),
|
|
137
|
+
f"Expected an error about duplicated tool name with autopublish {'enabled' if auto_publish else 'disabled'}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_create_tool_invalid_name(self):
|
|
142
|
+
test_params = [ True, False ]
|
|
143
|
+
|
|
144
|
+
for auto_publish in test_params:
|
|
145
|
+
with self.subTest(input=auto_publish):
|
|
146
|
+
new_tool = self.__load_tool()
|
|
147
|
+
new_tool2 = self.__load_tool()
|
|
148
|
+
|
|
149
|
+
with self.assertRaises(APIError) as exception:
|
|
150
|
+
new_tool.name = f"{new_tool.name}:invalid"
|
|
151
|
+
self.__create_tool(tool=new_tool, automatic_publish=auto_publish)
|
|
152
|
+
self.assertIn(
|
|
153
|
+
"Invalid character in name (: is not allowed).",
|
|
154
|
+
str(exception.exception),
|
|
155
|
+
f"Expected an error about invalid character (:) in tool name with autopublish {'enabled' if auto_publish else 'disabled'}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
with self.assertRaises(APIError) as exception:
|
|
159
|
+
new_tool2.name = f"{new_tool2.name}/invalid"
|
|
160
|
+
self.__create_tool(tool=new_tool2, automatic_publish=auto_publish)
|
|
161
|
+
self.assertIn(
|
|
162
|
+
"Invalid character in name (/ is not allowed).",
|
|
163
|
+
str(exception.exception),
|
|
164
|
+
f"Expected an error about invalid character (/) in tool name with autopublish {'enabled' if auto_publish else 'disabled'}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_create_tool_invalid_access_scope(self):
|
|
169
|
+
self.new_tool.access_scope = "project"
|
|
170
|
+
with self.assertRaises(ValueError) as exc:
|
|
171
|
+
self.__create_tool()
|
|
172
|
+
self.assertEqual(
|
|
173
|
+
str(exc.exception),
|
|
174
|
+
"Access scope must be one of public, private.",
|
|
175
|
+
"Expected a ValueError exception for invalid access scope"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_create_tool_default_scope(self):
|
|
180
|
+
self.new_tool.access_scope = None
|
|
181
|
+
self.created_tool = self.__create_tool()
|
|
182
|
+
|
|
183
|
+
self.assertEqual(self.created_tool.access_scope, "private", "Expected the default access scope to be 'private' when not specified")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_create_tool_no_public_name(self):
|
|
187
|
+
test_params = [ True, False ]
|
|
188
|
+
|
|
189
|
+
for auto_publish in test_params:
|
|
190
|
+
|
|
191
|
+
with self.subTest(input=auto_publish):
|
|
192
|
+
self.new_tool.access_scope = "public"
|
|
193
|
+
self.new_tool.public_name = None
|
|
194
|
+
with self.assertRaises(APIError) as exception:
|
|
195
|
+
self.__create_tool(automatic_publish=auto_publish)
|
|
196
|
+
self.assertIn(
|
|
197
|
+
"Tool publicName is required for tools with accessScope=public.",
|
|
198
|
+
str(exception.exception),
|
|
199
|
+
f"Expected an error about missing publicName for public access scope with autopublish {'enabled' if auto_publish else 'disabled'}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_create_tool_invalid_public_name(self):
|
|
204
|
+
test_params = [ True, False ]
|
|
205
|
+
|
|
206
|
+
for auto_publish in test_params:
|
|
207
|
+
with self.subTest(input=auto_publish):
|
|
208
|
+
self.new_tool.access_scope = "public"
|
|
209
|
+
self.new_tool.public_name = "com.sdk.testing#" # Add invalid character to public name
|
|
210
|
+
with self.assertRaises(APIError) as exception:
|
|
211
|
+
self.__create_tool(automatic_publish=auto_publish)
|
|
212
|
+
|
|
213
|
+
self.assertIn(
|
|
214
|
+
"Invalid public name, it can only contain lowercase letters, numbers, periods (.), dashes (-), and underscores (_). Please remove any other characters.",
|
|
215
|
+
str(exception.exception),
|
|
216
|
+
f"The expected error about invalid publicName was not returned when autopublish is {'enabled' if auto_publish else 'disabled'}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_create_tool_duplicated_public_name(self):
|
|
221
|
+
test_params = [ True, False ]
|
|
222
|
+
|
|
223
|
+
# Set the scope as public and assign a public name
|
|
224
|
+
self.new_tool.access_scope = "public"
|
|
225
|
+
self.new_tool.public_name=f"public_{self.new_tool.name}"
|
|
226
|
+
self.created_tool = self.__create_tool()
|
|
227
|
+
|
|
228
|
+
for auto_publish in test_params:
|
|
229
|
+
with self.subTest(input=auto_publish):
|
|
230
|
+
|
|
231
|
+
# Create a new with the same public name of created_tool
|
|
232
|
+
duplicated_pn_tool = self.__load_tool()
|
|
233
|
+
duplicated_pn_tool.access_scope = "public"
|
|
234
|
+
duplicated_pn_tool.public_name = self.created_tool.public_name
|
|
235
|
+
|
|
236
|
+
with self.assertRaises(APIError) as exception:
|
|
237
|
+
self.__create_tool(tool=duplicated_pn_tool, automatic_publish=auto_publish)
|
|
238
|
+
self.assertIn(
|
|
239
|
+
f"Tool already exists [publicName={self.created_tool.public_name}].",
|
|
240
|
+
str(exception.exception),
|
|
241
|
+
f"Expected an error about the duplicated public name when autopublish is {'enabled' if auto_publish else 'disabled'}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_create_tool_api_scope_with_no_open_api(self):
|
|
246
|
+
self.new_tool.scope = "api"
|
|
247
|
+
with self.assertRaises(ValueError) as exception:
|
|
248
|
+
self.new_tool.openApi = ""
|
|
249
|
+
self.assertIn(
|
|
250
|
+
'"Tool" object has no field "openApi"',
|
|
251
|
+
str(exception.exception),
|
|
252
|
+
"Expected a validation error when openApi is not provided for api scope"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_create_tool_api_scope_with_no_open_api_json(self):
|
|
257
|
+
self.new_tool.scope = "api"
|
|
258
|
+
with self.assertRaises(ValueError) as exception:
|
|
259
|
+
self.new_tool.openApiJson = ""
|
|
260
|
+
|
|
261
|
+
self.assertIn(
|
|
262
|
+
'"Tool" object has no field "openApiJson"',
|
|
263
|
+
str(exception.exception),
|
|
264
|
+
"Expected a validation error when openApiJson is not provided for api scope"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_create_tool_invalid_scope(self):
|
|
269
|
+
test_params = [ True, False ]
|
|
270
|
+
|
|
271
|
+
for auto_publish in test_params:
|
|
272
|
+
with self.subTest(input=auto_publish):
|
|
273
|
+
|
|
274
|
+
with self.assertRaises(ValueError) as exception:
|
|
275
|
+
self.new_tool.scope = "source"
|
|
276
|
+
self.__create_tool()
|
|
277
|
+
self.assertIn(
|
|
278
|
+
'Scope must be one of builtin, external, api, proxied',
|
|
279
|
+
str(exception.exception),
|
|
280
|
+
"Expected a validation error about allowed values for instructions"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_create_tool_autopublish(self):
|
|
285
|
+
self.created_tool = self.__create_tool(automatic_publish=True)
|
|
286
|
+
self.assertFalse(self.created_tool.is_draft, "Expected the tool to be published automatically")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_create_tool_autopublish_private_scope(self):
|
|
290
|
+
self.new_tool.access_scope = "private"
|
|
291
|
+
|
|
292
|
+
self.created_tool = self.__create_tool(automatic_publish=True)
|
|
293
|
+
self.assertFalse(self.created_tool.is_draft, "Expected the tool to be published automatically even with private scope")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
import uuid
|
|
3
|
+
from pygeai.lab.managers import AILabManager
|
|
4
|
+
from pygeai.lab.models import Tool, AgentData, Prompt, LlmConfig, Model
|
|
5
|
+
from pygeai.core.common.exceptions import MissingRequirementException, InvalidAPIResponseException
|
|
6
|
+
|
|
7
|
+
ai_lab_manager: AILabManager
|
|
8
|
+
|
|
9
|
+
class TestAILabDeleteToolIntegration(TestCase):
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
self.ai_lab_manager = AILabManager(alias="beta")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __create_tool(self):
|
|
16
|
+
"""
|
|
17
|
+
Helper to create a tool
|
|
18
|
+
"""
|
|
19
|
+
tool = Tool(
|
|
20
|
+
name=str(uuid.uuid4()),
|
|
21
|
+
description="Agent that translates from any language to english.",
|
|
22
|
+
scope="builtin"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
return self.ai_lab_manager.create_tool(
|
|
27
|
+
tool=tool
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __delete_tool(self, tool_id: str = None, tool_name: str = None):
|
|
31
|
+
return self.ai_lab_manager.delete_tool(tool_id=tool_id, tool_name=tool_name)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_delete_tool_by_id(self):
|
|
35
|
+
created_tool = self.__create_tool()
|
|
36
|
+
deleted_tool = self.__delete_tool(tool_id=created_tool.id)
|
|
37
|
+
|
|
38
|
+
self.assertEqual(
|
|
39
|
+
deleted_tool.content,
|
|
40
|
+
"Tool deleted successfully",
|
|
41
|
+
"Expected confirmation message after deletion"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_delete_tool_by_name(self):
|
|
46
|
+
created_tool = self.__create_tool()
|
|
47
|
+
deleted_tool = self.__delete_tool(tool_name=created_tool.name)
|
|
48
|
+
|
|
49
|
+
self.assertEqual(
|
|
50
|
+
deleted_tool.content,
|
|
51
|
+
"Tool deleted successfully",
|
|
52
|
+
"Expected confirmation message after deletion"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_delete_tool_no_id_nor_name(self):
|
|
57
|
+
with self.assertRaises(MissingRequirementException) as exception:
|
|
58
|
+
self.__delete_tool()
|
|
59
|
+
self.assertIn(
|
|
60
|
+
"Either tool_id or tool_name must be provided",
|
|
61
|
+
str(exception.exception),
|
|
62
|
+
"Expected error message when neither tool_id nor tool_name is provided"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_delete_tool_invalid_id_valid_name(self):
|
|
67
|
+
invalid_id = "0026e53d-ea78-4cac-af9f-12650invalid"
|
|
68
|
+
created_tool = self.__create_tool()
|
|
69
|
+
with self.assertRaises(InvalidAPIResponseException) as exception:
|
|
70
|
+
self.__delete_tool(tool_name=created_tool.name, tool_id=invalid_id)
|
|
71
|
+
|
|
72
|
+
self.assertIn(
|
|
73
|
+
f"Tool not found [IdOrName= {invalid_id}].",
|
|
74
|
+
str(exception.exception),
|
|
75
|
+
"Expected error message for valid tool name and invalid tool id"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_delete_tool_invalid_name_valid_id(self):
|
|
80
|
+
created_tool = self.__create_tool()
|
|
81
|
+
deleted_tool = self.__delete_tool(tool_id=created_tool.id, tool_name="toolName")
|
|
82
|
+
|
|
83
|
+
self.assertEqual(
|
|
84
|
+
deleted_tool.content,
|
|
85
|
+
"Tool deleted successfully",
|
|
86
|
+
"Expected confirmation message after deletion"
|
|
87
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
import unittest
|
|
3
|
+
from pygeai.lab.managers import AILabManager
|
|
4
|
+
from pygeai.lab.models import Tool, FilterSettings
|
|
5
|
+
from pygeai.core.common.exceptions import APIError
|
|
6
|
+
import copy
|
|
7
|
+
|
|
8
|
+
ai_lab_manager: AILabManager
|
|
9
|
+
|
|
10
|
+
class TestAILabGetToolIntegration(TestCase):
|
|
11
|
+
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.ai_lab_manager = AILabManager(alias="beta")
|
|
14
|
+
self.tool_id = "e3e4d64f-ce52-467e-90a9-aa4d08425e82"
|
|
15
|
+
self.filter_settings = FilterSettings(
|
|
16
|
+
revision="0",
|
|
17
|
+
version="0"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def __get_tool(self, tool_id=None, filter_settings: FilterSettings = None):
|
|
21
|
+
return self.ai_lab_manager.get_tool(
|
|
22
|
+
tool_id=self.tool_id if tool_id is None else tool_id,
|
|
23
|
+
filter_settings=self.filter_settings if filter_settings is None else filter_settings
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def test_get_tool(self):
|
|
27
|
+
tool = self.__get_tool()
|
|
28
|
+
self.assertIsInstance(tool, Tool, "Expected a tool")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@unittest.skip("Skipped: Validate that when no tool_id is provided, the complete tool list is returned")
|
|
32
|
+
def test_get_tool_no_tool_id(self):
|
|
33
|
+
with self.assertRaises(Exception) as context:
|
|
34
|
+
self.__get_tool(tool_id="")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_get_tool_invalid_tool_id(self):
|
|
38
|
+
invalid_id = "0026e53d-ea78-4cac-af9f-12650invalid"
|
|
39
|
+
with self.assertRaises(APIError) as context:
|
|
40
|
+
self.__get_tool(tool_id=invalid_id)
|
|
41
|
+
self.assertIn(
|
|
42
|
+
f"Tool not found [IdOrName= {invalid_id}].",
|
|
43
|
+
str(context.exception),
|
|
44
|
+
"Expected an error for invalid tool id"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_get_tool_no_revision(self):
|
|
49
|
+
filter_settings = copy.deepcopy(self.filter_settings)
|
|
50
|
+
filter_settings.revision = None
|
|
51
|
+
tool = self.__get_tool(filter_settings=filter_settings)
|
|
52
|
+
|
|
53
|
+
self.assertIsInstance(tool, Tool, "Expected a tool")
|
|
54
|
+
self.assertGreaterEqual(tool.revision, 1, "Expected tool revision to be the latest")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_get_tool_by_revision(self):
|
|
58
|
+
filter_settings = copy.deepcopy(self.filter_settings)
|
|
59
|
+
filter_settings.revision = "6"
|
|
60
|
+
tool = self.__get_tool(filter_settings=filter_settings)
|
|
61
|
+
|
|
62
|
+
self.assertEqual(6, tool.revision, "Expected agent revision to be 6")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_get_tool_by_earlier_revision(self):
|
|
66
|
+
filter_settings = copy.deepcopy(self.filter_settings)
|
|
67
|
+
filter_settings.revision = "2"
|
|
68
|
+
with self.assertRaises(APIError) as context:
|
|
69
|
+
self.__get_tool(filter_settings=filter_settings)
|
|
70
|
+
self.assertIn(
|
|
71
|
+
f"Requested revision not found [revision={filter_settings.revision}].",
|
|
72
|
+
str(context.exception),
|
|
73
|
+
"Expected an error for revision not found"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
#TODO: The API is returning the version of the tool, but the sdk does not
|
|
77
|
+
def test_get_tool_no_version(self):
|
|
78
|
+
filter_settings = copy.deepcopy(self.filter_settings)
|
|
79
|
+
filter_settings.version = None
|
|
80
|
+
tool = self.__get_tool(filter_settings=filter_settings)
|
|
81
|
+
|
|
82
|
+
self.assertIsInstance(tool, Tool, "Expected a tool")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
#TODO: The API is returning the version of the tool, but the sdk does not
|
|
86
|
+
def test_get_tool_by_version(self):
|
|
87
|
+
filter_settings = copy.deepcopy(self.filter_settings)
|
|
88
|
+
filter_settings.version = "1"
|
|
89
|
+
tool = self.__get_tool(filter_settings=filter_settings)
|
|
90
|
+
|
|
91
|
+
self.assertIsInstance(tool, Tool, "Expected a tool")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
from pygeai.lab.managers import AILabManager
|
|
3
|
+
from pygeai.lab.models import ToolList, FilterSettings
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
ai_lab_manager: AILabManager
|
|
7
|
+
|
|
8
|
+
class TestAILabListToolsIntegration(TestCase):
|
|
9
|
+
|
|
10
|
+
def setUp(self):
|
|
11
|
+
self.ai_lab_manager = AILabManager(alias="beta")
|
|
12
|
+
self.filter_settings = FilterSettings(
|
|
13
|
+
allow_external=False,
|
|
14
|
+
allow_drafts=True,
|
|
15
|
+
access_scope="private"
|
|
16
|
+
)
|
|
17
|
+
""" list-tools or lt List tools
|
|
18
|
+
--project-id or --pid ID of the project
|
|
19
|
+
--id ID of the tool to filter by. Defaults to an empty string (no filtering).
|
|
20
|
+
--count Number of tools to retrieve. Defaults to '100'.
|
|
21
|
+
--access-scope Access scope of the tools, either "public" or "private". Defaults to "public".
|
|
22
|
+
--allow-drafts Whether to include draft tools. Defaults to 1 (True).
|
|
23
|
+
--scope Scope of the tools, must be 'builtin', 'external', or 'api'. Defaults to 'api'.
|
|
24
|
+
--allow-external Whether to include external tools. Defaults to 1 (True). """
|
|
25
|
+
|
|
26
|
+
def __list_tools(self, filter_settings: FilterSettings = None):
|
|
27
|
+
filter_settings = filter_settings if filter_settings != None else self.filter_settings
|
|
28
|
+
return self.ai_lab_manager.list_tools(filter_settings=filter_settings)
|
|
29
|
+
|
|
30
|
+
def test_private_list_tools(self):
|
|
31
|
+
result = self.__list_tools()
|
|
32
|
+
self.assertIsInstance(result, ToolList , "Expected a list of tools")
|
|
33
|
+
print(result)
|
|
34
|
+
for tool in result.tools:
|
|
35
|
+
self.assertTrue(tool.access_scope == "private", "Expected all tools to be private")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pygeai.lab.managers import AILabManager
|
|
2
|
+
from pygeai.lab.models import Agent, AgentData, Prompt, LlmConfig, Model, Sampling, PromptExample, PromptOutput
|
|
3
|
+
|
|
4
|
+
agent = Agent(
|
|
5
|
+
id="f64ba214-152b-4dd4-be0d-2920da415f5d",
|
|
6
|
+
status="active",
|
|
7
|
+
name="Private Translator V25",
|
|
8
|
+
access_scope="private",
|
|
9
|
+
public_name="com.genexus.geai.private_translator#",
|
|
10
|
+
job_description="Translates",
|
|
11
|
+
avatar_image="https://www.shareicon.net/data/128x128/2016/11/09/851442_logo_512x512.png",
|
|
12
|
+
description="Agent that translates from any language to english.",
|
|
13
|
+
is_draft=False,
|
|
14
|
+
is_readonly=False,
|
|
15
|
+
revision=1,
|
|
16
|
+
version=None,
|
|
17
|
+
agent_data=AgentData(
|
|
18
|
+
prompt=Prompt(
|
|
19
|
+
instructions="the user will provide a text, you must return the same text translated to english",
|
|
20
|
+
inputs=["text", "avoid slang indicator"],
|
|
21
|
+
outputs=[
|
|
22
|
+
PromptOutput(key="translated_text", description="translated text, with slang or not depending on the indication. in plain text."),
|
|
23
|
+
PromptOutput(key="summary", description="a summary in the original language of the text to be translated, also in plain text.")
|
|
24
|
+
],
|
|
25
|
+
examples=[
|
|
26
|
+
PromptExample(input_data="opitiiiis mundo [no-slang]", output='{"translated_text":"hello world","summary":"saludo"}'),
|
|
27
|
+
PromptExample(input_data="esto es una prueba pincheguey [keep-slang]", output='{"translated_text":"this is a test pal","summary":"prueba"}')
|
|
28
|
+
]
|
|
29
|
+
),
|
|
30
|
+
llm_config=LlmConfig(
|
|
31
|
+
max_tokens=5000,
|
|
32
|
+
timeout=0,
|
|
33
|
+
sampling=Sampling(temperature=0.5, top_k=0, top_p=0)
|
|
34
|
+
),
|
|
35
|
+
models=[Model(name="gpt-4-turbo-preview")]
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
manager = AILabManager()
|
|
41
|
+
result = manager.create_agent(
|
|
42
|
+
agent=agent,
|
|
43
|
+
automatic_publish=False
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
print(f"Agent: {agent.to_dict()}")
|
|
48
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pygeai.lab.managers import AILabManager
|
|
2
|
+
from pygeai.lab.models import Agent, AgentData, Prompt, LlmConfig, Model, Sampling, PromptExample, PromptOutput
|
|
3
|
+
|
|
4
|
+
agent = Agent(
|
|
5
|
+
id="f64ba214-152b-4dd4-be0d-2920da415f5d",
|
|
6
|
+
status="active",
|
|
7
|
+
name="Private Translator V25",
|
|
8
|
+
access_scope="private",
|
|
9
|
+
public_name="com.genexus.geai.private_translator_25",
|
|
10
|
+
job_description="Translates",
|
|
11
|
+
avatar_image="https://www.shareicon.net/data/128x128/2016/11/09/851442_logo_512x512.png",
|
|
12
|
+
description="Agent that translates from any language to english.",
|
|
13
|
+
is_draft=False,
|
|
14
|
+
is_readonly=False,
|
|
15
|
+
revision=1,
|
|
16
|
+
version=None,
|
|
17
|
+
agent_data=AgentData(
|
|
18
|
+
prompt=Prompt(
|
|
19
|
+
instructions="the user will provide a text, you must return the same text translated to english",
|
|
20
|
+
inputs=["text", "avoid slang indicator"],
|
|
21
|
+
outputs=[
|
|
22
|
+
PromptOutput(key="translated_text", description="translated text, with slang or not depending on the indication. in plain text."),
|
|
23
|
+
PromptOutput(key="summary", description="a summary in the original language of the text to be translated, also in plain text.")
|
|
24
|
+
],
|
|
25
|
+
examples=[
|
|
26
|
+
PromptExample(input_data="opitiiiis mundo [no-slang]", output='{"translated_text":"hello world","summary":"saludo"}'),
|
|
27
|
+
PromptExample(input_data="esto es una prueba pincheguey [keep-slang]", output='{"translated_text":"this is a test pal","summary":"prueba"}')
|
|
28
|
+
]
|
|
29
|
+
),
|
|
30
|
+
llm_config=LlmConfig(
|
|
31
|
+
max_tokens=5000,
|
|
32
|
+
timeout=0,
|
|
33
|
+
sampling=Sampling(temperature=0.5, top_k=0, top_p=0)
|
|
34
|
+
),
|
|
35
|
+
models=[Model(name="gpt-4-turbo-preview")]
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
manager = AILabManager()
|
|
41
|
+
result = manager.create_agent(
|
|
42
|
+
agent=agent,
|
|
43
|
+
automatic_publish=False
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
print(f"Agent: {agent.to_dict()}")
|
|
48
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from pygeai.lab.managers import AILabManager
|
|
2
|
+
from pygeai.lab.models import Tool, ToolParameter
|
|
3
|
+
|
|
4
|
+
parameters = [
|
|
5
|
+
ToolParameter(
|
|
6
|
+
key="input",
|
|
7
|
+
data_type="String",
|
|
8
|
+
description="some input that the tool needs.",
|
|
9
|
+
is_required=True
|
|
10
|
+
),
|
|
11
|
+
ToolParameter(
|
|
12
|
+
key="some_nonsensitive_id",
|
|
13
|
+
data_type="String",
|
|
14
|
+
description="Configuration that is static, in the sense that whenever the tool is used, the value for this parameter is configured here. The llm will not know about it.",
|
|
15
|
+
is_required=True,
|
|
16
|
+
type="config",
|
|
17
|
+
from_secret=False,
|
|
18
|
+
value="b001e30b4016001f5f76b9ae9215ac40"
|
|
19
|
+
),
|
|
20
|
+
ToolParameter(
|
|
21
|
+
key="api_token",
|
|
22
|
+
data_type="String",
|
|
23
|
+
description="Configuration that is static, but it is sensitive information . The value is stored in secret-manager",
|
|
24
|
+
is_required=True,
|
|
25
|
+
type="config",
|
|
26
|
+
value="0cd84dc7-f3f5-4a03-9288-cdfd8d72fde1"
|
|
27
|
+
)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
tool = Tool(
|
|
31
|
+
name="sample_tool_v5",
|
|
32
|
+
access_scope="private",
|
|
33
|
+
public_name="sample.tool.test#",
|
|
34
|
+
description="a builtin tool that does something but really does nothing cos it does not exist.",
|
|
35
|
+
scope="builtin",
|
|
36
|
+
parameters=parameters
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
manager = AILabManager()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
result = manager.create_tool(
|
|
44
|
+
tool=tool,
|
|
45
|
+
automatic_publish=False
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
print(f"Created tool: {result.name}, ID: {result.id}")
|
|
49
|
+
print(f"Description: {result.description}")
|
|
50
|
+
print(f"Messages: {result.messages}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygeai
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.0b4
|
|
4
4
|
Summary: Software Development Kit to interact with Globant Enterprise AI.
|
|
5
5
|
Author-email: Globant <geai-sdk@globant.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -41,11 +41,11 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
PyGEAI is a Software Development Kit (SDK) for interacting with [Globant Enterprise AI](https://wiki.genexus.com/enterprise-ai/wiki?8,Table+of+contents%3AEnterprise+AI). It comprises libraries, tools, code samples, and documentation to simplify your experience with the platform.
|
|
43
43
|
|
|
44
|
-
##
|
|
44
|
+
## Terms and conditions
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
By using the Python SDK to interact with Globant Enterprise AI, you agree with the following Terms and Conditions:
|
|
47
47
|
|
|
48
|
-
[
|
|
48
|
+
[Terms and Conditions](https://www.globant.com/enterprise-ai/terms-of-use)
|
|
49
49
|
|
|
50
50
|
## Compatibility
|
|
51
51
|
This package is compatible with the Globant Enterprise AI release from June 2025.
|
|
@@ -16,7 +16,7 @@ pygeai/assistant/rag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
16
16
|
pygeai/assistant/rag/clients.py,sha256=9a8loVLRnVkA3nHvvOpbdUEhy_TEnHm1rhdBYrBVABE,15893
|
|
17
17
|
pygeai/assistant/rag/endpoints.py,sha256=7YlHIvAYj3-xsCWtVMDYobxXbAO0lCo9yJdOrQxwCrQ,1145
|
|
18
18
|
pygeai/assistant/rag/mappers.py,sha256=n3aeNXqz_7zq_JWq5wJfeNX1kvV3arOxAoUsqRYOZsc,8645
|
|
19
|
-
pygeai/assistant/rag/models.py,sha256=
|
|
19
|
+
pygeai/assistant/rag/models.py,sha256=g5UiHuRjobgU1WgUMxeBzXykxgJ5q7eb_YY8qDciNvw,15732
|
|
20
20
|
pygeai/assistant/rag/responses.py,sha256=fY97ibsCVLQ3Ssnjuvj-JeA883WqjOw7ZdxbpQp_B1E,196
|
|
21
21
|
pygeai/chat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
pygeai/chat/clients.py,sha256=QEyeTIPxp6xXKAEkE_XkjIxZDnaH808GKhIYr7ulrSA,10785
|
|
@@ -144,7 +144,7 @@ pygeai/health/endpoints.py,sha256=UAzMcqSXZtMj4r8M8B7a_a5LT6X_jMFNsCTvcsjNTYA,71
|
|
|
144
144
|
pygeai/lab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
145
145
|
pygeai/lab/constants.py,sha256=ddgDnXP4GD0woi-FUJaJXzaWS3H6zmDN0B-v8utM95Q,170
|
|
146
146
|
pygeai/lab/managers.py,sha256=9wV6SipzsIwFP9SXsKqZ0X5x6KbUuo6iCxPZF4zNGj4,72714
|
|
147
|
-
pygeai/lab/models.py,sha256=
|
|
147
|
+
pygeai/lab/models.py,sha256=1m41gSqpXZVO9AcPVxzlsC-TgxZcCsgGUbpN5zoDMjU,71451
|
|
148
148
|
pygeai/lab/runners.py,sha256=-uaCPHpFyiKtVOxlEjPjAc9h-onSdGAcYJ5IAZPqlb0,4147
|
|
149
149
|
pygeai/lab/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
150
150
|
pygeai/lab/agents/clients.py,sha256=2SYNjyD9LZaOUxmTcrwzqcHwW7K1wAmYwen_u0G-RfU,22659
|
|
@@ -153,7 +153,7 @@ pygeai/lab/agents/mappers.py,sha256=K6rxsO2Nq6GglmCUmyDKUNmzTG8HRbCelap6qaVKXQw,
|
|
|
153
153
|
pygeai/lab/processes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
154
154
|
pygeai/lab/processes/clients.py,sha256=C1YYJE7c5qJSZUIDKzo5kK-LOZP6jbdrG01aE8zoEYg,51403
|
|
155
155
|
pygeai/lab/processes/endpoints.py,sha256=nFIEcNP22xe4j6URI6KcwTh7h-xgYjYYuHT6PDPiO3I,2100
|
|
156
|
-
pygeai/lab/processes/mappers.py,sha256=
|
|
156
|
+
pygeai/lab/processes/mappers.py,sha256=YOWcVKdcJmLMAq-f3qevzqQ8L_hjb0_jVXBdCHutpzk,15815
|
|
157
157
|
pygeai/lab/spec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
158
158
|
pygeai/lab/spec/loader.py,sha256=Dq9MhLqFwF4RPdBBaqKPGqt43-PrNlsHpe-NXe4S0qQ,709
|
|
159
159
|
pygeai/lab/spec/parsers.py,sha256=oG7tY-GylweRxpvtCl3p53t0IoTX3UZFiB77x__3Qp8,646
|
|
@@ -164,7 +164,7 @@ pygeai/lab/strategies/mappers.py,sha256=6C_jubAVXMKLGQy5NUD0OX7SlrU2mLe2QsgzeJ1-
|
|
|
164
164
|
pygeai/lab/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
165
165
|
pygeai/lab/tools/clients.py,sha256=TfgociWyvzao3B6g2VfjAb6FmXp1YqmJRG2TT7RwOic,27441
|
|
166
166
|
pygeai/lab/tools/endpoints.py,sha256=HiGoMs4OVeCgH7EAERTtifFPl53NryA1Awh7D6AO8bA,699
|
|
167
|
-
pygeai/lab/tools/mappers.py,sha256=
|
|
167
|
+
pygeai/lab/tools/mappers.py,sha256=bYi5k36h0k4mCvOnV-r8YOHKz0U9P0mH21GNs20w2eM,4998
|
|
168
168
|
pygeai/man/__init__.py,sha256=gqGI92vUPt6RPweoWX3mTUYPWNDlm6aGUjQOnYXqthk,53
|
|
169
169
|
pygeai/man/man1/__init__.py,sha256=CFvES6cP_sbhgpm-I-QSbPC1f7Bw7cFsMW2-sxm4FtM,54
|
|
170
170
|
pygeai/man/man1/geai-proxy.1,sha256=N5jtjzS5dB3JjAkG0Rw8EBzhC6Jgoy6zbS7XDgcE4EA,6735
|
|
@@ -268,15 +268,22 @@ pygeai/tests/gam/test_clients.py,sha256=vNz-4ux0cubztTY-_fEPWEoMCt5VAmZLecd0V-sE
|
|
|
268
268
|
pygeai/tests/health/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
269
269
|
pygeai/tests/health/test_clients.py,sha256=kfakkZHFMfo2IAN-PzmtMGmgR4iNiN1RpRopI--0qHI,1525
|
|
270
270
|
pygeai/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
271
|
+
pygeai/tests/integration/chat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
272
|
+
pygeai/tests/integration/chat/test_generate_image.py,sha256=byCQQK6dIy68yPAhAa66bh7N0Xz5WnKSClx1vaIIzZA,5431
|
|
271
273
|
pygeai/tests/integration/lab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
272
274
|
pygeai/tests/integration/lab/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
273
275
|
pygeai/tests/integration/lab/agents/test_agents_list.py,sha256=F2KUCdeiaBC3dn8ARNWqSz_kJcRyA0HC1nquhamN35Q,4187
|
|
274
|
-
pygeai/tests/integration/lab/agents/test_create_agent.py,sha256=
|
|
276
|
+
pygeai/tests/integration/lab/agents/test_create_agent.py,sha256=yL3owPJP0RoHSy3lEAdH06LJXqxgt3f2l2plk75d4cc,14206
|
|
275
277
|
pygeai/tests/integration/lab/agents/test_create_sharing_link.py,sha256=y-e8Q_TfuLz7XXMRERSKA_-OQJUMBIsJcK0lQ0Oh858,2467
|
|
276
278
|
pygeai/tests/integration/lab/agents/test_delete_agent.py,sha256=sb3RfoZJdzQvcVdNcXY2C2FO3yY1ZNiAZ_6Ay6f331E,2524
|
|
277
279
|
pygeai/tests/integration/lab/agents/test_get_agent.py,sha256=oW1F6SENvhL9jZC021Rj-f_Xek2DSTx3SsZBr3YT6Hk,3666
|
|
278
280
|
pygeai/tests/integration/lab/agents/test_publish_agent_revision.py,sha256=4vpuAVBenLCyWvaKTA2PQVLn_e-abGDscngUzZm3dMs,5284
|
|
279
|
-
pygeai/tests/integration/lab/agents/test_update_agent.py,sha256=
|
|
281
|
+
pygeai/tests/integration/lab/agents/test_update_agent.py,sha256=1seho_GOtOHik_YJ9GPA4McSZorohhFRvuzY7UYXbxo,11726
|
|
282
|
+
pygeai/tests/integration/lab/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
283
|
+
pygeai/tests/integration/lab/tools/test_create_tool.py,sha256=G74P0_DUMpfCH3HBvImpUKD_aMHT8V6uPyVzo2V81lg,12479
|
|
284
|
+
pygeai/tests/integration/lab/tools/test_delete_tool.py,sha256=wy979nZh8ERd-k3jhJTjHqG4wxWE4sx-r4yn2nBc7Aw,2913
|
|
285
|
+
pygeai/tests/integration/lab/tools/test_get_tool.py,sha256=3fVDQlklmvOUgYDp0ATv5RqRmApgD4Qw_YGqjBOaOOo,3437
|
|
286
|
+
pygeai/tests/integration/lab/tools/test_list_tools.py,sha256=afJ-Y11uCJEvfzs0FzdjExqksO3PswglWHEg2_nBJ-4,1725
|
|
280
287
|
pygeai/tests/lab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
281
288
|
pygeai/tests/lab/test_managers.py,sha256=AsOAvyCkRpbskEy214aV2TwrqilWH6bxOiTWDOb1twQ,29778
|
|
282
289
|
pygeai/tests/lab/test_mappers.py,sha256=2cLSggf168XWFpeZeBR7uJ-8C32TKb7qA91i_9fr_b0,11409
|
|
@@ -368,6 +375,8 @@ pygeai/tests/snippets/lab/runner_1.py,sha256=QD92MvC22wpWj6YyrSgpp46EcL0ciac2x1z
|
|
|
368
375
|
pygeai/tests/snippets/lab/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
369
376
|
pygeai/tests/snippets/lab/agents/create_agent.py,sha256=EVyfQzDST9A9KaM0ToZJKlJ7yCbfhpSVkVK_MaUVQPw,1875
|
|
370
377
|
pygeai/tests/snippets/lab/agents/create_agent_2.py,sha256=jd7HKhle_c0S0vI80AejOyLaNqBWkILlRF_znzyCGcQ,1879
|
|
378
|
+
pygeai/tests/snippets/lab/agents/create_agent_edge_case.py,sha256=8dA9giNdsHjFZsIWTlBFk8e1QS3YtbZxklsVu0ZrDrk,1877
|
|
379
|
+
pygeai/tests/snippets/lab/agents/create_agent_without_instructions.py,sha256=jd7HKhle_c0S0vI80AejOyLaNqBWkILlRF_znzyCGcQ,1879
|
|
371
380
|
pygeai/tests/snippets/lab/agents/delete_agent.py,sha256=GfDX667_V3tZMz3vjsbrxoFZggzpwjZYH_PVO2Qjw5s,269
|
|
372
381
|
pygeai/tests/snippets/lab/agents/get_agent.py,sha256=bcqloJHwmNsFjEfri6QIRaTuHzwLtfEqIQPIC5pdkWQ,516
|
|
373
382
|
pygeai/tests/snippets/lab/agents/get_sharing_link.py,sha256=2mYPwMgFFbqzmWZEBrFYSjdZscVyjJYCs4A3L0x8Sr8,355
|
|
@@ -394,6 +403,7 @@ pygeai/tests/snippets/lab/strategies/list_reasoning_strategies.py,sha256=4pqsW16
|
|
|
394
403
|
pygeai/tests/snippets/lab/strategies/update_reasoning_strategy.py,sha256=OIoHNkdnXbC9GacPgXUG1jKlVizVtWfRI-E8_3xF5b0,889
|
|
395
404
|
pygeai/tests/snippets/lab/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
396
405
|
pygeai/tests/snippets/lab/tools/create_tool.py,sha256=dreldR8QIu9Q9tjOG2hkD6b8KZa_VrwSfSnCrzPgQLA,1400
|
|
406
|
+
pygeai/tests/snippets/lab/tools/create_tool_edge_case.py,sha256=8w4mvoRTMTyc70yYm2bgV2dr_Rh5QpPJR8VoqX-eY-s,1465
|
|
397
407
|
pygeai/tests/snippets/lab/tools/delete_tool.py,sha256=pnIkYvdP7X7Gx79AMK5MSVliIXdHSpyVwRhH3kgi7ys,452
|
|
398
408
|
pygeai/tests/snippets/lab/tools/get_parameter.py,sha256=dXdlHhoWxzZIYdsvHKnLLT5Vff2Tip46XCoOo-B8Gf0,490
|
|
399
409
|
pygeai/tests/snippets/lab/tools/get_tool.py,sha256=-fkKAE6nflwtLY_Lf6itZJyx_9aanFp-TSDHUzub1AM,477
|
|
@@ -482,9 +492,9 @@ pygeai/vendor/a2a/utils/helpers.py,sha256=6Tbd8SVfXvdNEk6WYmLOjrAxkzFf1aIg8dkFfB
|
|
|
482
492
|
pygeai/vendor/a2a/utils/message.py,sha256=gc_EKO69CJ4HkR76IFgsy-kENJz1dn7CfSgWJWvt-gs,2197
|
|
483
493
|
pygeai/vendor/a2a/utils/task.py,sha256=BYRA_L1HpoUGJAVlyHML0lCM9Awhf2Ovjj7oPFXKbh0,1647
|
|
484
494
|
pygeai/vendor/a2a/utils/telemetry.py,sha256=VvSp1Ztqaobkmq9-3sNhhPEilJS32-JTSfKzegkj6FU,10861
|
|
485
|
-
pygeai-0.4.
|
|
486
|
-
pygeai-0.4.
|
|
487
|
-
pygeai-0.4.
|
|
488
|
-
pygeai-0.4.
|
|
489
|
-
pygeai-0.4.
|
|
490
|
-
pygeai-0.4.
|
|
495
|
+
pygeai-0.4.0b4.dist-info/licenses/LICENSE,sha256=eHfqo7-AWS8cMq0cg03lq7owsLeCmZA-xS5L0kuHnl8,1474
|
|
496
|
+
pygeai-0.4.0b4.dist-info/METADATA,sha256=plyocQ_kqcryzOcFS26Ontgdg5bv5yMrbcitm3bsOcQ,6940
|
|
497
|
+
pygeai-0.4.0b4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
498
|
+
pygeai-0.4.0b4.dist-info/entry_points.txt,sha256=OAmwuXVCQBTCE3HeVegVd37hbhCcp9TPahvdrCuMYWw,178
|
|
499
|
+
pygeai-0.4.0b4.dist-info/top_level.txt,sha256=bJFwp2tURmCfB94yXDF7ylvdSJXFDDJsyUOb-7PJgwc,7
|
|
500
|
+
pygeai-0.4.0b4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|