oskaragent 0.1.38a0__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.
@@ -0,0 +1,328 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field, asdict, InitVar
4
+ from typing import Any, Optional
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class AgentConfig:
9
+ # region Public configuration fields
10
+ agent_id: Optional[str] = None
11
+ agent_name: str = "Oskar"
12
+ model: str = "gpt-5.2"
13
+ model_settings: Optional[dict[str, Any]] = None
14
+ system_prompt: Optional[str] = None
15
+ description: Optional[str] = 'Sou um agente de Inteligência Artificial.'
16
+ tools_names: list[str] = field(default_factory=list)
17
+
18
+ """
19
+ Exemplo de ferramentas personalizadas:
20
+ [
21
+ {
22
+ 'tool': 'GetOpportunitySummary',
23
+ 'custom_tool': ask_to_agent_tool,
24
+ 'description': 'Prepara um sumário de uma oportunidade. Para usar essa ferramenta submeta um prompt solicitando o sumário, incluindo código da oportunidade.',
25
+ 'agent_name': 'opportunity_specialist',
26
+ }
27
+ ]
28
+ """
29
+ custom_tools: Optional[list[dict[str, Any]]] = None
30
+
31
+ """
32
+ Exemplo de bases de conhecimento:
33
+ [
34
+ {
35
+ "name": "psicologia",
36
+ "folder": "./sources/vectorstore"
37
+ }
38
+ ]
39
+ """
40
+ knowledge_base: Optional[list[dict[str, Any]]] = None
41
+
42
+ """
43
+ Exemplo de arquivos e bancos de dados:
44
+ [
45
+ {
46
+ "name": "basileia",
47
+ "description": "Dados de temperatura da cidade de Basileia",
48
+ "pathname": "./sources/Basileia.csv"
49
+ }
50
+ ]
51
+ """
52
+ working_files: Optional[list[dict[str, Any]]] = None
53
+
54
+ """
55
+ Exemplo de arquivos e bancos de dados:
56
+ [
57
+ {
58
+ "name": "Chamados",
59
+ "description": "Informações sobre chamados de suporte técnico",
60
+ "connection_string": "Driver={SQL Server};Server=MYSERVER;Database=DB_ITSM;Uid=user;Pwd=pwd",
61
+ "query": "select * from vwTickets",
62
+ }
63
+ ]
64
+ """
65
+ working_databases: Optional[list[dict[str, Any]]] = None
66
+
67
+ # json_config is accepted during initialization but not stored
68
+ json_config: InitVar[Optional[dict[str, Any]]] = None
69
+ # endregion
70
+
71
+
72
+ def __post_init__(self, json_config: Optional[dict[str, Any]]):
73
+ """Finalize dataclass initialization by resolving defaults and validation.
74
+
75
+ Args:
76
+ json_config (Optional[dict[str, Any]]): Configuração em formato dict usada para hidratar o agente.
77
+
78
+ Returns:
79
+ None: O método ajusta atributos internos sem produzir valor de retorno.
80
+ """
81
+ # If a JSON config was provided, apply it first
82
+ if json_config:
83
+ self.restore_from_json(json_config)
84
+
85
+ # Fill sensible defaults consistent with previous behavior
86
+ if not self.agent_name:
87
+ self.agent_name = "oskaragent"
88
+
89
+ if not self.model:
90
+ self.model = "gpt-5.2"
91
+
92
+ if self.system_prompt is None:
93
+ self.system_prompt = f"Você é um assistente prestativo. Seu nome é {self.agent_name}."
94
+
95
+ if self.tools_names is None:
96
+ # Keep a list even if None was explicitly provided
97
+ self.tools_names = []
98
+
99
+ if self.agent_id is None:
100
+ # Default agent_id to agent_name
101
+ self.agent_id = self.agent_name
102
+
103
+ # Final type and value checks
104
+ self._validate()
105
+
106
+
107
+ def to_json(self) -> dict[str, Any]:
108
+ """Serialize configuration values into a JSON-friendly dictionary.
109
+
110
+ Returns:
111
+ dict[str, Any]: Dicionário contendo os campos públicos da configuração do agente.
112
+ """
113
+ data = asdict(self)
114
+ # `json_config` is an InitVar and not present; ensure consistency
115
+ return data
116
+
117
+
118
+ def restore_from_json(self, agent_config: dict[str, Any]) -> None:
119
+ """Load configuration values from a plain dictionary.
120
+
121
+ Args:
122
+ agent_config (dict[str, Any]): Dados serializados utilizados para atualizar a configuração.
123
+
124
+ Returns:
125
+ None: O método modifica a instância atual in-place.
126
+ """
127
+ if not isinstance(agent_config, dict):
128
+ return
129
+
130
+ # Apply values only if present in the dict (do not clear existing ones)
131
+ if (v := agent_config.get("agent_id")) is not None:
132
+ self.agent_id = v
133
+ if (v := agent_config.get("agent_name")) is not None:
134
+ self.agent_name = v
135
+ if (v := agent_config.get("model")) is not None:
136
+ self.model = v
137
+ if (v := agent_config.get("model_settings")) is not None:
138
+ self.model_settings = v
139
+ if (v := agent_config.get("system_prompt")) is not None:
140
+ self.system_prompt = v
141
+ if (v := agent_config.get("description")) is not None:
142
+ self.description = v
143
+ if (v := agent_config.get("tools_names")) is not None:
144
+ self.tools_names = v
145
+ if (v := agent_config.get("custom_tools")) is not None:
146
+ self.custom_tools = v
147
+ if (v := agent_config.get("rag")) is not None:
148
+ self.knowledge_base = v
149
+ if (v := agent_config.get("working_files")) is not None:
150
+ self.working_files = v
151
+ if (v := agent_config.get("working_databases")) is not None:
152
+ self.working_databases = v
153
+ # endregion
154
+
155
+
156
+ def _coerce_list_of_str(value: Any) -> Optional[list[str]]:
157
+ """Normalize arbitrary input into a list of strings when possible.
158
+
159
+ Args:
160
+ value (Any): Valor original que pode representar uma lista ou elemento único.
161
+
162
+ Returns:
163
+ Optional[list[str]]: Lista de strings convertidas ou `None` se a conversão não for viável.
164
+ """
165
+ if value is None:
166
+ return None
167
+ if isinstance(value, list):
168
+ return [str(v) for v in value]
169
+ # Single string -> list
170
+ if isinstance(value, str):
171
+ return [value]
172
+ # Fallback: try to iterate
173
+ try:
174
+ return [str(v) for v in value]
175
+ except Exception:
176
+ return None
177
+
178
+
179
+ def _default_system_prompt(agent_name: str) -> str:
180
+ """Generate the default system prompt text for the agent.
181
+
182
+ Args:
183
+ agent_name (str): Nome do agente usado na saudação.
184
+
185
+ Returns:
186
+ str: Texto de prompt padrão a ser usado pelo agente.
187
+ """
188
+ return f"Você é um assistente prestativo. Seu nome é {agent_name}."
189
+
190
+
191
+ def _ensure_list(value: Optional[list[str]]) -> list[str]:
192
+ """Ensure the provided value is a list, returning an empty list otherwise.
193
+
194
+ Args:
195
+ value (Optional[list[str]]): Lista opcional de strings.
196
+
197
+ Returns:
198
+ list[str]: Lista original ou uma nova lista vazia.
199
+ """
200
+ return value if isinstance(value, list) else []
201
+
202
+
203
+ def _ensure_str_or_none(value: Any) -> Optional[str]:
204
+ """Convert a value to string when available, preserving `None`.
205
+
206
+ Args:
207
+ value (Any): Valor a ser convertido.
208
+
209
+ Returns:
210
+ Optional[str]: String convertida ou `None` quando o valor é nulo.
211
+ """
212
+ if value is None:
213
+ return None
214
+ return str(value)
215
+
216
+
217
+ def _ensure_dict_or_none(value: Any) -> Optional[dict[str, Any]]:
218
+ """Attempt to coerce input to a dictionary, preserving `None`.
219
+
220
+ Args:
221
+ value (Any): Estrutura a ser convertida para `dict`.
222
+
223
+ Returns:
224
+ Optional[dict[str, Any]]: Dicionário convertido ou `None` quando indisponível.
225
+ """
226
+ if value is None:
227
+ return None
228
+ return dict(value)
229
+
230
+
231
+ def _ensure_int(value: Any, default: int) -> int:
232
+ """Cast a value to integer, returning a fallback when conversion fails.
233
+
234
+ Args:
235
+ value (Any): Valor potencialmente numérico.
236
+ default (int): Valor padrão utilizado em caso de falha.
237
+
238
+ Returns:
239
+ int: Inteiro convertido ou o valor padrão.
240
+ """
241
+ try:
242
+ return int(value)
243
+ except Exception:
244
+ return default
245
+
246
+
247
+ def _validate_model_settings(value: Any) -> Optional[dict[str, Any]]:
248
+ """Validate that model settings are dictionary-like.
249
+
250
+ Args:
251
+ value (Any): Estrutura de configuração fornecida.
252
+
253
+ Returns:
254
+ Optional[dict[str, Any]]: Dicionário com as configurações ou `None` quando inválido.
255
+ """
256
+ if value is None:
257
+ return None
258
+ try:
259
+ return dict(value)
260
+ except Exception:
261
+ return None
262
+
263
+
264
+ def _validate_working_lists(cfg: AgentConfig) -> None:
265
+ """Normalize list-based fields for the provided configuration object.
266
+
267
+ Args:
268
+ cfg (AgentConfig): Instância que será atualizada in-place.
269
+
270
+ Returns:
271
+ None: Apenas ajusta os atributos `tools_names`, `working_files` e `working_dbs`.
272
+ """
273
+ cfg.tools_names = _ensure_list(_coerce_list_of_str(cfg.tools_names))
274
+
275
+
276
+ def _validate_strings(cfg: AgentConfig) -> None:
277
+ """Guarantee that string-based fields are populated with coherent defaults.
278
+
279
+ Args:
280
+ cfg (AgentConfig): Instância de configuração a ser ajustada.
281
+
282
+ Returns:
283
+ None: Atualiza atributos de forma destrutiva na instância recebida.
284
+ """
285
+ cfg.agent_name = str(cfg.agent_name or "oskaragent")
286
+ cfg.agent_id = str(cfg.agent_id or cfg.agent_name)
287
+ cfg.model = str(cfg.model or "gpt-5")
288
+ cfg.system_prompt = _ensure_str_or_none(cfg.system_prompt) or _default_system_prompt(cfg.agent_name)
289
+ cfg.description = _ensure_str_or_none(cfg.description) or 'Esse é um agente de Inteligência Artificial.'
290
+
291
+
292
+ def _validate_misc(cfg: AgentConfig) -> None:
293
+ """Handle validation of heterogeneous configuration attributes.
294
+
295
+ Args:
296
+ cfg (AgentConfig): Configuração a ser saneada.
297
+
298
+ Returns:
299
+ None: Apenas normaliza `model_settings` e `mcp_tools`.
300
+ """
301
+ cfg.model_settings = _validate_model_settings(cfg.model_settings)
302
+ cfg.custom_tools = _ensure_list(cfg.custom_tools)
303
+
304
+
305
+ def _validate_dataclass(cfg: AgentConfig) -> None:
306
+ """Run the full validation pipeline on the dataclass instance.
307
+
308
+ Args:
309
+ cfg (AgentConfig): Instância alvo da validação.
310
+
311
+ Returns:
312
+ None: O procedimento atualiza os campos existentes sem retorno.
313
+ """
314
+ _validate_strings(cfg)
315
+ _validate_working_lists(cfg)
316
+ _validate_misc(cfg)
317
+
318
+
319
+ def _validate(self: AgentConfig) -> None:
320
+ """Invoke the standalone validation routine bound to the dataclass.
321
+
322
+ Returns:
323
+ None: Encaminha a chamada para `_validate_dataclass` sem retorno explícito.
324
+ """
325
+ _validate_dataclass(self)
326
+
327
+
328
+ AgentConfig._validate = _validate # type: ignore[attr-defined]
@@ -0,0 +1,102 @@
1
+ import asyncio
2
+ from typing import Iterable, Any
3
+
4
+ from mcp import ClientSession, types
5
+ from mcp.client.streamable_http import streamable_http_client
6
+
7
+ from .helpers import ToolContext, log_msg
8
+
9
+
10
+ def build_mcp_tool_schemas(tool_names: Iterable[str], mcp_tools_schemas: list) -> list[dict[str, Any]]:
11
+ """Generate minimal JSON schema definitions for API-exposed tools.
12
+
13
+ Args:
14
+ tool_names (Iterable[str]): Conjunto de nomes de ferramentas permitidos na sessão.
15
+ mcp_tools_schemas: Optional[dict]: Definições das ferramentas externas
16
+
17
+ Returns:
18
+ list[dict[str, Any]]: Lista de esquemas no formato esperado pela OpenAI Responses API.
19
+ """
20
+ schemas: list[dict[str, Any]] = []
21
+ for name in tool_names:
22
+ for mcp_tool_schema in mcp_tools_schemas:
23
+ if mcp_tool_schema["name"] == name:
24
+ schemas.append(mcp_tool_schema)
25
+ break
26
+
27
+ return schemas
28
+
29
+
30
+ async def build_mcp_tools_schemas(mcp_tools: list[dict]) -> list[dict]:
31
+ """Carrega e valida as definições das ferramentas externas disponíveis para o MCP."""
32
+ mcp_tools_schemas = []
33
+ for server in mcp_tools:
34
+ server_url = server["mcpServerUrl"]
35
+
36
+ async with streamable_http_client(server_url) as (read_stream, write_stream, _):
37
+ async with ClientSession(read_stream, write_stream) as session:
38
+ await session.initialize()
39
+ tools = await session.list_tools()
40
+ for tool in tools.tools:
41
+ for tool_name in server["tools"]:
42
+ if tool_name == tool.name:
43
+ tool_schema = {
44
+ "type": "function",
45
+ "name": tool.name,
46
+ "description": tool.description,
47
+ "args_obj": tool.inputSchema,
48
+ "mcpServerUrl": server_url,
49
+ }
50
+ tool_schema["args_obj"]["title"] = tool_schema["name"] + "_arguments"
51
+ mcp_tools_schemas.append(tool_schema)
52
+ break
53
+
54
+ return mcp_tools_schemas
55
+
56
+
57
+ def get_mcp_tools_names(mcp_tools_schemas: list[dict[str, Any]]) -> list[str]:
58
+ """Gera uma lista com os nomes das ferramentas configuradas para o MCP."""
59
+ return [tool_schema["name"] for tool_schema in mcp_tools_schemas]
60
+
61
+
62
+ def get_mcp_tools_schema(mcp_tools_schemas: list[dict[str, Any]], tool_name: str) -> dict[str, Any] | None:
63
+ """Retorna o esquema de uma ferramenta MCP específica a partir de uma lista de esquemas."""
64
+ return next((tool_schema for tool_schema in mcp_tools_schemas if tool_schema.get("name") == tool_name), None)
65
+
66
+
67
+ def exec_mcp_tool(tool_name: str,
68
+ args_obj: dict[str, Any],
69
+ ctx: ToolContext) -> Any:
70
+ """Executa uma ferramenta personalizada disponibilizada pelo MCP."""
71
+ return asyncio.run(mcp_tool(tool_name, args_obj, ctx))
72
+
73
+
74
+ async def mcp_tool(tool_name: str,
75
+ args_obj: dict[str, Any],
76
+ ctx: ToolContext) -> Any:
77
+ """Executa uma ferramenta personalizada disponibilizada pelo MCP."""
78
+ tool_schema = get_mcp_tools_schema(ctx.mcp_tools_schema, tool_name)
79
+ server_url = tool_schema["mcpServerUrl"]
80
+
81
+ msg_id = getattr(ctx, "message_id", "-")
82
+ if getattr(ctx, "is_verbose", False):
83
+ log_msg(f"id={msg_id} mcp_call server={server_url} tool_name={tool_name}", func="mcp_tool", action="tools", color="MAGENTA")
84
+
85
+ tool_parameters = {
86
+ param_name: args_obj[param_name] if param_name in args_obj else tool_schema["args_obj"][param_name].get("default")
87
+ for param_name in tool_schema.get("args_obj", {}).get("properties", {}).keys()
88
+ }
89
+
90
+ async with streamable_http_client(server_url) as (read_stream, write_stream, _):
91
+ async with ClientSession(read_stream, write_stream) as session:
92
+ await session.initialize()
93
+
94
+ result = await session.call_tool(
95
+ tool_name,
96
+ arguments=tool_parameters,
97
+ )
98
+
99
+ if isinstance(result.content[0], types.TextContent):
100
+ result = result.content[0].text
101
+
102
+ return result