mirror of
https://github.com/domfelipe/hermes-agent-custom.git
synced 2026-08-07 08:36:40 +00:00
feat: post mika actions back to platform
This commit is contained in:
parent
11b664b8d5
commit
7718920777
8 changed files with 364 additions and 30 deletions
5
.github/workflows/docker.yml
vendored
5
.github/workflows/docker.yml
vendored
|
|
@ -21,6 +21,11 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run runtime tests
|
||||||
|
run: |
|
||||||
|
python3 -m unittest discover -s tests -v
|
||||||
|
python3 -m compileall plugins patches tests
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
|
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
|
@ -74,6 +74,7 @@ RUN printf '%s\n' \
|
||||||
'Default soul. Override via HERMES_SOUL_OVERRIDE.' \
|
'Default soul. Override via HERMES_SOUL_OVERRIDE.' \
|
||||||
'' \
|
'' \
|
||||||
'Quando o usuário pedir para agendar, lembrar ou automatizar algo recorrente, use a tool cronjob_create passando a frase original dele em natural_language_input.' \
|
'Quando o usuário pedir para agendar, lembrar ou automatizar algo recorrente, use a tool cronjob_create passando a frase original dele em natural_language_input.' \
|
||||||
|
'Quando o usuário pedir para criar, salvar ou ensinar uma skill nova, use a tool skill_create passando a frase original dele em natural_language_input.' \
|
||||||
> /opt/data/.hermes/SOUL.md
|
> /opt/data/.hermes/SOUL.md
|
||||||
|
|
||||||
# Cria o usuário hermes (esperado pelos scripts internos do Hermes)
|
# Cria o usuário hermes (esperado pelos scripts internos do Hermes)
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -9,6 +9,7 @@ The Hermes gateway ignores environment variables for the primary model configura
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The image pre-configures:
|
The image pre-configures:
|
||||||
|
|
||||||
- **Provider**: `ollama-cloud`
|
- **Provider**: `ollama-cloud`
|
||||||
- **Model**: `gemma4:31b-cloud`
|
- **Model**: `gemma4:31b-cloud`
|
||||||
- **Config path**: `/opt/data/.hermes/config.yaml`
|
- **Config path**: `/opt/data/.hermes/config.yaml`
|
||||||
|
|
@ -28,6 +29,23 @@ PORT=8642
|
||||||
TELEGRAM_BOT_TOKEN=your-bot-token
|
TELEGRAM_BOT_TOKEN=your-bot-token
|
||||||
TELEGRAM_ALLOWED_USERS=your-user-id
|
TELEGRAM_ALLOWED_USERS=your-user-id
|
||||||
TELEGRAM_HOME_CHANNEL=your-channel-id
|
TELEGRAM_HOME_CHANNEL=your-channel-id
|
||||||
|
MIKA_AGENT_INSTANCE_ID=agent-instance-uuid
|
||||||
|
MIKA_PLATFORM_FUNCTIONS_BASE_URL=https://<project>.supabase.co/functions/v1
|
||||||
|
MIKA_CREATE_CRONJOB_URL=https://<project>.supabase.co/functions/v1/create-cronjob-from-agent
|
||||||
|
MIKA_CREATE_SKILL_URL=https://<project>.supabase.co/functions/v1/create-skill-from-agent
|
||||||
|
MIKA_INTERNAL_FUNCTION_SECRET=shared-internal-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
`MIKA_CREATE_CRONJOB_URL` and `MIKA_CREATE_SKILL_URL` are optional when
|
||||||
|
`MIKA_PLATFORM_FUNCTIONS_BASE_URL` or `SUPABASE_URL` is present. The Mika
|
||||||
|
platform provisions all of these automatically for managed Railway services.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m unittest discover -s tests -v
|
||||||
|
python3 -m compileall plugins patches tests
|
||||||
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploy to Railway
|
## Deploy to Railway
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ from plugins.mika_runtime.tools import (
|
||||||
CRONJOB_CREATE_SCHEMA,
|
CRONJOB_CREATE_SCHEMA,
|
||||||
INTEGRATIONS_STATUS_SCHEMA,
|
INTEGRATIONS_STATUS_SCHEMA,
|
||||||
NOTION_API_SCHEMA,
|
NOTION_API_SCHEMA,
|
||||||
|
SKILL_CREATE_SCHEMA,
|
||||||
TODOIST_API_SCHEMA,
|
TODOIST_API_SCHEMA,
|
||||||
handle_calcom_api,
|
handle_calcom_api,
|
||||||
handle_cronjob_create,
|
handle_cronjob_create,
|
||||||
handle_integrations_status,
|
handle_integrations_status,
|
||||||
handle_notion_api,
|
handle_notion_api,
|
||||||
|
handle_skill_create,
|
||||||
handle_todoist_api,
|
handle_todoist_api,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -51,3 +53,10 @@ def register(ctx) -> None:
|
||||||
handler=handle_cronjob_create,
|
handler=handle_cronjob_create,
|
||||||
emoji="⏰",
|
emoji="⏰",
|
||||||
)
|
)
|
||||||
|
ctx.register_tool(
|
||||||
|
name="skill_create",
|
||||||
|
toolset="mika_integrations",
|
||||||
|
schema=SKILL_CREATE_SCHEMA,
|
||||||
|
handler=handle_skill_create,
|
||||||
|
emoji="🧩",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ provides_tools:
|
||||||
- mika_todoist_api
|
- mika_todoist_api
|
||||||
- mika_calcom_api
|
- mika_calcom_api
|
||||||
- cronjob_create
|
- cronjob_create
|
||||||
|
- skill_create
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@ DEFAULT_NOTION_VERSION = "2022-06-28"
|
||||||
DEFAULT_CALCOM_VERSION = "2026-02-25"
|
DEFAULT_CALCOM_VERSION = "2026-02-25"
|
||||||
MAX_RESPONSE_CHARS = 20000
|
MAX_RESPONSE_CHARS = 20000
|
||||||
USER_AGENT = "domco-mika-runtime/0.1"
|
USER_AGENT = "domco-mika-runtime/0.1"
|
||||||
|
AGENT_INSTANCE_ENV_NAMES = (
|
||||||
|
"MIKA_AGENT_INSTANCE_ID",
|
||||||
|
"HERMES_AGENT_INSTANCE_ID",
|
||||||
|
"AGENT_INSTANCE_ID",
|
||||||
|
)
|
||||||
|
INTERNAL_SECRET_ENV_NAMES = (
|
||||||
|
"MIKA_INTERNAL_FUNCTION_SECRET",
|
||||||
|
"HERMES_INTERNAL_FUNCTION_SECRET",
|
||||||
|
"INTERNAL_FUNCTION_SECRET",
|
||||||
|
)
|
||||||
|
|
||||||
INTEGRATIONS_STATUS_SCHEMA = {
|
INTEGRATIONS_STATUS_SCHEMA = {
|
||||||
"name": "mika_integrations_status",
|
"name": "mika_integrations_status",
|
||||||
|
|
@ -535,61 +545,209 @@ CRONJOB_CREATE_SCHEMA = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def handle_cronjob_create(args: dict[str, Any], **_: Any) -> str:
|
SKILL_CREATE_SCHEMA = {
|
||||||
supabase_url = os.environ.get("SUPABASE_URL", "").rstrip("/")
|
"name": "skill_create",
|
||||||
internal_secret = os.environ.get("INTERNAL_FUNCTION_SECRET", "")
|
"description": (
|
||||||
agent_instance_id = os.environ.get("AGENT_INSTANCE_ID", "")
|
"Creates a new custom Mika/Hermes skill via the Mika platform. Use this "
|
||||||
|
"when the user asks to teach the assistant a new procedure, add a new "
|
||||||
|
"skill, save a repeatable workflow, or turn instructions into a reusable "
|
||||||
|
"capability."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"natural_language_input": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"The user's original request describing the skill to create."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A short skill name (optional).",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A short description of what the skill does (optional).",
|
||||||
|
},
|
||||||
|
"trigger_keywords": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Comma-separated phrases that should trigger this skill (optional)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"markdown_content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"Full SKILL.md content if already drafted. If omitted, Mika "
|
||||||
|
"will generate a valid skill from natural_language_input."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["natural_language_input"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _first_env(*names: str) -> str:
|
||||||
|
for name in names:
|
||||||
|
value = os.environ.get(name, "").strip()
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_functions_base_url() -> str:
|
||||||
|
explicit = _first_env(
|
||||||
|
"MIKA_PLATFORM_FUNCTIONS_BASE_URL",
|
||||||
|
"HERMES_PLATFORM_FUNCTIONS_BASE_URL",
|
||||||
|
).rstrip("/")
|
||||||
|
if explicit:
|
||||||
|
return explicit
|
||||||
|
|
||||||
|
supabase_url = os.environ.get("SUPABASE_URL", "").strip().rstrip("/")
|
||||||
if not supabase_url:
|
if not supabase_url:
|
||||||
return "Erro: variável de ambiente SUPABASE_URL não configurada."
|
return ""
|
||||||
|
return f"{supabase_url}/functions/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_endpoint(action: str) -> str:
|
||||||
|
if action == "cronjob":
|
||||||
|
explicit = _first_env("MIKA_CREATE_CRONJOB_URL", "HERMES_CREATE_CRONJOB_URL")
|
||||||
|
path = "create-cronjob-from-agent"
|
||||||
|
elif action == "skill":
|
||||||
|
explicit = _first_env("MIKA_CREATE_SKILL_URL", "HERMES_CREATE_SKILL_URL")
|
||||||
|
path = "create-skill-from-agent"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unknown platform action: {action}")
|
||||||
|
|
||||||
|
if explicit:
|
||||||
|
return explicit
|
||||||
|
|
||||||
|
base_url = _platform_functions_base_url()
|
||||||
|
if not base_url:
|
||||||
|
return ""
|
||||||
|
return f"{base_url}/{path}"
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_auth_context(action: str) -> tuple[str, str, str] | str:
|
||||||
|
endpoint = _platform_endpoint(action)
|
||||||
|
internal_secret = _first_env(*INTERNAL_SECRET_ENV_NAMES)
|
||||||
|
agent_instance_id = _first_env(*AGENT_INSTANCE_ENV_NAMES)
|
||||||
|
|
||||||
|
if not endpoint:
|
||||||
|
return (
|
||||||
|
"Erro: endpoint da plataforma não configurado. Defina "
|
||||||
|
"MIKA_CREATE_CRONJOB_URL/MIKA_CREATE_SKILL_URL ou SUPABASE_URL."
|
||||||
|
)
|
||||||
if not internal_secret:
|
if not internal_secret:
|
||||||
return "Erro: variável de ambiente INTERNAL_FUNCTION_SECRET não configurada."
|
return "Erro: segredo interno da plataforma não configurado."
|
||||||
if not agent_instance_id:
|
if not agent_instance_id:
|
||||||
return "Erro: variável de ambiente AGENT_INSTANCE_ID não configurada."
|
return "Erro: agent_instance_id da Mika não configurado no runtime."
|
||||||
|
|
||||||
natural_language_input = str(args.get("natural_language_input") or "").strip()
|
return endpoint, internal_secret, agent_instance_id
|
||||||
if not natural_language_input:
|
|
||||||
return "Erro: natural_language_input é obrigatório."
|
|
||||||
|
|
||||||
name = args.get("name") or None
|
|
||||||
|
|
||||||
url = f"{supabase_url}/functions/v1/create-cronjob-from-agent"
|
def _post_platform_action(action: str, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
||||||
|
ctx = _platform_auth_context(action)
|
||||||
|
if isinstance(ctx, str):
|
||||||
|
return 0, {"ok": False, "error": ctx}
|
||||||
|
|
||||||
|
url, internal_secret, agent_instance_id = ctx
|
||||||
body_payload = {
|
body_payload = {
|
||||||
"agent_instance_id": agent_instance_id,
|
"agent_instance_id": agent_instance_id,
|
||||||
"natural_language_input": natural_language_input,
|
**payload,
|
||||||
"name": name,
|
|
||||||
}
|
}
|
||||||
payload = json.dumps(body_payload, ensure_ascii=False).encode("utf-8")
|
body = json.dumps(body_payload, ensure_ascii=False).encode("utf-8")
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-internal-secret": internal_secret,
|
"X-Internal-Secret": internal_secret,
|
||||||
"User-Agent": USER_AGENT,
|
"User-Agent": USER_AGENT,
|
||||||
}
|
}
|
||||||
|
|
||||||
req = request.Request(url=url, data=payload, method="POST", headers=headers)
|
req = request.Request(url=url, data=body, method="POST", headers=headers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with request.urlopen(req, timeout=30) as response:
|
with request.urlopen(req, timeout=45) as response:
|
||||||
raw_body = response.read()
|
raw_body = response.read()
|
||||||
try:
|
try:
|
||||||
data = json.loads(raw_body.decode("utf-8", errors="replace"))
|
data = json.loads(raw_body.decode("utf-8", errors="replace"))
|
||||||
except Exception:
|
except Exception:
|
||||||
data = {}
|
data = {}
|
||||||
human_readable = data.get("human_readable") or data.get("description") or ""
|
return int(response.status), data
|
||||||
next_run_at = data.get("next_run_at") or ""
|
|
||||||
if human_readable:
|
|
||||||
msg = f"Automação criada: {human_readable}."
|
|
||||||
if next_run_at:
|
|
||||||
msg += f" Próxima execução: {next_run_at}."
|
|
||||||
return msg
|
|
||||||
return "Automação criada com sucesso."
|
|
||||||
except error.HTTPError as exc:
|
except error.HTTPError as exc:
|
||||||
raw_body = exc.read()
|
raw_body = exc.read()
|
||||||
try:
|
try:
|
||||||
data = json.loads(raw_body.decode("utf-8", errors="replace"))
|
data = json.loads(raw_body.decode("utf-8", errors="replace"))
|
||||||
err_msg = data.get("error") or data.get("message") or str(exc)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
err_msg = str(exc)
|
data = {"error": str(exc)}
|
||||||
return f"Erro ao criar automação: {err_msg}"
|
return int(exc.code), data
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return f"Erro de rede ao criar automação: {exc}"
|
return 0, {"error": f"Erro de rede ao chamar plataforma: {exc}"}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_cronjob_create(args: dict[str, Any], **_: Any) -> str:
|
||||||
|
natural_language_input = str(args.get("natural_language_input") or "").strip()
|
||||||
|
if not natural_language_input:
|
||||||
|
return "Erro: natural_language_input é obrigatório."
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"natural_language_input": natural_language_input,
|
||||||
|
}
|
||||||
|
|
||||||
|
name = args.get("name") or None
|
||||||
|
if name:
|
||||||
|
payload["name"] = str(name)
|
||||||
|
|
||||||
|
status, data = _post_platform_action("cronjob", payload)
|
||||||
|
|
||||||
|
if status < 200 or status >= 300 or data.get("success") is False:
|
||||||
|
err_msg = data.get("error") or data.get("message") or data.get("runtime_sync_error")
|
||||||
|
if not err_msg:
|
||||||
|
err_msg = f"HTTP {status}" if status else "falha desconhecida"
|
||||||
|
return f"Erro ao criar automação: {err_msg}"
|
||||||
|
|
||||||
|
human_readable = data.get("human_readable") or data.get("description") or ""
|
||||||
|
next_run_at = data.get("next_run_at") or ""
|
||||||
|
if human_readable:
|
||||||
|
msg = f"Automação criada e sincronizada: {human_readable}."
|
||||||
|
if next_run_at:
|
||||||
|
msg += f" Próxima execução: {next_run_at}."
|
||||||
|
return msg
|
||||||
|
return "Automação criada e sincronizada com sucesso."
|
||||||
|
|
||||||
|
|
||||||
|
def handle_skill_create(args: dict[str, Any], **_: Any) -> str:
|
||||||
|
natural_language_input = str(args.get("natural_language_input") or "").strip()
|
||||||
|
if not natural_language_input:
|
||||||
|
return "Erro: natural_language_input é obrigatório."
|
||||||
|
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
"natural_language_input": natural_language_input,
|
||||||
|
}
|
||||||
|
for key in ("name", "description", "trigger_keywords", "markdown_content"):
|
||||||
|
value = args.get(key)
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
payload[key] = value.strip()
|
||||||
|
|
||||||
|
status, data = _post_platform_action("skill", payload)
|
||||||
|
|
||||||
|
if status < 200 or status >= 300 or data.get("success") is False:
|
||||||
|
if data.get("skill_id") and data.get("runtime_sync_ok") is False:
|
||||||
|
return (
|
||||||
|
"Skill criada na plataforma, mas ainda não sincronizada no runtime. "
|
||||||
|
f"Ela ficou em status {data.get('status') or 'testing'}. "
|
||||||
|
f"Erro: {data.get('runtime_sync_error') or 'sync falhou'}"
|
||||||
|
)
|
||||||
|
err_msg = data.get("error") or data.get("message")
|
||||||
|
if not err_msg:
|
||||||
|
err_msg = f"HTTP {status}" if status else "falha desconhecida"
|
||||||
|
return f"Erro ao criar skill: {err_msg}"
|
||||||
|
|
||||||
|
name = data.get("name") or "Skill"
|
||||||
|
synced_count = data.get("synced_count")
|
||||||
|
msg = f"Skill criada e sincronizada: {name}."
|
||||||
|
if synced_count is not None:
|
||||||
|
msg += f" Skills ativas sincronizadas: {synced_count}."
|
||||||
|
return msg
|
||||||
|
|
|
||||||
139
tests/test_mika_runtime_tools.py
Normal file
139
tests/test_mika_runtime_tools.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
_HERMES_HOME = tempfile.mkdtemp(prefix="mika-runtime-test-")
|
||||||
|
_hermes_constants = types.ModuleType("hermes_constants")
|
||||||
|
_hermes_constants.get_hermes_home = lambda: _HERMES_HOME
|
||||||
|
sys.modules.setdefault("hermes_constants", _hermes_constants)
|
||||||
|
|
||||||
|
from plugins.mika_runtime import tools # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
def __init__(self, status: int, payload: dict[str, object]):
|
||||||
|
self.status = status
|
||||||
|
self._payload = payload
|
||||||
|
self.headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
def __enter__(self) -> "FakeResponse":
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_args: object) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read(self) -> bytes:
|
||||||
|
return json.dumps(self._payload).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class MikaRuntimePlatformActionTests(unittest.TestCase):
|
||||||
|
def test_cronjob_create_posts_platform_contract(self) -> None:
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_urlopen(req, timeout=0):
|
||||||
|
captured["url"] = req.full_url
|
||||||
|
captured["timeout"] = timeout
|
||||||
|
captured["secret"] = req.get_header("X-internal-secret")
|
||||||
|
captured["user_agent"] = req.get_header("User-agent")
|
||||||
|
captured["body"] = json.loads(req.data.decode("utf-8"))
|
||||||
|
return FakeResponse(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"human_readable": "toda segunda as 09:00",
|
||||||
|
"next_run_at": "2026-06-01T09:00:00Z",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"MIKA_AGENT_INSTANCE_ID": "agent-123",
|
||||||
|
"MIKA_INTERNAL_FUNCTION_SECRET": "secret-abc",
|
||||||
|
"MIKA_CREATE_CRONJOB_URL": "https://example.test/functions/v1/create-cronjob-from-agent",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
), mock.patch.object(tools.request, "urlopen", side_effect=fake_urlopen):
|
||||||
|
result = tools.handle_cronjob_create({
|
||||||
|
"natural_language_input": "toda segunda as 9h me envie um resumo",
|
||||||
|
"name": "Resumo semanal",
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertIn("Automação criada e sincronizada", result)
|
||||||
|
self.assertEqual(
|
||||||
|
captured["url"],
|
||||||
|
"https://example.test/functions/v1/create-cronjob-from-agent",
|
||||||
|
)
|
||||||
|
self.assertEqual(captured["secret"], "secret-abc")
|
||||||
|
self.assertEqual(captured["user_agent"], tools.USER_AGENT)
|
||||||
|
self.assertEqual(captured["timeout"], 45)
|
||||||
|
self.assertEqual(captured["body"]["agent_instance_id"], "agent-123")
|
||||||
|
self.assertEqual(
|
||||||
|
captured["body"]["natural_language_input"],
|
||||||
|
"toda segunda as 9h me envie um resumo",
|
||||||
|
)
|
||||||
|
self.assertEqual(captured["body"]["name"], "Resumo semanal")
|
||||||
|
|
||||||
|
def test_skill_create_uses_supabase_url_fallback_and_reports_sync_failure(self) -> None:
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_urlopen(req, timeout=0):
|
||||||
|
captured["url"] = req.full_url
|
||||||
|
captured["body"] = json.loads(req.data.decode("utf-8"))
|
||||||
|
return FakeResponse(
|
||||||
|
502,
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"skill_id": "skill-123",
|
||||||
|
"status": "testing",
|
||||||
|
"runtime_sync_ok": False,
|
||||||
|
"runtime_sync_error": "runtime offline",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"AGENT_INSTANCE_ID": "agent-456",
|
||||||
|
"INTERNAL_FUNCTION_SECRET": "secret-def",
|
||||||
|
"SUPABASE_URL": "https://project.supabase.co",
|
||||||
|
},
|
||||||
|
clear=True,
|
||||||
|
), mock.patch.object(tools.request, "urlopen", side_effect=fake_urlopen):
|
||||||
|
result = tools.handle_skill_create({
|
||||||
|
"natural_language_input": "aprenda meu processo de pré-vendas",
|
||||||
|
"name": "Pré-vendas",
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
captured["url"],
|
||||||
|
"https://project.supabase.co/functions/v1/create-skill-from-agent",
|
||||||
|
)
|
||||||
|
self.assertEqual(captured["body"]["agent_instance_id"], "agent-456")
|
||||||
|
self.assertEqual(captured["body"]["name"], "Pré-vendas")
|
||||||
|
self.assertIn("Skill criada na plataforma", result)
|
||||||
|
self.assertIn("testing", result)
|
||||||
|
self.assertIn("runtime offline", result)
|
||||||
|
|
||||||
|
def test_missing_platform_config_does_not_call_network(self) -> None:
|
||||||
|
with mock.patch.dict(os.environ, {}, clear=True), mock.patch.object(
|
||||||
|
tools.request,
|
||||||
|
"urlopen",
|
||||||
|
) as urlopen:
|
||||||
|
result = tools.handle_cronjob_create({
|
||||||
|
"natural_language_input": "me lembre todo dia",
|
||||||
|
})
|
||||||
|
|
||||||
|
urlopen.assert_not_called()
|
||||||
|
self.assertIn("endpoint da plataforma não configurado", result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue