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.
- oskaragent/__init__.py +49 -0
- oskaragent/agent.py +1911 -0
- oskaragent/agent_config.py +328 -0
- oskaragent/agent_mcp_tools.py +102 -0
- oskaragent/agent_tools.py +962 -0
- oskaragent/helpers.py +175 -0
- oskaragent-0.1.38a0.dist-info/METADATA +29 -0
- oskaragent-0.1.38a0.dist-info/RECORD +30 -0
- oskaragent-0.1.38a0.dist-info/WHEEL +5 -0
- oskaragent-0.1.38a0.dist-info/licenses/LICENSE +21 -0
- oskaragent-0.1.38a0.dist-info/top_level.txt +2 -0
- tests/1a_test_basico.py +43 -0
- tests/1b_test_history.py +72 -0
- tests/1c_test_basico.py +61 -0
- tests/2a_test_tool_python.py +50 -0
- tests/2b_test_tool_calculator.py +54 -0
- tests/2c_test_tool_savefile.py +50 -0
- tests/3a_test_upload_md.py +46 -0
- tests/3b_test_upload_img.py +43 -0
- tests/3c_test_upload_pdf_compare.py +44 -0
- tests/4_test_RAG.py +56 -0
- tests/5_test_MAS.py +58 -0
- tests/6a_test_MCP_tool_CRM.py +77 -0
- tests/6b_test_MCP_tool_ITSM.py +72 -0
- tests/6c_test_MCP_tool_SQL.py +69 -0
- tests/6d_test_MCP_tool_DOC_SQL.py +45 -0
- tests/7a_test_BI_CSV.py +37 -0
- tests/7b_test_BI_SQL.py +47 -0
- tests/8a_test_external_tool.py +194 -0
- tests/helpers.py +60 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textwrap import dedent
|
|
4
|
+
from typing import Any
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from oskaragent.helpers import ToolContext, vlog
|
|
8
|
+
from tests.helpers import set_key
|
|
9
|
+
from oskaragent.agent import Oskar
|
|
10
|
+
from oskaragent.agent_config import AgentConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
OpportunityData = dict[str, Any]
|
|
14
|
+
|
|
15
|
+
_OPPORTUNITY_FIXTURES: list[OpportunityData] = [
|
|
16
|
+
{
|
|
17
|
+
"Id": "OPO-ADVICEHEALTH-2025-12-0001",
|
|
18
|
+
"StageName": "Detalhando",
|
|
19
|
+
"Probability": 25.0,
|
|
20
|
+
"CreatedDate": "2025-12-01T14:27:23.000+0000",
|
|
21
|
+
"OwnerId": "005SG000009Yux4YAC",
|
|
22
|
+
"ShortDescription": "Renovação FortiNac",
|
|
23
|
+
"ProductFamily": "Fortinet",
|
|
24
|
+
"ProductLine": "SSD",
|
|
25
|
+
"PainDescription": "Cliente precisa de proteção de NAC / ZTNA / Token para sua infra. Por isso, precisa renovar.",
|
|
26
|
+
"PainImpact": "Cliente não pode ficar desprotegido.",
|
|
27
|
+
"Renewal": True,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"Id": "OPO-ACME-2025-11-0042",
|
|
31
|
+
"StageName": "Qualificando",
|
|
32
|
+
"Probability": 10.0,
|
|
33
|
+
"CreatedDate": "2025-11-18T09:02:11.000+0000",
|
|
34
|
+
"OwnerId": "005SG000009Yux4YAC",
|
|
35
|
+
"ShortDescription": "Ampliação de licenças EDR",
|
|
36
|
+
"ProductFamily": "Endpoint",
|
|
37
|
+
"ProductLine": "EDR",
|
|
38
|
+
"PainDescription": "Necessidade de melhorar cobertura de detecção e resposta.",
|
|
39
|
+
"PainImpact": "Maior risco de incidentes e indisponibilidade.",
|
|
40
|
+
"Renewal": False,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"Id": "OPO-FOO-2026-01-0007",
|
|
44
|
+
"StageName": "Negociando",
|
|
45
|
+
"Probability": 60.0,
|
|
46
|
+
"CreatedDate": "2025-12-15T16:40:00.000+0000",
|
|
47
|
+
"OwnerId": "005SG000009Yux4YAC",
|
|
48
|
+
"ShortDescription": "Projeto de migração de firewall",
|
|
49
|
+
"ProductFamily": "Network Security",
|
|
50
|
+
"ProductLine": "Firewall",
|
|
51
|
+
"PainDescription": "Infra atual sem suporte e com limitações de throughput.",
|
|
52
|
+
"PainImpact": "Risco de falhas e gargalos em períodos críticos.",
|
|
53
|
+
"Renewal": False,
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_opportunity_info_by_id(
|
|
61
|
+
opportunity_id: str
|
|
62
|
+
) -> OpportunityData:
|
|
63
|
+
"""Search opportunity fixtures by name.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
- On success: opportunity data with `Status="OK"`.
|
|
67
|
+
- On failure: a small error payload with `Status` describing the issue.
|
|
68
|
+
"""
|
|
69
|
+
normalized_query = opportunity_id.strip().upper()
|
|
70
|
+
if not normalized_query or normalized_query == "*":
|
|
71
|
+
return {
|
|
72
|
+
"Status": "ERROR",
|
|
73
|
+
"ErrorCode": "INVALID_ARGUMENT",
|
|
74
|
+
"Message": "opportunity_id is required",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for oportunidade_info in _OPPORTUNITY_FIXTURES:
|
|
78
|
+
if normalized_query == oportunidade_info["Id"].upper():
|
|
79
|
+
return oportunidade_info
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"Status": "ERROR",
|
|
83
|
+
"ErrorCode": "NOT_FOUND",
|
|
84
|
+
"Message": "opportunity not found",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def external_tools_function_handler(tool_name: str, args_obj: dict | None, ctx: ToolContext | None) -> dict[str, Any]:
|
|
89
|
+
"""
|
|
90
|
+
Handles the execution of an external tool function and processes the result.
|
|
91
|
+
|
|
92
|
+
This function takes a dictionary of parameters, processes them using an
|
|
93
|
+
external tool functionality, and returns the resulting data in a specified
|
|
94
|
+
format.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
tool_name (str): The name of the external tool function to execute.
|
|
98
|
+
args_obj (dict | None): A dictionary containing the parameters to
|
|
99
|
+
process. If None, default behavior or configurations are applied.
|
|
100
|
+
ctx (ToolContext | None, optional): Contexto opcional usado para logs verbosos. Defaults to None.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict[str, Any]: A dictionary containing the result of the processing,
|
|
104
|
+
formatted as a key-value pair.
|
|
105
|
+
"""
|
|
106
|
+
if ctx and getattr(ctx, "is_verbose", False):
|
|
107
|
+
vlog(ctx, f"session={getattr(ctx, 'session_id', '-')}", func=tool_name)
|
|
108
|
+
|
|
109
|
+
match tool_name:
|
|
110
|
+
case "get_opportunity_info_by_id":
|
|
111
|
+
opportunity_id = args_obj.get("opportunity_id")
|
|
112
|
+
if opportunity_id is None:
|
|
113
|
+
raise ValueError("opportunity_id is required")
|
|
114
|
+
return get_opportunity_info_by_id(opportunity_id)
|
|
115
|
+
|
|
116
|
+
raise ValueError(f"Unsupported tool name: {tool_name}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _response_log(assistant_message_struct: dict[str, Any]):
|
|
120
|
+
usage = assistant_message_struct.get("usage", {}) or {}
|
|
121
|
+
print(
|
|
122
|
+
f"[callback] message_id={assistant_message_struct.get('message_id')} "
|
|
123
|
+
f"tokens={{'input_tokens': {usage.get('input_tokens')}, 'output_tokens': {usage.get('output_tokens')}, 'total_tokens': {usage.get('total_tokens')}}}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> int:
|
|
128
|
+
# Set OpenAI API key
|
|
129
|
+
set_key()
|
|
130
|
+
|
|
131
|
+
system_prompt = dedent("""
|
|
132
|
+
### Instruções:
|
|
133
|
+
Atue como um analista especializado no módulo de vendas do Salesforce. Use suas habilidades e conhecimentos técnicos para orientar vendedores na superação de obstáculos em negociações, oferecendo estratégias eficazes para fechar vendas. Você deve manter um comportamento didático, criativo, organizado e comunicativo, empregando seu elevado pensamento crítico para adaptar as soluções às necessidades específicas dos clientes.
|
|
134
|
+
|
|
135
|
+
Você tem conhecimento em venda de produtos de segurança da informação dos fabricantes Check Point, Fortinet, Checkmarx, Proofpoint, Algosec e vários outros. Então você também pode responder perguntas e pedidos técnicos referentes a esses produtos.
|
|
136
|
+
|
|
137
|
+
### Interlocutor
|
|
138
|
+
Suas respostas devem ser direcionadas a vendedores que possuem um entendimento básico a intermediário sobre produtos de segurança da informação e técnicas de venda, mas buscam aprimoramento na negociação e no entendimento técnico avançado das soluções oferecidas.
|
|
139
|
+
|
|
140
|
+
### Tarefas
|
|
141
|
+
- Usar a ferramenta apropriada para buscar as informações atualizadas da oportunidade, que estão no Salesforce.
|
|
142
|
+
- Responda ao pedido realizado.
|
|
143
|
+
|
|
144
|
+
### Modo, tom e estilo da resposta
|
|
145
|
+
Suas respostas devem ser diretas, confiantes e didáticas, mantendo um tom profissional e acessível. Priorize a clareza e precisão nas informações técnicas, adaptando a complexidade das explicações ao nível de entendimento do interlocutor.
|
|
146
|
+
|
|
147
|
+
### Casos atípicos
|
|
148
|
+
1. Se o interlocutor pedir algo sobre uma oportunidade, mas não informar o código da oportunidade, então considere o código {OPO}.
|
|
149
|
+
2. Se o interlocutor fizer perguntas que não se relacionam diretamente com estratégias de venda ou produtos de segurança da informação, responda que seu foco é fornecer consultoria em vendas e segurança da informação e sugira buscar um especialista adequado para outras questões.
|
|
150
|
+
|
|
151
|
+
### Limites da conversa
|
|
152
|
+
Responda apenas perguntas sobre estratégias de venda, produtos de segurança da informação e sua aplicação, e uso do CRM Salesforce. Não responda perguntas e pedidos que não sejam associados ao papel 'Consultor de Vendas'.
|
|
153
|
+
""")
|
|
154
|
+
|
|
155
|
+
external_tools = [
|
|
156
|
+
{
|
|
157
|
+
"type": "function",
|
|
158
|
+
"name": "get_opportunity_info_by_id",
|
|
159
|
+
"description": "Use essa ferramenta para obter os dados de uma oportunidade que está no CRM, cujo código é fornecido.",
|
|
160
|
+
"parameters": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"opportunity_id": {"type": "string", "description": "Código da oportunidade no CRM"},
|
|
164
|
+
},
|
|
165
|
+
"required": ["opportunity_id"],
|
|
166
|
+
"additionalProperties": False,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
ag_cfg = AgentConfig(
|
|
172
|
+
system_prompt=system_prompt,
|
|
173
|
+
tools_names=["get_opportunity_info_by_id"],
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
agent = Oskar(agent_config=ag_cfg,
|
|
177
|
+
response_callback=_response_log,
|
|
178
|
+
external_tools_schema=external_tools,
|
|
179
|
+
external_tools_function_handler=external_tools_function_handler,
|
|
180
|
+
is_verbose=True)
|
|
181
|
+
|
|
182
|
+
# No explicit question provided: run a quick test prompt and exit
|
|
183
|
+
# test_q = "Qual é a dor do cliente na oportunidade OPO-ORIZON-2024-08-0001?"
|
|
184
|
+
test_q = "Qual é a dor do cliente na oportunidade OPO-FOO-2026-01-0007?"
|
|
185
|
+
res = agent.answer(test_q)
|
|
186
|
+
|
|
187
|
+
print(test_q)
|
|
188
|
+
# Ensure UTF-8 characters are printed properly (no ASCII escapes)
|
|
189
|
+
print(json.dumps(res, indent=2, ensure_ascii=False))
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
raise SystemExit(main())
|
tests/helpers.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_keys() -> str | None:
|
|
11
|
+
"""
|
|
12
|
+
Carrega o YAML de chaves de acesso.
|
|
13
|
+
|
|
14
|
+
:return:
|
|
15
|
+
"""
|
|
16
|
+
root = Path(__file__).resolve().parent.parent
|
|
17
|
+
keys_path = root / "tests" / "keys.yaml"
|
|
18
|
+
|
|
19
|
+
if keys_path.exists():
|
|
20
|
+
with open(keys_path, 'r', encoding='UTF-8') as file:
|
|
21
|
+
keys_str = file.read()
|
|
22
|
+
return keys_str
|
|
23
|
+
# endiif --
|
|
24
|
+
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_key(key: str = None) -> Optional[str]:
|
|
29
|
+
"""Return the OPENAI_API_KEY value from the project root `keys.yaml`.
|
|
30
|
+
|
|
31
|
+
Looks for `keys.yaml` in the same directory as this file (the project root)
|
|
32
|
+
and extracts the value of the `OPENAI_API_KEY` entry without requiring a YAML
|
|
33
|
+
parser dependency.
|
|
34
|
+
"""
|
|
35
|
+
keys_yaml = yaml.load(load_keys(), Loader=yaml.FullLoader)
|
|
36
|
+
|
|
37
|
+
if key is None:
|
|
38
|
+
# retorna a chave da OpenAI
|
|
39
|
+
return keys_yaml.get('OPENAI_API_KEY', None)
|
|
40
|
+
|
|
41
|
+
if '.' not in key:
|
|
42
|
+
return keys_yaml.get(key, None)
|
|
43
|
+
# endfor --
|
|
44
|
+
|
|
45
|
+
tks = key.split('.')
|
|
46
|
+
value = keys_yaml
|
|
47
|
+
|
|
48
|
+
for tk in tks:
|
|
49
|
+
if not value:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
value = value.get(tk)
|
|
53
|
+
# endfor --
|
|
54
|
+
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def set_key():
|
|
59
|
+
key = get_key()
|
|
60
|
+
os.environ["OPENAI_API_KEY"] = key
|