failproofai 1.0.12-beta.2 → 1.0.12
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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pwj2h2efe5f-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00l2u6j86cb8y._.js → [root-of-the-server]__167mipqmjprq5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bseae9u7g98r._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0mouhaflye1ro._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1fw0_nzkae5ao._.js → _1d98fyfrcaqhy._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_20hn4gn87nsjc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr0vt64h._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1fv03uigni8s5.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0janj0i6dqko7._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{1c_jf6g5wnslk.js → 0el9n3h9_zp5m.js} +1 -1
- package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
- package/.next/standalone/.next/static/chunks/{2o6d37j425uci.js → 0umzct31duk5g.js} +1 -1
- package/.next/standalone/.next/static/chunks/{42xptz88mtq3x.js → 0wobqgosirs8p.js} +1 -1
- package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ay__-d2h1xb5.js → 1kduj74-715jh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{40oworn_-x12p.js → 2701amhrbsbot.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tbm3vjm62h-2.js → 27cavoc8paelg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1my3-5p47t7r-.js → 2ve23a7keupjw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{21hckpmsc4g70.js → 3bv6xaoitjpnh.js} +2 -2
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +19 -0
- package/.next/standalone/fp-cloud-cli/README.md +120 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +198 -7
- package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +187 -81
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +168 -16
- package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
- package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
- package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
- package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
- package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
- package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
- package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +85 -37
- package/dist/worker.mjs +73 -28
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +9 -9
- package/src/hooks/agent-skill.ts +3 -0
- package/src/hooks/fp-home.ts +1 -1
- package/src/hooks/handler.ts +22 -3
- package/src/hooks/semantic/intent.ts +127 -24
- package/src/hooks/semantic/jev-review.ts +21 -2
- package/.next/standalone/.next/static/chunks/20py8k1gd_i1m.js +0 -1
- package/.next/standalone/.next/static/chunks/2hmex93ppj1ke.js +0 -1
- /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LHCtFxVG0mdvd4lsXAOFC → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Author, test and deploy managed evaluators, independently of result readers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from .. import client as api, cloud_output, dates, evaluator_spec as spec, output
|
|
13
|
+
from .._context import GLOBALS_EPILOG, require_auth
|
|
14
|
+
from ..errors import ApiError, NotFoundError
|
|
15
|
+
from . import _cloud, _write
|
|
16
|
+
|
|
17
|
+
PATH = "/api/evaluator-definitions"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def definitions(cctx) -> list:
|
|
21
|
+
return _cloud.rows(api.cloud_request(cctx, "GET", PATH), "definitions")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def select(items: list, handle: str) -> dict:
|
|
25
|
+
key, _, version = handle.partition("@")
|
|
26
|
+
matches = [r for r in items if r.get("eval_key") == key and (not version or str(r.get("eval_version")) == version)]
|
|
27
|
+
if not matches:
|
|
28
|
+
raise NotFoundError(f"no evaluator {handle}; run `fp evaluators list --all`")
|
|
29
|
+
if not version:
|
|
30
|
+
enabled = [r for r in matches if r.get("enabled", True)]
|
|
31
|
+
matches = enabled or matches
|
|
32
|
+
if len(matches) != 1:
|
|
33
|
+
spec.fail(f"{handle} matches multiple versions; use KEY@VERSION")
|
|
34
|
+
return matches[0]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_defs(ctx: typer.Context, key: Optional[str] = typer.Option(None, "--key"),
|
|
38
|
+
all_versions: bool = typer.Option(False, "--all")) -> None:
|
|
39
|
+
"""List definitions (enabled only unless --all). Needs evaluations:read."""
|
|
40
|
+
items = definitions(require_auth(ctx.obj))
|
|
41
|
+
items = [r for r in items if (not key or r.get("eval_key") == key) and (all_versions or r.get("enabled", True))]
|
|
42
|
+
_cloud.emit(ctx.obj, items, "Evaluators")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def show(ctx: typer.Context, handle: str = typer.Argument(..., metavar="KEY[@VERSION]")) -> None:
|
|
46
|
+
"""Show one definition, including its full source."""
|
|
47
|
+
_cloud.emit(ctx.obj, select(definitions(require_auth(ctx.obj)), handle), "Evaluator")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _create(state, cctx, body, sessions, disabled=False, force=False) -> dict:
|
|
51
|
+
if sessions:
|
|
52
|
+
if spec.source_kind(body) == "jev":
|
|
53
|
+
output.warn("Jev dry-runs spend model budget.")
|
|
54
|
+
preview = api.cloud_request(cctx, "POST", f"{PATH}/dry-run", body=spec.dry_run_body(body, sessions))
|
|
55
|
+
if not state.json:
|
|
56
|
+
cloud_output.dry_run(preview)
|
|
57
|
+
if dry_run_failed(preview) and force:
|
|
58
|
+
output.warn("Dry-run failed; --force authorizes creating the evaluator anyway.")
|
|
59
|
+
else:
|
|
60
|
+
check_dry_run(preview)
|
|
61
|
+
_write.confirm(state, f"Create evaluator {body['eval_key']}@{body['eval_version']}", assume_yes=False)
|
|
62
|
+
row = api.cloud_request(cctx, "POST", PATH, body=body)
|
|
63
|
+
if not isinstance(row, dict) or not row.get("id"):
|
|
64
|
+
raise ApiError("Cloud created no identifiable evaluator; check evaluators list --all before retrying")
|
|
65
|
+
if disabled:
|
|
66
|
+
api.cloud_request(cctx, "PATCH", f"{PATH}/{row['id']}", body={"enabled": False})
|
|
67
|
+
row = {**row, "enabled": False}
|
|
68
|
+
_write.record_action("evaluator_created", resource="evaluator", mode=spec.source_kind(body), success=True)
|
|
69
|
+
return row
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create(
|
|
73
|
+
ctx: typer.Context,
|
|
74
|
+
key: Optional[str] = typer.Option(None, "--key"),
|
|
75
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
76
|
+
version: Optional[str] = typer.Option(None, "--version"),
|
|
77
|
+
label: Optional[List[str]] = typer.Option(None, "--label"),
|
|
78
|
+
condition: Optional[str] = typer.Option(None, "--condition", help="Python expression or @file."),
|
|
79
|
+
code: Optional[str] = typer.Option(None, "--code", help="Restricted Python expression file, or - for stdin."),
|
|
80
|
+
result_kind: Optional[str] = typer.Option(None, "--result-kind"),
|
|
81
|
+
timeout: Optional[float] = typer.Option(None, "--timeout"),
|
|
82
|
+
judge: bool = typer.Option(False, "--judge"),
|
|
83
|
+
criteria: Optional[str] = typer.Option(None, "--criteria"),
|
|
84
|
+
step: Optional[List[str]] = typer.Option(None, "--step"),
|
|
85
|
+
threshold: Optional[float] = typer.Option(None, "--threshold"),
|
|
86
|
+
jev: Optional[str] = typer.Option(None, "--jev", help="noul or score."),
|
|
87
|
+
instructions: Optional[str] = typer.Option(None, "--instructions"),
|
|
88
|
+
true: Optional[str] = typer.Option(None, "--true"),
|
|
89
|
+
false: Optional[str] = typer.Option(None, "--false"),
|
|
90
|
+
level: Optional[List[str]] = typer.Option(None, "--level", help="3–5 distinct levels, worst to best."),
|
|
91
|
+
file: Optional[str] = typer.Option(None, "--file"),
|
|
92
|
+
dry_run_on: Optional[List[str]] = typer.Option(None, "--dry-run-on"),
|
|
93
|
+
disabled: bool = typer.Option(False, "--disabled"),
|
|
94
|
+
force: bool = typer.Option(False, "--force", help="Create despite failed --dry-run-on results (review the failures first)."),
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Create a code, LLM judge or Jev evaluator (enabled by default).
|
|
97
|
+
|
|
98
|
+
Needs evaluations:trigger; session dry-run also needs events:read.
|
|
99
|
+
|
|
100
|
+
Code: `fp --json evaluators create --key concise --name Concise --code concise.py --result-kind score --dry-run-on run-001`
|
|
101
|
+
|
|
102
|
+
Judge: `fp --json evaluators create --key relevance --name Relevance --judge --criteria "Grade relevance to the question" --step "Assess answer specificity" --threshold 0.7`
|
|
103
|
+
|
|
104
|
+
Jev: `fp --json evaluators create --key resolved --name Resolved --jev noul --instructions "Assess whether the request was resolved" --true "Fully resolved" --false "Not resolved" --threshold 0.8`
|
|
105
|
+
|
|
106
|
+
Spec: `fp --json evaluators create --file evaluator.json`
|
|
107
|
+
"""
|
|
108
|
+
body = spec.build(
|
|
109
|
+
key=key, name=name, version=version, label=label, condition=condition,
|
|
110
|
+
code=code, result_kind=result_kind, timeout=timeout, judge=judge, criteria=criteria,
|
|
111
|
+
step=step, threshold_value=threshold, jev=jev, instructions=instructions,
|
|
112
|
+
true=true, false=false, level=level, file=file,
|
|
113
|
+
)
|
|
114
|
+
# Validate before any request, including the judge refusal / session bound.
|
|
115
|
+
if dry_run_on:
|
|
116
|
+
spec.dry_run_body(body, dry_run_on)
|
|
117
|
+
row = _create(ctx.obj, require_auth(ctx.obj), body, dry_run_on, disabled, force)
|
|
118
|
+
if ctx.obj.json:
|
|
119
|
+
output.emit_json(row)
|
|
120
|
+
else:
|
|
121
|
+
cloud_output.evaluator({**body, **row}, "Evaluator created")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def deploy(
|
|
125
|
+
ctx: typer.Context,
|
|
126
|
+
key: str = typer.Argument(..., metavar="EVAL_KEY"),
|
|
127
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
128
|
+
version: Optional[str] = typer.Option(None, "--version"),
|
|
129
|
+
label: Optional[List[str]] = typer.Option(None, "--label"),
|
|
130
|
+
condition: Optional[str] = typer.Option(None, "--condition"),
|
|
131
|
+
code: Optional[str] = typer.Option(None, "--code"),
|
|
132
|
+
result_kind: Optional[str] = typer.Option(None, "--result-kind"),
|
|
133
|
+
timeout: Optional[float] = typer.Option(None, "--timeout"),
|
|
134
|
+
judge: bool = typer.Option(False, "--judge"),
|
|
135
|
+
criteria: Optional[str] = typer.Option(None, "--criteria"),
|
|
136
|
+
step: Optional[List[str]] = typer.Option(None, "--step"),
|
|
137
|
+
threshold: Optional[float] = typer.Option(None, "--threshold"),
|
|
138
|
+
jev: Optional[str] = typer.Option(None, "--jev"),
|
|
139
|
+
instructions: Optional[str] = typer.Option(None, "--instructions"),
|
|
140
|
+
true: Optional[str] = typer.Option(None, "--true"),
|
|
141
|
+
false: Optional[str] = typer.Option(None, "--false"),
|
|
142
|
+
level: Optional[List[str]] = typer.Option(None, "--level"),
|
|
143
|
+
file: Optional[str] = typer.Option(None, "--file"),
|
|
144
|
+
dry_run_on: Optional[List[str]] = typer.Option(None, "--dry-run-on"),
|
|
145
|
+
backfill: Optional[str] = typer.Option(None, "--backfill", help="Score stored sessions too, e.g. 7d (up to 90d). Judge/Jev backfills spend model budget."),
|
|
146
|
+
force: bool = typer.Option(False, "--force", help="Deploy despite failed --dry-run-on results (review the failures first)."),
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Publish a NEW immutable version, then disable enabled predecessors.
|
|
149
|
+
|
|
150
|
+
Numeric versions auto-increment; for other version names pass --version.
|
|
151
|
+
A failed predecessor PATCH leaves the new version enabled and reports its id.
|
|
152
|
+
"""
|
|
153
|
+
body = spec.build(
|
|
154
|
+
key=None if file else key, name=name, version=None if file else version, label=label,
|
|
155
|
+
condition=condition, code=code, result_kind=result_kind, timeout=timeout,
|
|
156
|
+
judge=judge, criteria=criteria, step=step, threshold_value=threshold, jev=jev,
|
|
157
|
+
instructions=instructions, true=true, false=false, level=level, file=file, complete=False,
|
|
158
|
+
)
|
|
159
|
+
if file and body.get("eval_key") is not None and body["eval_key"] != key:
|
|
160
|
+
spec.fail("The spec eval_key must match the deploy argument")
|
|
161
|
+
body["eval_key"] = key
|
|
162
|
+
if version is not None:
|
|
163
|
+
body["eval_version"] = version
|
|
164
|
+
if dry_run_on:
|
|
165
|
+
spec.dry_run_body(body, dry_run_on)
|
|
166
|
+
backfill_body = None
|
|
167
|
+
if backfill:
|
|
168
|
+
if not re.fullmatch(r"[1-9][0-9]*d", backfill) or int(backfill[:-1]) > 90:
|
|
169
|
+
spec.fail("--backfill must be a duration in days from 1d to 90d")
|
|
170
|
+
now = datetime.now(timezone.utc)
|
|
171
|
+
backfill_body = {
|
|
172
|
+
"from": dates._iso(now - timedelta(days=int(backfill[:-1]))),
|
|
173
|
+
"to": dates._iso(now),
|
|
174
|
+
"eval_key": key,
|
|
175
|
+
"dry_run": False,
|
|
176
|
+
}
|
|
177
|
+
if spec.source_kind(body) != "code":
|
|
178
|
+
output.warn("Backfilling a judge/Jev evaluator spends the org's model budget.")
|
|
179
|
+
cctx = require_auth(ctx.obj)
|
|
180
|
+
existing = [r for r in definitions(cctx) if r.get("eval_key") == key]
|
|
181
|
+
if "eval_version" not in body:
|
|
182
|
+
versions = [str(r.get("eval_version", "")) for r in existing]
|
|
183
|
+
if any(not v.isdigit() for v in versions):
|
|
184
|
+
spec.fail("Non-numeric versions exist; specify a new --version")
|
|
185
|
+
body["eval_version"] = str(max([int(v) for v in versions] or [0]) + 1)
|
|
186
|
+
if any(str(r.get("eval_version")) == body["eval_version"] for r in existing):
|
|
187
|
+
spec.fail(f"Version {key}@{body['eval_version']} already exists; deploy needs a NEW version")
|
|
188
|
+
body.setdefault("display_name", existing[-1].get("display_name", key) if existing else key)
|
|
189
|
+
if existing and not file:
|
|
190
|
+
# A new version keeps the previous version's scope and labels unless a flag
|
|
191
|
+
# changes them; silently dropping the condition would widen the evaluator to
|
|
192
|
+
# every session. A --file spec states every field itself.
|
|
193
|
+
latest = max(existing, key=lambda r: str(r.get("created_at") or ""))
|
|
194
|
+
if condition is None:
|
|
195
|
+
body["condition_source"] = latest.get("condition_source")
|
|
196
|
+
if not label:
|
|
197
|
+
body["labels"] = list(latest.get("labels") or [])
|
|
198
|
+
spec.validate(body)
|
|
199
|
+
created = _create(ctx.obj, cctx, body, dry_run_on, force=force)
|
|
200
|
+
disabled_ids = []
|
|
201
|
+
try:
|
|
202
|
+
for old in existing:
|
|
203
|
+
if old.get("enabled", True):
|
|
204
|
+
api.cloud_request(cctx, "PATCH", f"{PATH}/{old['id']}", body={"enabled": False})
|
|
205
|
+
disabled_ids.append(old["id"])
|
|
206
|
+
if backfill_body:
|
|
207
|
+
api.cloud_request(cctx, "POST", "/api/evaluations/backfill", body=backfill_body)
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
output.warn(f"Published {key}@{body['eval_version']} (id {created['id']}); disabled {len(disabled_ids)} predecessors. Follow-up failed; inspect evaluators list --all before retrying.")
|
|
210
|
+
raise exc
|
|
211
|
+
_write.record_action("evaluator_deployed", resource="evaluator", success=True)
|
|
212
|
+
if ctx.obj.json:
|
|
213
|
+
output.emit_json({"created": created, "disabled": disabled_ids})
|
|
214
|
+
else:
|
|
215
|
+
cloud_output.deployed({**body, **created}, [r for r in existing if r["id"] in disabled_ids])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def draft(
|
|
219
|
+
ctx: typer.Context,
|
|
220
|
+
intent: str = typer.Argument(...),
|
|
221
|
+
kind: str = typer.Option("code", "--kind"),
|
|
222
|
+
out: str = typer.Option("evaluator.json", "--out"),
|
|
223
|
+
) -> None:
|
|
224
|
+
"""AI-compose an API spec file. Session only; review it before deployment."""
|
|
225
|
+
_cloud.session_only(ctx.obj, "evaluators draft")
|
|
226
|
+
if kind not in ("code", "judge", "jev"):
|
|
227
|
+
spec.fail("--kind must be code, judge or jev")
|
|
228
|
+
data = api.cloud_request(require_auth(ctx.obj), "POST", "/api/agent/compose-eval",
|
|
229
|
+
body={"intent": f"Author a {kind} evaluator. {intent}", "resultKind": "score"})
|
|
230
|
+
if not isinstance(data, dict):
|
|
231
|
+
spec.fail("Cloud draft was not an evaluator JSON spec")
|
|
232
|
+
body = spec.from_draft(data, kind)
|
|
233
|
+
_cloud.write_file(out, json.dumps(body, indent=2, ensure_ascii=False) + "\n")
|
|
234
|
+
_cloud.emit(ctx.obj, {"out": out, "spec": body}, "Evaluator draft")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def dry_run(
|
|
238
|
+
ctx: typer.Context,
|
|
239
|
+
code: Optional[str] = typer.Option(None, "--code"),
|
|
240
|
+
result_kind: Optional[str] = typer.Option(None, "--result-kind"),
|
|
241
|
+
timeout: Optional[float] = typer.Option(None, "--timeout"),
|
|
242
|
+
condition: Optional[str] = typer.Option(None, "--condition"),
|
|
243
|
+
judge: bool = typer.Option(False, "--judge"),
|
|
244
|
+
criteria: Optional[str] = typer.Option(None, "--criteria"),
|
|
245
|
+
step: Optional[List[str]] = typer.Option(None, "--step"),
|
|
246
|
+
threshold: Optional[float] = typer.Option(None, "--threshold"),
|
|
247
|
+
jev: Optional[str] = typer.Option(None, "--jev"),
|
|
248
|
+
instructions: Optional[str] = typer.Option(None, "--instructions"),
|
|
249
|
+
true: Optional[str] = typer.Option(None, "--true"),
|
|
250
|
+
false: Optional[str] = typer.Option(None, "--false"),
|
|
251
|
+
level: Optional[List[str]] = typer.Option(None, "--level"),
|
|
252
|
+
file: Optional[str] = typer.Option(None, "--file"),
|
|
253
|
+
session: Optional[List[str]] = typer.Option(None, "--session"),
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Compile only, or run on ≤10 sessions. Cloud refuses judge dry-runs.
|
|
256
|
+
|
|
257
|
+
Jev dry-runs execute model calls and spend budget, including before deployment.
|
|
258
|
+
"""
|
|
259
|
+
body = spec.build(
|
|
260
|
+
code=code, result_kind=result_kind, timeout=timeout, condition=condition,
|
|
261
|
+
judge=judge, criteria=criteria, step=step, threshold_value=threshold, jev=jev,
|
|
262
|
+
instructions=instructions, true=true, false=false, level=level, file=file, complete=False,
|
|
263
|
+
)
|
|
264
|
+
body = spec.dry_run_body(body, session)
|
|
265
|
+
if spec.source_kind(body) == "jev":
|
|
266
|
+
output.warn("Jev dry-runs spend model budget.")
|
|
267
|
+
result = api.cloud_request(require_auth(ctx.obj), "POST", f"{PATH}/dry-run", body=body)
|
|
268
|
+
_cloud.emit(ctx.obj, result, "Evaluator dry-run")
|
|
269
|
+
if dry_run_failed(result):
|
|
270
|
+
# Preserve the full dry-run result in --json, but do not signal success.
|
|
271
|
+
raise typer.Exit(code=1)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def dry_run_failed(result):
|
|
275
|
+
return isinstance(result, dict) and (
|
|
276
|
+
result.get("ok") is False
|
|
277
|
+
or any(r.get("ok") is False for r in result.get("runs", []) if isinstance(r, dict))
|
|
278
|
+
or result.get("summary", {}).get("failed", 0) > 0
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def check_dry_run(result):
|
|
283
|
+
if dry_run_failed(result):
|
|
284
|
+
failures = [r for r in result.get("runs", []) if r.get("ok") is False]
|
|
285
|
+
detail = "; ".join(str(r.get("error_message") or r.get("error_kind") or "run failed") for r in failures)
|
|
286
|
+
raise ApiError(f"Dry-run failed; evaluator was not created. {detail}".strip())
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def toggle(ctx, handle, enabled):
|
|
290
|
+
cctx = require_auth(ctx.obj)
|
|
291
|
+
row = select(definitions(cctx), handle)
|
|
292
|
+
_write.confirm(ctx.obj, f"{'Enable' if enabled else 'Disable'} {handle}", assume_yes=False)
|
|
293
|
+
result = api.cloud_request(cctx, "PATCH", f"{PATH}/{row['id']}", body={"enabled": enabled})
|
|
294
|
+
_write.record_action("evaluator_toggled", resource="evaluator", success=True)
|
|
295
|
+
if ctx.obj.json:
|
|
296
|
+
output.emit_json(result or {**row, "enabled": enabled})
|
|
297
|
+
else:
|
|
298
|
+
cloud_output.evaluator({**row, **(result or {}), "enabled": enabled})
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def enable(ctx: typer.Context, handle: str = typer.Argument(...)) -> None:
|
|
302
|
+
"""Enable KEY[@VERSION]."""
|
|
303
|
+
toggle(ctx, handle, True)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def disable(ctx: typer.Context, handle: str = typer.Argument(...)) -> None:
|
|
307
|
+
"""Disable KEY[@VERSION]."""
|
|
308
|
+
toggle(ctx, handle, False)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def label_def(ctx: typer.Context, handle: str = typer.Argument(...),
|
|
312
|
+
add: Optional[List[str]] = typer.Option(None, "--add"),
|
|
313
|
+
remove: Optional[List[str]] = typer.Option(None, "--remove")) -> None:
|
|
314
|
+
"""Add/remove labels on KEY[@VERSION]."""
|
|
315
|
+
if not add and not remove:
|
|
316
|
+
spec.fail("Pass --add or --remove")
|
|
317
|
+
if set(add or []) & set(remove or []):
|
|
318
|
+
spec.fail("A label cannot be both added and removed")
|
|
319
|
+
cctx = require_auth(ctx.obj)
|
|
320
|
+
row = select(definitions(cctx), handle)
|
|
321
|
+
labels = list(dict.fromkeys([v for v in row.get("labels", []) if v not in (remove or [])] + (add or [])))
|
|
322
|
+
spec.validate_labels(labels)
|
|
323
|
+
_write.confirm(ctx.obj, f"Label {handle}", assume_yes=False)
|
|
324
|
+
data = api.cloud_request(cctx, "PATCH", f"{PATH}/{row['id']}", body={"labels": labels})
|
|
325
|
+
if ctx.obj.json:
|
|
326
|
+
output.emit_json(data or {**row, "labels": labels})
|
|
327
|
+
else:
|
|
328
|
+
cloud_output.evaluator({**row, **(data or {}), "labels": labels}, "Evaluator labels")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def instances(ctx: typer.Context) -> None:
|
|
332
|
+
"""List managed evaluator instances."""
|
|
333
|
+
_cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/evaluator-instances"), "Instances")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def schema(ctx: typer.Context) -> None:
|
|
337
|
+
"""Schema profile. Needs evaluations:trigger and events:read."""
|
|
338
|
+
_cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/evaluator-schema-profile"), "Schema profile")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def jobs(ctx: typer.Context) -> None:
|
|
342
|
+
"""List evaluation jobs."""
|
|
343
|
+
_cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/evaluation-jobs"), "Evaluation jobs")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def backfill(ctx: typer.Context, frm: str = typer.Option(..., "--from"),
|
|
347
|
+
to: str = typer.Option(..., "--to"), key: Optional[str] = typer.Option(None, "--key"),
|
|
348
|
+
dry_run: bool = typer.Option(False, "--dry-run")) -> None:
|
|
349
|
+
"""Score stored sessions an evaluator has not scored yet (at most 90 days).
|
|
350
|
+
|
|
351
|
+
Covers code, LLM judge and Jev evaluators (every enabled one, or only --key).
|
|
352
|
+
Only missing (session, evaluator) pairs are queued; --dry-run counts them.
|
|
353
|
+
Needs evaluations:trigger.
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
frm = dates._validate_iso(frm, "--from")
|
|
357
|
+
to = dates._validate_iso(to, "--to")
|
|
358
|
+
start = datetime.fromisoformat(frm.replace("Z", "+00:00"))
|
|
359
|
+
end = datetime.fromisoformat(to.replace("Z", "+00:00"))
|
|
360
|
+
except ValueError as exc:
|
|
361
|
+
spec.fail(str(exc))
|
|
362
|
+
if end <= start or end - start > timedelta(days=90):
|
|
363
|
+
spec.fail("Backfill needs an increasing range of at most 90 days")
|
|
364
|
+
if not dry_run:
|
|
365
|
+
output.warn("Judge and Jev evaluators in the backfill spend the org's model budget; --dry-run counts the work first.")
|
|
366
|
+
_write.confirm(ctx.obj, "Backfill evaluations", assume_yes=dry_run)
|
|
367
|
+
_cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "POST", "/api/evaluations/backfill",
|
|
368
|
+
body={"from": dates._iso(start), "to": dates._iso(end), "eval_key": key, "dry_run": dry_run}), "Evaluation backfill")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def register(app: typer.Typer) -> None:
|
|
372
|
+
group = typer.Typer(no_args_is_help=True, rich_markup_mode="markdown", help="Author, test and deploy code, LLM-judge and Jev evaluators.", context_settings={"help_option_names": ["-h", "--help"]})
|
|
373
|
+
for name, fn in (
|
|
374
|
+
("list", list_defs), ("show", show), ("create", create), ("deploy", deploy),
|
|
375
|
+
("draft", draft), ("dry-run", dry_run), ("enable", enable), ("disable", disable),
|
|
376
|
+
("label", label_def), ("instances", instances), ("schema", schema), ("jobs", jobs),
|
|
377
|
+
("backfill", backfill),
|
|
378
|
+
):
|
|
379
|
+
group.command(name, epilog=GLOBALS_EPILOG)(fn)
|
|
380
|
+
app.add_typer(group, name="evaluators")
|
|
@@ -70,6 +70,8 @@ def events(
|
|
|
70
70
|
* `fp --json events --session-id run-001 --all` — a run's full timeline: event types + summaries (light, fast)
|
|
71
71
|
* `fp --json events --full --session-id run-001 --all | jq '.events[].payload'` — that run's raw payloads (full feed, bounded to one run)
|
|
72
72
|
"""
|
|
73
|
+
if ctx.invoked_subcommand:
|
|
74
|
+
return
|
|
73
75
|
state: AppState = ctx.obj
|
|
74
76
|
cctx = require_auth(state)
|
|
75
77
|
frm, to = resolve_dates(since, ts_from, ts_to)
|
|
@@ -155,5 +157,16 @@ def events(
|
|
|
155
157
|
output.recheck_filters_hint(active_filters)
|
|
156
158
|
|
|
157
159
|
|
|
160
|
+
def latency(ctx: typer.Context, since: Optional[str] = typer.Option(None, "--since")) -> None:
|
|
161
|
+
"""Aggregate event latency."""
|
|
162
|
+
from . import _cloud
|
|
163
|
+
frm, to = resolve_dates(since, None, None)
|
|
164
|
+
_cloud.emit(ctx.obj, api.cloud_request(require_auth(ctx.obj), "GET", "/api/events/latency_aggregate",
|
|
165
|
+
params={"ts_from": frm, "ts_to": to}), "Event latency")
|
|
166
|
+
|
|
167
|
+
|
|
158
168
|
def register(app: typer.Typer) -> None:
|
|
159
|
-
|
|
169
|
+
group = typer.Typer(invoke_without_command=True, rich_markup_mode="markdown", context_settings={"help_option_names": ["-h", "--help"]})
|
|
170
|
+
group.callback(epilog=GLOBALS_EPILOG)(events)
|
|
171
|
+
group.command("latency", epilog=GLOBALS_EPILOG)(latency)
|
|
172
|
+
app.add_typer(group, name="events")
|
|
@@ -14,7 +14,8 @@ import typer
|
|
|
14
14
|
|
|
15
15
|
from .. import client as api
|
|
16
16
|
from .. import output
|
|
17
|
-
from ..
|
|
17
|
+
from .. import _click_compat as click
|
|
18
|
+
from .._context import GLOBALS_EPILOG, AppState, require_auth, resolve_dates, validate_limit
|
|
18
19
|
from ..errors import ApiError, ForbiddenError, NotFoundError
|
|
19
20
|
from . import _write
|
|
20
21
|
|
|
@@ -68,12 +69,40 @@ def _fail(state: AppState, exc: Exception, *, incident_id: str = "") -> None:
|
|
|
68
69
|
raise exc
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def _issue_filters(
|
|
73
|
+
agent_id, env, machine, source, audit_id, assignee, archived, search, since,
|
|
74
|
+
) -> dict:
|
|
75
|
+
if archived is not None and archived not in ("false", "true", "only", "all"):
|
|
76
|
+
raise click.UsageError("--archived must be false, only or all")
|
|
77
|
+
if source is not None:
|
|
78
|
+
bad = [s for s in source.split(",") if s.strip() and s.strip() not in ("alert", "audit", "manual")]
|
|
79
|
+
if bad:
|
|
80
|
+
raise click.UsageError("--source must be alert, audit or manual (CSV)")
|
|
81
|
+
opened_from = None
|
|
82
|
+
if since is not None:
|
|
83
|
+
opened_from, _ = resolve_dates(since, None, None)
|
|
84
|
+
return {
|
|
85
|
+
"agent": agent_id, "environment": env, "machine": machine, "source": source,
|
|
86
|
+
"audit_id": audit_id, "assignee": assignee, "archived": archived, "q": search,
|
|
87
|
+
"opened_from": opened_from,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
71
91
|
def incidents_list(
|
|
72
92
|
ctx: typer.Context,
|
|
73
93
|
state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s): firing, acknowledged, resolved, closed (CSV)."),
|
|
74
94
|
alert_id: Optional[str] = typer.Option(None, "--alert-id", help="Only incidents for this alert."),
|
|
75
95
|
limit: int = typer.Option(50, "--limit", "-n", help="Max incidents to return."),
|
|
76
96
|
show_id: bool = typer.Option(False, "--show-id", help="Show the full incident id instead of the short form (always full in --json)."),
|
|
97
|
+
agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Only issues touching these agent ids (CSV)."),
|
|
98
|
+
env: Optional[str] = typer.Option(None, "--env", help="Only these environments (CSV)."),
|
|
99
|
+
machine: Optional[str] = typer.Option(None, "--machine", help="Only these machine ids (CSV)."),
|
|
100
|
+
source: Optional[str] = typer.Option(None, "--source", help="alert, audit and/or manual (CSV)."),
|
|
101
|
+
audit_id: Optional[str] = typer.Option(None, "--audit-id", help="Only issues from these audits (CSV of ids)."),
|
|
102
|
+
assignee: Optional[str] = typer.Option(None, "--assignee", help="Emails, `me` or `unassigned` (CSV)."),
|
|
103
|
+
archived: Optional[str] = typer.Option(None, "--archived", help="false (default), only or all."),
|
|
104
|
+
search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive text in the title or summary."),
|
|
105
|
+
since: Optional[str] = typer.Option(None, "--since", help="Opened within: 15m 1h 6h 24h 7d."),
|
|
77
106
|
) -> None:
|
|
78
107
|
"""List incidents in a boxed table (newest-opened first).
|
|
79
108
|
|
|
@@ -94,7 +123,8 @@ def incidents_list(
|
|
|
94
123
|
_validate_states(state_filter)
|
|
95
124
|
validate_limit(limit)
|
|
96
125
|
cctx = require_auth(state)
|
|
97
|
-
|
|
126
|
+
extra = _issue_filters(agent_id, env, machine, source, audit_id, assignee, archived, search, since)
|
|
127
|
+
incidents = api.list_incidents(cctx, state=state_filter, alert_id=alert_id, limit=limit, filters=extra)
|
|
98
128
|
if state.json:
|
|
99
129
|
output.emit_json({"issues": incidents})
|
|
100
130
|
return
|
|
@@ -105,6 +135,16 @@ def incidents_list(
|
|
|
105
135
|
def incidents_count(
|
|
106
136
|
ctx: typer.Context,
|
|
107
137
|
state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s) (CSV)."),
|
|
138
|
+
alert_id: Optional[str] = typer.Option(None, "--alert-id", help="Only issues from these alerts (CSV of ids)."),
|
|
139
|
+
agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Only issues touching these agent ids (CSV)."),
|
|
140
|
+
env: Optional[str] = typer.Option(None, "--env", help="Only these environments (CSV)."),
|
|
141
|
+
machine: Optional[str] = typer.Option(None, "--machine", help="Only these machine ids (CSV)."),
|
|
142
|
+
source: Optional[str] = typer.Option(None, "--source", help="alert, audit and/or manual (CSV)."),
|
|
143
|
+
audit_id: Optional[str] = typer.Option(None, "--audit-id", help="Only issues from these audits (CSV of ids)."),
|
|
144
|
+
assignee: Optional[str] = typer.Option(None, "--assignee", help="Emails, `me` or `unassigned` (CSV)."),
|
|
145
|
+
archived: Optional[str] = typer.Option(None, "--archived", help="false (default), only or all."),
|
|
146
|
+
search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive text in the title or summary."),
|
|
147
|
+
since: Optional[str] = typer.Option(None, "--since", help="Opened within: 15m 1h 6h 24h 7d."),
|
|
108
148
|
) -> None:
|
|
109
149
|
"""Count incidents (optionally by state) as a compact stat card.
|
|
110
150
|
|
|
@@ -118,7 +158,10 @@ def incidents_count(
|
|
|
118
158
|
state: AppState = ctx.obj
|
|
119
159
|
_validate_states(state_filter)
|
|
120
160
|
cctx = require_auth(state)
|
|
121
|
-
|
|
161
|
+
extra = _issue_filters(agent_id, env, machine, source, audit_id, assignee, archived, search, since)
|
|
162
|
+
if alert_id:
|
|
163
|
+
extra["alert_id"] = alert_id
|
|
164
|
+
count = api.count_incidents(cctx, state=state_filter, filters=extra)
|
|
122
165
|
if state.json:
|
|
123
166
|
output.emit_json({"count": count})
|
|
124
167
|
else:
|
|
@@ -261,10 +304,11 @@ def incidents_close(
|
|
|
261
304
|
|
|
262
305
|
For an issue that came from an audit, this also marks the finding `dismissed`. It does
|
|
263
306
|
NOT write the finding's org-wide suppression: closing one issue never hides that pattern
|
|
264
|
-
in your other audits. Use `fp audits finding-
|
|
307
|
+
in your other audits. Use `fp audits dismiss <finding-id>` for that.
|
|
265
308
|
|
|
266
|
-
Needs `issues:close`. Exits
|
|
267
|
-
and closing must not overwrite the record that someone
|
|
309
|
+
Needs `issues:close`. Exits 1 (the server answers 409 conflict) if the issue already
|
|
310
|
+
ended — an issue ends once, and closing must not overwrite the record that someone
|
|
311
|
+
believed they had fixed it.
|
|
268
312
|
|
|
269
313
|
With `--json`: `{"closed": true, "id": "<id>"}` (or `{cancelled: true}` on a declined
|
|
270
314
|
prompt).
|
|
@@ -666,6 +710,37 @@ def incidents_open(
|
|
|
666
710
|
output.render_incident_opened(summary=summary, severity=sev, state=st, title=hero)
|
|
667
711
|
|
|
668
712
|
|
|
713
|
+
def incidents_facets(
|
|
714
|
+
ctx: typer.Context,
|
|
715
|
+
state_filter: Optional[str] = typer.Option(None, "--state", help="Filter by state(s) (CSV)."),
|
|
716
|
+
alert_id: Optional[str] = typer.Option(None, "--alert-id", help="Only issues from these alerts (CSV of ids)."),
|
|
717
|
+
agent_id: Optional[str] = typer.Option(None, "--agent-id", help="Only issues touching these agent ids (CSV)."),
|
|
718
|
+
env: Optional[str] = typer.Option(None, "--env", help="Only these environments (CSV)."),
|
|
719
|
+
machine: Optional[str] = typer.Option(None, "--machine", help="Only these machine ids (CSV)."),
|
|
720
|
+
source: Optional[str] = typer.Option(None, "--source", help="alert, audit and/or manual (CSV)."),
|
|
721
|
+
audit_id: Optional[str] = typer.Option(None, "--audit-id", help="Only issues from these audits (CSV of ids)."),
|
|
722
|
+
assignee: Optional[str] = typer.Option(None, "--assignee", help="Emails, `me` or `unassigned` (CSV)."),
|
|
723
|
+
archived: Optional[str] = typer.Option(None, "--archived", help="false (default), only or all."),
|
|
724
|
+
search: Optional[str] = typer.Option(None, "--search", help="Case-insensitive text in the title or summary."),
|
|
725
|
+
since: Optional[str] = typer.Option(None, "--since", help="Opened within: 15m 1h 6h 24h 7d."),
|
|
726
|
+
) -> None:
|
|
727
|
+
"""The values the issue filters can take right now (agents, environments, machines,
|
|
728
|
+
sources, assignees), with counts — the dashboard inbox's filter bar. Needs `issues:read`.
|
|
729
|
+
|
|
730
|
+
Example:
|
|
731
|
+
|
|
732
|
+
* `fp issues facets --state firing,acknowledged`
|
|
733
|
+
"""
|
|
734
|
+
from . import _cloud
|
|
735
|
+
state: AppState = ctx.obj
|
|
736
|
+
_validate_states(state_filter)
|
|
737
|
+
extra = _issue_filters(agent_id, env, machine, source, audit_id, assignee, archived, search, since)
|
|
738
|
+
params = {"state": state_filter, "alert_id": alert_id, **extra}
|
|
739
|
+
data = api.cloud_request(require_auth(state), "GET", "/api/issues/facets",
|
|
740
|
+
params={k: v for k, v in params.items() if v is not None})
|
|
741
|
+
_cloud.emit(state, data, "Issue facets")
|
|
742
|
+
|
|
743
|
+
|
|
669
744
|
def register(app: typer.Typer) -> None:
|
|
670
745
|
inc = typer.Typer(
|
|
671
746
|
no_args_is_help=True,
|
|
@@ -675,6 +750,7 @@ def register(app: typer.Typer) -> None:
|
|
|
675
750
|
)
|
|
676
751
|
inc.command("list", epilog=GLOBALS_EPILOG)(incidents_list)
|
|
677
752
|
inc.command("count", epilog=GLOBALS_EPILOG)(incidents_count)
|
|
753
|
+
inc.command("facets", epilog=GLOBALS_EPILOG)(incidents_facets)
|
|
678
754
|
inc.command("show", epilog=GLOBALS_EPILOG)(incidents_show)
|
|
679
755
|
inc.command("ack", epilog=GLOBALS_EPILOG)(incidents_ack)
|
|
680
756
|
inc.command("assign", epilog=GLOBALS_EPILOG)(incidents_assign)
|
|
@@ -24,6 +24,9 @@ _LIST_KINDS = {
|
|
|
24
24
|
"hooks": ("hook_names", "events:read", "Hook names seen across events.", "seen across events"),
|
|
25
25
|
"tools": ("tool_names", "events:read", "Tool names seen across events.", "seen across events"),
|
|
26
26
|
"error_types": ("error_types", "events:read", "Error types seen across events.", "seen across events"),
|
|
27
|
+
"trigger_events": ("trigger_events", "events:read", "Hook trigger events seen across events.", "seen across events"),
|
|
28
|
+
"eval_envs": ("eval_environments", "evaluations:read", "Environments that have evaluation results.", "with evaluation results"),
|
|
29
|
+
"metric_keys": ("metric_keys", "evaluations:read", "Evaluation metric keys (metrics, not scores).", "evaluation metric keys"),
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
def _make_list_cmd(name: str, facet_key: str, perm: str, summary: str, description: str):
|
|
@@ -56,7 +59,7 @@ def register(app: typer.Typer) -> None:
|
|
|
56
59
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
57
60
|
help="List the distinct values behind the dashboard's filter dropdowns. Subcommands: "
|
|
58
61
|
"**envs**, **agents**, **event_types**, **score_filters**, **models**, **hooks**, "
|
|
59
|
-
"**tools**, **error_types**.",
|
|
62
|
+
"**tools**, **error_types**, **trigger_events**, **eval_envs**, **metric_keys**.",
|
|
60
63
|
)
|
|
61
64
|
for name, (facet_key, perm, summary, description) in _LIST_KINDS.items():
|
|
62
65
|
list_app.command(name, epilog=GLOBALS_EPILOG)(_make_list_cmd(name, facet_key, perm, summary, description))
|
|
@@ -316,4 +316,21 @@ def register(app: typer.Typer) -> None:
|
|
|
316
316
|
orgs_app.command("switch", epilog=GLOBALS_EPILOG)(org_switch)
|
|
317
317
|
orgs_app.command("current", epilog=GLOBALS_EPILOG)(org_current)
|
|
318
318
|
orgs_app.command("perms", epilog=GLOBALS_EPILOG)(org_perms)
|
|
319
|
+
orgs_app.command("create", epilog=GLOBALS_EPILOG)(org_create)
|
|
319
320
|
app.add_typer(orgs_app, name="orgs")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def org_create(ctx: typer.Context, name: str = typer.Option(..., "--name"),
|
|
324
|
+
slug: Optional[str] = typer.Option(None, "--slug")) -> None:
|
|
325
|
+
"""Create an organization (session only; does not switch the saved org)."""
|
|
326
|
+
from . import _cloud
|
|
327
|
+
from .account_cmds import request
|
|
328
|
+
from .. import _click_compat as click
|
|
329
|
+
_cloud.session_only(ctx.obj, "orgs create")
|
|
330
|
+
if not name.strip():
|
|
331
|
+
raise click.UsageError("--name cannot be empty")
|
|
332
|
+
import re
|
|
333
|
+
slug = slug or re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
334
|
+
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", slug):
|
|
335
|
+
raise click.UsageError("--slug must be lowercase letters/digits separated by hyphens")
|
|
336
|
+
request(ctx, "POST", "/api/orgs", {"name": name, "slug": slug})
|