astromesh-cli 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/.gitignore +9 -0
- astromesh_cli-0.4.0/CHANGELOG.md +46 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/PKG-INFO +3 -3
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/__init__.py +1 -1
- astromesh_cli-0.4.0/astromesh_cli/commands/eval.py +17 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/new.py +7 -2
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/run.py +42 -9
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/main.py +5 -2
- astromesh_cli-0.4.0/astromesh_cli/templates/agent.yaml.j2 +47 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/pyproject.toml +2 -2
- astromesh_cli-0.4.0/tests/test_core070.py +73 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/uv.lock +18 -4
- astromesh_cli-0.3.0/CHANGELOG.md +0 -23
- astromesh_cli-0.3.0/astromesh_cli/templates/agent.yaml.j2 +0 -29
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/README.md +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/client.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/__init__.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/agents.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/ask.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/dev.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/doctor.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/mesh.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/metrics.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/peers.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/providers.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/services.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/status.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/tools.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/commands/traces.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/output.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/templates/tool.py.j2 +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/astromesh_cli/templates/workflow.yaml.j2 +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/tests/conftest.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/tests/test_ask.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/tests/test_client.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/tests/test_main.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/tests/test_output.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/tests/test_run_usage.py +0 -0
- {astromesh_cli-0.3.0 → astromesh_cli-0.4.0}/tests/test_version.py +0 -0
|
@@ -43,3 +43,12 @@ docs-site/.astro/
|
|
|
43
43
|
# SDD scratch
|
|
44
44
|
.superpowers/
|
|
45
45
|
config/connections.yaml
|
|
46
|
+
|
|
47
|
+
# Worktrees de sesiones de Claude Code. Son repos git EMBEBIDOS: un `git add -A`
|
|
48
|
+
# se los lleva adentro de un commit, y ya pasó — en astromesh-herald entró uno
|
|
49
|
+
# dentro de un commit de release. Se ignora SÓLO `worktrees/`: este repo SÍ
|
|
50
|
+
# versiona `.claude/skills/` y sus comandos, y un ignore amplio se los llevaría.
|
|
51
|
+
.claude/worktrees/
|
|
52
|
+
|
|
53
|
+
# macOS
|
|
54
|
+
.DS_Store
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `astromeshctl eval <config_dir> [archivos...] [--out reporte.json]`: corre los `kind: Eval` con
|
|
14
|
+
el runner del core (`astromesh.evals`, el mismo de `astromesh-eval`). Exit 0 si todos cumplen,
|
|
15
|
+
1 si alguno queda bajo umbral, 2 por error de carga (un eval inválido no corre ningún caso).
|
|
16
|
+
- `run` muestra un panel **BLOCKED by guardrail** con dirección, regla y motivo cuando el core
|
|
17
|
+
0.70 bloquea la corrida (que vuelve HTTP 200 con el rechazo como `answer`), y una tabla con las
|
|
18
|
+
`propuestas` de las tools `mode: propose`. La tabla por modelo suma la columna `Cached`
|
|
19
|
+
(`tokens_cached`).
|
|
20
|
+
- `new agent --context-window N`; la plantilla trae `guardrails` válidos para el core 0.70
|
|
21
|
+
(`pii_detection` en entrada y salida, `max_length`, `cost_limit` 4000), `context_window`
|
|
22
|
+
(comentado si no se pasa) y un `knowledge.bundle` comentado. Un test la carga con
|
|
23
|
+
`AgentRuntime` sin warnings de guardrails.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Piso del core `astromesh>=0.70.0`.
|
|
27
|
+
|
|
28
|
+
## [0.3.1] - 2026-09-27
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Bumped the `astromesh` core dependency floor to `>=0.59.0` (`pyproject.toml`). No CLI code changes.
|
|
32
|
+
|
|
33
|
+
## [0.2.1] - 2026-08-10
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Bumped the `astromesh` core dependency floor to `>=0.40.0` (`pyproject.toml`).
|
|
37
|
+
|
|
38
|
+
## [0.2.0] - 2026-07-27
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- `run` now surfaces the per-model consumption breakdown (`usage.by_model`, astromesh core v0.36.0): a "Por modelo" table with provider / model / role / calls / tokens in-out / cost.
|
|
42
|
+
- `version` now also reports the installed astromesh **core** version (via `importlib.metadata`), not just the CLI's own.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Bumped the astromesh core dependency pin to `>=0.36.0`.
|
|
46
|
+
- `run` and `ask` now read the real run-response contract (`answer` + `usage`) instead of the stale `response`/`tokens_used` field names, via a shared `_format_run_output` helper; `trace_id` is read from the nested `trace`. `ask` previously rendered an empty body against core ≥ the version that introduced `answer`.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: astromesh-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI tool for managing Astromesh nodes and clusters
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
|
-
Requires-Dist: astromesh>=0.
|
|
7
|
+
Requires-Dist: astromesh>=0.70.0
|
|
8
8
|
Requires-Dist: jinja2>=3.1.0
|
|
9
9
|
Requires-Dist: rich>=13.0.0
|
|
10
10
|
Requires-Dist: typer>=0.12.0
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""astromeshctl eval — run `kind: Eval` files with core's eval runner (`astromesh-eval`)."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def eval_command(
|
|
7
|
+
config_dir: str = typer.Argument(..., help="Config tree with the agents under test"),
|
|
8
|
+
files: list[str] | None = typer.Argument( # noqa: B008
|
|
9
|
+
None, help="*.eval.yaml files (default: <config_dir>/evals/*.eval.yaml)"
|
|
10
|
+
),
|
|
11
|
+
out: str | None = typer.Option(None, "--out", help="Write the JSON report here"),
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Run agent evals locally. Exit 0 all pass, 1 below threshold, 2 load error."""
|
|
14
|
+
from astromesh.evals.__main__ import main
|
|
15
|
+
|
|
16
|
+
argv = [config_dir, *(files or [])] + (["--out", out] if out else [])
|
|
17
|
+
raise typer.Exit(code=main(argv))
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""astromeshctl new — Scaffold new agents, workflows, and tools."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
from jinja2 import Environment, FileSystemLoader
|
|
@@ -27,6 +26,7 @@ def render_agent_template(
|
|
|
27
26
|
temperature: float = 0.3,
|
|
28
27
|
max_tokens: int = 2048,
|
|
29
28
|
description: str = "",
|
|
29
|
+
context_window: int | None = None,
|
|
30
30
|
) -> str:
|
|
31
31
|
"""Render an agent YAML template."""
|
|
32
32
|
env = _get_jinja_env()
|
|
@@ -42,6 +42,7 @@ def render_agent_template(
|
|
|
42
42
|
tools=tools or [],
|
|
43
43
|
temperature=temperature,
|
|
44
44
|
max_tokens=max_tokens,
|
|
45
|
+
context_window=context_window,
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
|
|
@@ -80,7 +81,10 @@ def new_agent(
|
|
|
80
81
|
provider: str = typer.Option("ollama", "--provider", help="LLM provider"),
|
|
81
82
|
model: str = typer.Option("llama3.1:8b", "--model", help="Model name"),
|
|
82
83
|
orchestration: str = typer.Option("react", "--orchestration", help="Orchestration pattern"),
|
|
83
|
-
tools:
|
|
84
|
+
tools: list[str] | None = typer.Option(None, "--tools", help="Tools to include"), # noqa: B008
|
|
85
|
+
context_window: int | None = typer.Option(
|
|
86
|
+
None, "--context-window", help="Model context window in tokens (sizes the history)"
|
|
87
|
+
),
|
|
84
88
|
output_dir: str = typer.Option("./config/agents", "--output-dir", help="Output directory"),
|
|
85
89
|
force: bool = typer.Option(False, "--force", help="Overwrite existing file"),
|
|
86
90
|
) -> None:
|
|
@@ -99,6 +103,7 @@ def new_agent(
|
|
|
99
103
|
model=model,
|
|
100
104
|
orchestration_pattern=orchestration,
|
|
101
105
|
tools=tools,
|
|
106
|
+
context_window=context_window,
|
|
102
107
|
)
|
|
103
108
|
dest.write_text(content)
|
|
104
109
|
console.print(
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import uuid
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
from rich.panel import Panel
|
|
@@ -36,16 +35,44 @@ def _format_run_output(data: dict) -> tuple[str, str, list[dict]]:
|
|
|
36
35
|
return text, subtitle, rows
|
|
37
36
|
|
|
38
37
|
|
|
38
|
+
def _print_guardrail_and_propuestas(data: dict) -> None:
|
|
39
|
+
"""Core 0.70: a blocked run is HTTP 200 with the refusal as `answer`; say so."""
|
|
40
|
+
if data.get("blocked"):
|
|
41
|
+
g = data.get("guardrail") or {}
|
|
42
|
+
console.print(
|
|
43
|
+
Panel(
|
|
44
|
+
f"{g.get('direction', '?')} rule [bold]{g.get('rule', '?')}[/bold]: "
|
|
45
|
+
f"{g.get('reason', '')}",
|
|
46
|
+
title="[red]BLOCKED by guardrail[/red]",
|
|
47
|
+
border_style="red",
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
propuestas = [p for p in data.get("propuestas") or [] if isinstance(p, dict)]
|
|
51
|
+
if propuestas:
|
|
52
|
+
table = Table(title="Propuestas (sin ejecutar)", show_header=True)
|
|
53
|
+
for col in ("Tool", "Tipo", "Destino", "Operación", "Argumentos"):
|
|
54
|
+
table.add_column(col, style="cyan" if col == "Tool" else None)
|
|
55
|
+
for p in propuestas:
|
|
56
|
+
table.add_row(
|
|
57
|
+
str(p.get("tool", "")),
|
|
58
|
+
str(p.get("tipo", "")),
|
|
59
|
+
str(p.get("destino", "")),
|
|
60
|
+
str(p.get("operacion", "")),
|
|
61
|
+
json.dumps(p.get("argumentos", {}), ensure_ascii=False),
|
|
62
|
+
)
|
|
63
|
+
console.print(table)
|
|
64
|
+
|
|
65
|
+
|
|
39
66
|
def run_command(
|
|
40
67
|
name: str = typer.Argument(..., help="Agent or workflow name to run"),
|
|
41
68
|
query: str = typer.Argument("", help="Query to send to the agent"),
|
|
42
|
-
session:
|
|
69
|
+
session: str | None = typer.Option(
|
|
43
70
|
None, "--session", help="Session ID (auto-generated if not set)"
|
|
44
71
|
),
|
|
45
72
|
json_output: bool = typer.Option(False, "--json", help="Output raw JSON response"),
|
|
46
73
|
timeout: float = typer.Option(60.0, "--timeout", help="Request timeout in seconds"),
|
|
47
74
|
workflow: bool = typer.Option(False, "--workflow", help="Run as workflow instead of agent"),
|
|
48
|
-
input_data:
|
|
75
|
+
input_data: str | None = typer.Option(None, "--input", help="Workflow input data (JSON)"),
|
|
49
76
|
) -> None:
|
|
50
77
|
"""Execute an agent with a query or run a workflow."""
|
|
51
78
|
if workflow:
|
|
@@ -64,7 +91,7 @@ def run_command(
|
|
|
64
91
|
json={"query": query, "session_id": session_id},
|
|
65
92
|
timeout=timeout,
|
|
66
93
|
)
|
|
67
|
-
except Exception as e:
|
|
94
|
+
except Exception as e: # noqa: BLE001
|
|
68
95
|
print_error(f"Failed to run agent '{name}': {e}")
|
|
69
96
|
raise typer.Exit(code=1)
|
|
70
97
|
|
|
@@ -78,18 +105,24 @@ def run_command(
|
|
|
78
105
|
text,
|
|
79
106
|
title=f"[cyan]{name}[/cyan]",
|
|
80
107
|
subtitle=subtitle,
|
|
81
|
-
border_style="blue",
|
|
108
|
+
border_style="red" if data.get("blocked") else "blue",
|
|
82
109
|
)
|
|
83
110
|
)
|
|
111
|
+
_print_guardrail_and_propuestas(data)
|
|
84
112
|
if by_model_rows:
|
|
85
113
|
table = Table(title="Por modelo", show_header=True)
|
|
86
|
-
for col in ("Provider", "Model", "Role", "Calls", "In", "Out", "Cost"):
|
|
114
|
+
for col in ("Provider", "Model", "Role", "Calls", "In", "Cached", "Out", "Cost"):
|
|
87
115
|
table.add_column(col, style="cyan" if col == "Provider" else None)
|
|
88
116
|
for r in by_model_rows:
|
|
89
117
|
cost = r.get("cost", 0.0) or 0.0
|
|
90
118
|
table.add_row(
|
|
91
|
-
r.get("provider", ""),
|
|
92
|
-
|
|
119
|
+
r.get("provider", ""),
|
|
120
|
+
r.get("model", ""),
|
|
121
|
+
r.get("role", ""),
|
|
122
|
+
str(r.get("calls", 0)),
|
|
123
|
+
str(r.get("tokens_in", 0)),
|
|
124
|
+
str(r.get("tokens_cached", 0)),
|
|
125
|
+
str(r.get("tokens_out", 0)),
|
|
93
126
|
f"${cost:.4f}" if cost else "—",
|
|
94
127
|
)
|
|
95
128
|
console.print(table)
|
|
@@ -115,7 +148,7 @@ def _run_workflow(
|
|
|
115
148
|
json=payload,
|
|
116
149
|
timeout=timeout,
|
|
117
150
|
)
|
|
118
|
-
except Exception as e:
|
|
151
|
+
except Exception as e: # noqa: BLE001
|
|
119
152
|
print_error(f"Failed to run workflow '{name}': {e}")
|
|
120
153
|
raise typer.Exit(code=1)
|
|
121
154
|
|
|
@@ -21,6 +21,7 @@ from astromesh_cli.commands import (
|
|
|
21
21
|
tools,
|
|
22
22
|
traces,
|
|
23
23
|
)
|
|
24
|
+
from astromesh_cli.commands import eval as eval_cmd
|
|
24
25
|
|
|
25
26
|
app = typer.Typer(
|
|
26
27
|
name="astromeshctl",
|
|
@@ -44,12 +45,14 @@ app.command("trace")(traces.trace_command)
|
|
|
44
45
|
app.command("metrics")(metrics.metrics_command)
|
|
45
46
|
app.command("cost")(metrics.cost_command)
|
|
46
47
|
app.command("ask")(ask.ask_command)
|
|
48
|
+
app.command("eval")(eval_cmd.eval_command)
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
@app.command()
|
|
50
52
|
def version():
|
|
51
53
|
"""Show astromesh version."""
|
|
52
|
-
from importlib.metadata import PackageNotFoundError
|
|
54
|
+
from importlib.metadata import PackageNotFoundError
|
|
55
|
+
from importlib.metadata import version as _pkg_version
|
|
53
56
|
|
|
54
57
|
typer.echo(f"astromesh-cli {__version__}")
|
|
55
58
|
try:
|
|
@@ -70,7 +73,7 @@ def _load_plugins(app: typer.Typer) -> None:
|
|
|
70
73
|
try:
|
|
71
74
|
register_fn = ep.load()
|
|
72
75
|
register_fn(app)
|
|
73
|
-
except Exception as exc:
|
|
76
|
+
except Exception as exc: # noqa: BLE001
|
|
74
77
|
typer.echo(f"Warning: failed to load plugin '{ep.name}': {exc}", err=True)
|
|
75
78
|
|
|
76
79
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
apiVersion: astromesh/v1
|
|
2
|
+
kind: Agent
|
|
3
|
+
metadata:
|
|
4
|
+
name: {{ name }}
|
|
5
|
+
spec:
|
|
6
|
+
identity:
|
|
7
|
+
display_name: "{{ display_name }}"
|
|
8
|
+
description: "{{ description }}"
|
|
9
|
+
model:
|
|
10
|
+
primary:
|
|
11
|
+
provider: {{ provider }}
|
|
12
|
+
model: {{ model }}
|
|
13
|
+
temperature: {{ temperature }}
|
|
14
|
+
max_tokens: {{ max_tokens }}
|
|
15
|
+
{% if context_window %}
|
|
16
|
+
context_window: {{ context_window }}
|
|
17
|
+
{% else %}
|
|
18
|
+
# Declare the model's window so the runtime sizes the history budget:
|
|
19
|
+
# context_window: 128000
|
|
20
|
+
{% endif %}
|
|
21
|
+
routing:
|
|
22
|
+
strategy: quality_first
|
|
23
|
+
prompts:
|
|
24
|
+
system: |
|
|
25
|
+
You are {{ display_name }}, a helpful AI assistant.
|
|
26
|
+
orchestration:
|
|
27
|
+
pattern: {{ orchestration_pattern }}
|
|
28
|
+
max_iterations: 10
|
|
29
|
+
timeout: 30
|
|
30
|
+
{% if tools %}
|
|
31
|
+
tools:
|
|
32
|
+
{% for tool in tools %}
|
|
33
|
+
- name: {{ tool }}
|
|
34
|
+
{% endfor %}
|
|
35
|
+
{% endif %}
|
|
36
|
+
guardrails:
|
|
37
|
+
input:
|
|
38
|
+
- type: pii_detection # redact (default) | block | warn
|
|
39
|
+
- type: max_length # truncate (default) | block
|
|
40
|
+
max_chars: 10000
|
|
41
|
+
output:
|
|
42
|
+
- type: pii_detection
|
|
43
|
+
- type: cost_limit # blocks the run past this many output tokens
|
|
44
|
+
max_tokens_per_turn: 4000
|
|
45
|
+
# An OKF knowledge bundle (a directory) adds the kb_index/kb_read/kb_find tools:
|
|
46
|
+
# knowledge:
|
|
47
|
+
# bundle: ./knowledge/{{ name }}
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "astromesh-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "CLI tool for managing Astromesh nodes and clusters"
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
11
11
|
dependencies = [
|
|
12
|
-
"astromesh>=0.
|
|
12
|
+
"astromesh>=0.70.0",
|
|
13
13
|
"typer>=0.12.0",
|
|
14
14
|
"rich>=13.0.0",
|
|
15
15
|
"jinja2>=3.1.0",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""CLI against core 0.70: blocked/propuestas in `run`, a template core loads, `eval`."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from astromesh_cli.commands import run as run_mod
|
|
9
|
+
from astromesh_cli.commands.new import render_agent_template
|
|
10
|
+
from astromesh_cli.main import app
|
|
11
|
+
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_run_shows_blocked_and_propuestas(monkeypatch):
|
|
16
|
+
body = {
|
|
17
|
+
"answer": "I can't help with that request.",
|
|
18
|
+
"blocked": True,
|
|
19
|
+
"guardrail": {"direction": "input", "rule": "pii_detection", "reason": "pii detected"},
|
|
20
|
+
"propuestas": [
|
|
21
|
+
{"tool": "erp_crear", "tipo": "api", "destino": "erp", "operacion": "crear"}
|
|
22
|
+
],
|
|
23
|
+
"usage": {"tokens_in": 0, "tokens_out": 0, "by_model": []},
|
|
24
|
+
}
|
|
25
|
+
monkeypatch.setattr(run_mod, "api_post_with_timeout", lambda *a, **k: body)
|
|
26
|
+
result = runner.invoke(app, ["run", "support", "hi"])
|
|
27
|
+
assert result.exit_code == 0
|
|
28
|
+
assert "BLOCKED by guardrail" in result.stdout and "pii_detection" in result.stdout
|
|
29
|
+
assert "erp_crear" in result.stdout
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def test_agent_template_loads_in_core_without_warnings(tmp_path, caplog):
|
|
33
|
+
from astromesh.runtime.engine import AgentRuntime
|
|
34
|
+
|
|
35
|
+
(tmp_path / "agents").mkdir()
|
|
36
|
+
text = render_agent_template("bot", context_window=128000)
|
|
37
|
+
spec = yaml.safe_load(text)["spec"]
|
|
38
|
+
assert spec["model"]["primary"]["context_window"] == 128000
|
|
39
|
+
(tmp_path / "agents" / "bot.agent.yaml").write_text(text)
|
|
40
|
+
runtime = AgentRuntime(config_dir=str(tmp_path))
|
|
41
|
+
with caplog.at_level(logging.WARNING, logger="astromesh.core.guardrails"):
|
|
42
|
+
await runtime.bootstrap()
|
|
43
|
+
assert "bot" in runtime._agents, runtime._agent_errors
|
|
44
|
+
assert not caplog.records
|
|
45
|
+
guard = runtime._agents["bot"]._guardrails
|
|
46
|
+
assert guard.has_input and guard.has_output and guard.cost_limit["max_tokens"] == 4000
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_template_without_context_window_leaves_it_commented():
|
|
50
|
+
assert (
|
|
51
|
+
"context_window"
|
|
52
|
+
not in yaml.safe_load(render_agent_template("bot"))["spec"]["model"]["primary"]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_eval_forwards_to_core_runner(monkeypatch):
|
|
57
|
+
import astromesh.evals.__main__ as core_eval
|
|
58
|
+
|
|
59
|
+
seen = {}
|
|
60
|
+
|
|
61
|
+
def fake_main(argv):
|
|
62
|
+
seen["argv"] = argv
|
|
63
|
+
return 1
|
|
64
|
+
|
|
65
|
+
monkeypatch.setattr(core_eval, "main", fake_main)
|
|
66
|
+
result = runner.invoke(app, ["eval", "cfg", "a.eval.yaml", "--out", "r.json"])
|
|
67
|
+
assert result.exit_code == 1
|
|
68
|
+
assert seen["argv"] == ["cfg", "a.eval.yaml", "--out", "r.json"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_eval_without_evals_is_a_load_error(tmp_path):
|
|
72
|
+
result = runner.invoke(app, ["eval", str(tmp_path)])
|
|
73
|
+
assert result.exit_code == 2
|
|
@@ -35,9 +35,10 @@ wheels = [
|
|
|
35
35
|
|
|
36
36
|
[[package]]
|
|
37
37
|
name = "astromesh"
|
|
38
|
-
version = "0.
|
|
38
|
+
version = "0.70.0"
|
|
39
39
|
source = { editable = "../" }
|
|
40
40
|
dependencies = [
|
|
41
|
+
{ name = "astromesh-okf" },
|
|
41
42
|
{ name = "fastapi" },
|
|
42
43
|
{ name = "httpx" },
|
|
43
44
|
{ name = "jinja2" },
|
|
@@ -52,6 +53,7 @@ requires-dist = [
|
|
|
52
53
|
{ name = "aiosqlite", marker = "extra == 'sqlite'", specifier = ">=0.20.0" },
|
|
53
54
|
{ name = "astromesh", extras = ["redis", "postgres", "sqlite", "chromadb", "qdrant", "faiss", "embeddings", "onnx", "observability", "mcp", "mesh", "litellm", "centinela", "glyph"], marker = "extra == 'all'" },
|
|
54
55
|
{ name = "astromesh-glyph", marker = "extra == 'glyph'", editable = "../astromesh-glyph" },
|
|
56
|
+
{ name = "astromesh-okf", specifier = ">=0.1,<0.2" },
|
|
55
57
|
{ name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.30.0" },
|
|
56
58
|
{ name = "chromadb", marker = "extra == 'chromadb'", specifier = ">=0.5.0" },
|
|
57
59
|
{ name = "faiss-cpu", marker = "extra == 'faiss'", specifier = ">=1.8.0" },
|
|
@@ -60,7 +62,7 @@ requires-dist = [
|
|
|
60
62
|
{ name = "huggingface-hub", marker = "extra == 'centinela'", specifier = ">=0.24" },
|
|
61
63
|
{ name = "jinja2", specifier = ">=3.1.0" },
|
|
62
64
|
{ name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.50.0" },
|
|
63
|
-
{ name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.
|
|
65
|
+
{ name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.28.1,<2" },
|
|
64
66
|
{ name = "onnxruntime", marker = "extra == 'onnx'", specifier = ">=1.19.0" },
|
|
65
67
|
{ name = "opentelemetry-api", marker = "extra == 'observability'", specifier = ">=1.27.0" },
|
|
66
68
|
{ name = "opentelemetry-exporter-otlp", marker = "extra == 'observability'", specifier = ">=1.27.0" },
|
|
@@ -68,7 +70,7 @@ requires-dist = [
|
|
|
68
70
|
{ name = "prometheus-client", marker = "extra == 'observability'", specifier = ">=0.21.0" },
|
|
69
71
|
{ name = "psutil", specifier = ">=5.9.0" },
|
|
70
72
|
{ name = "psutil", marker = "extra == 'mesh'", specifier = ">=5.9.0" },
|
|
71
|
-
{ name = "pydantic", specifier = ">=2.9.0" },
|
|
73
|
+
{ name = "pydantic", specifier = ">=2.9.0,<3" },
|
|
72
74
|
{ name = "pyyaml", specifier = ">=6.0" },
|
|
73
75
|
{ name = "qdrant-client", marker = "extra == 'qdrant'", specifier = ">=1.11.0" },
|
|
74
76
|
{ name = "redis", extras = ["hiredis"], marker = "extra == 'redis'", specifier = ">=5.0.0" },
|
|
@@ -95,7 +97,7 @@ dev = [
|
|
|
95
97
|
|
|
96
98
|
[[package]]
|
|
97
99
|
name = "astromesh-cli"
|
|
98
|
-
version = "0.
|
|
100
|
+
version = "0.4.0"
|
|
99
101
|
source = { editable = "." }
|
|
100
102
|
dependencies = [
|
|
101
103
|
{ name = "astromesh" },
|
|
@@ -137,6 +139,18 @@ requires-dist = [
|
|
|
137
139
|
]
|
|
138
140
|
provides-extras = ["dev", "test", "all"]
|
|
139
141
|
|
|
142
|
+
[[package]]
|
|
143
|
+
name = "astromesh-okf"
|
|
144
|
+
version = "0.1.0"
|
|
145
|
+
source = { registry = "https://pypi.org/simple" }
|
|
146
|
+
dependencies = [
|
|
147
|
+
{ name = "pyyaml" },
|
|
148
|
+
]
|
|
149
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a5/33/ccf8f70e6cd22021dff6e11cba7c34b3d25d504675a1a417f18bc7c37730/astromesh_okf-0.1.0.tar.gz", hash = "sha256:acb932dc0a6969a77b52588e3572c147b0b69f5136860818c1b83474b503c7cf", size = 51340, upload-time = "2026-10-14T02:03:58.256Z" }
|
|
150
|
+
wheels = [
|
|
151
|
+
{ url = "https://files.pythonhosted.org/packages/e9/9d/cf840f718086009198c763516beec46abaf5d2346ea2afde34c9c6be94b3/astromesh_okf-0.1.0-py3-none-any.whl", hash = "sha256:8be5fa9d80cbd331decc02f2f28b3b614d79f5fc7d7967ed15cb1f0aa20b2bab", size = 28989, upload-time = "2026-10-14T02:03:57.036Z" },
|
|
152
|
+
]
|
|
153
|
+
|
|
140
154
|
[[package]]
|
|
141
155
|
name = "certifi"
|
|
142
156
|
version = "2026.7.22"
|
astromesh_cli-0.3.0/CHANGELOG.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [Unreleased]
|
|
9
|
-
|
|
10
|
-
## [0.2.1] - 2026-08-10
|
|
11
|
-
|
|
12
|
-
### Changed
|
|
13
|
-
- Bumped the `astromesh` core dependency floor to `>=0.40.0` (`pyproject.toml`).
|
|
14
|
-
|
|
15
|
-
## [0.2.0] - 2026-07-27
|
|
16
|
-
|
|
17
|
-
### Added
|
|
18
|
-
- `run` now surfaces the per-model consumption breakdown (`usage.by_model`, astromesh core v0.36.0): a "Por modelo" table with provider / model / role / calls / tokens in-out / cost.
|
|
19
|
-
- `version` now also reports the installed astromesh **core** version (via `importlib.metadata`), not just the CLI's own.
|
|
20
|
-
|
|
21
|
-
### Changed
|
|
22
|
-
- Bumped the astromesh core dependency pin to `>=0.36.0`.
|
|
23
|
-
- `run` and `ask` now read the real run-response contract (`answer` + `usage`) instead of the stale `response`/`tokens_used` field names, via a shared `_format_run_output` helper; `trace_id` is read from the nested `trace`. `ask` previously rendered an empty body against core ≥ the version that introduced `answer`.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
apiVersion: astromesh/v1
|
|
2
|
-
kind: Agent
|
|
3
|
-
metadata:
|
|
4
|
-
name: {{ name }}
|
|
5
|
-
spec:
|
|
6
|
-
identity:
|
|
7
|
-
display_name: "{{ display_name }}"
|
|
8
|
-
description: "{{ description }}"
|
|
9
|
-
model:
|
|
10
|
-
primary:
|
|
11
|
-
provider: {{ provider }}
|
|
12
|
-
model: {{ model }}
|
|
13
|
-
temperature: {{ temperature }}
|
|
14
|
-
max_tokens: {{ max_tokens }}
|
|
15
|
-
routing:
|
|
16
|
-
strategy: quality_first
|
|
17
|
-
prompts:
|
|
18
|
-
system: |
|
|
19
|
-
You are {{ display_name }}, a helpful AI assistant.
|
|
20
|
-
orchestration:
|
|
21
|
-
pattern: {{ orchestration_pattern }}
|
|
22
|
-
max_iterations: 10
|
|
23
|
-
timeout: 30
|
|
24
|
-
{% if tools %}
|
|
25
|
-
tools:
|
|
26
|
-
{% for tool in tools %}
|
|
27
|
-
- name: {{ tool }}
|
|
28
|
-
{% endfor %}
|
|
29
|
-
{% endif %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|