claritty-sdk 2.0.4__tar.gz → 2.2.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.
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/PKG-INFO +1 -1
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/__init__.py +9 -36
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/cli_verify.py +48 -1
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/context.py +3 -14
- claritty_sdk-2.2.0/claritty_sdk/decorators.py +116 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/graph.py +54 -105
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/llm.py +35 -4
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/manifest.py +415 -6
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/expressions.py +29 -8
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/platform_client.py +9 -7
- claritty_sdk-2.2.0/claritty_sdk/runtime/team_runner.py +285 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/tool_loop.py +27 -3
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/workflow_engine.py +336 -25
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/PKG-INFO +1 -1
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/SOURCES.txt +8 -5
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/pyproject.toml +1 -1
- claritty_sdk-2.2.0/tests/test_agent_reasoning.py +66 -0
- claritty_sdk-2.2.0/tests/test_execution_end_to_end.py +134 -0
- claritty_sdk-2.2.0/tests/test_manifest_doc_parity.py +44 -0
- claritty_sdk-2.2.0/tests/test_parallel_fanin.py +78 -0
- claritty_sdk-2.2.0/tests/test_team_mode.py +168 -0
- claritty_sdk-2.2.0/tests/test_workflow_engine_failfast.py +201 -0
- claritty_sdk-2.0.4/claritty_sdk/decorators.py +0 -283
- claritty_sdk-2.0.4/claritty_sdk/enums.py +0 -20
- claritty_sdk-2.0.4/claritty_sdk/executor.py +0 -154
- claritty_sdk-2.0.4/claritty_sdk/registry.py +0 -151
- claritty_sdk-2.0.4/claritty_sdk/trigger_manager.py +0 -164
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/README.md +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/cli_eval.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/eval.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/__init__.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/__init__.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/airtable.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/anthropic.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/asana.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/calendly.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/catalog.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/clickup.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/client.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/discord.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/dropbox.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/executor_client.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/github.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/gitlab.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/gmail.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/google_calendar.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/google_drive.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/hubspot.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/jira.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/linear.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/linkedin.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/mailchimp.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/mcp_client.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/microsoft_teams.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/notion.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/notion_mcp.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/openai.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/outlook.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/outlook_calendar.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/pipedrive.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/platform_creds.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/postgres.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/posthog.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/s3.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/salesforce.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/slack.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/stripe.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/trello.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/twilio.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/integrations/x_twitter.pyi +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/__init__.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/bootstrap.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/schema_validate.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/runtime/tracing.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk/validators.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/dependency_links.txt +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/entry_points.txt +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/requires.txt +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/claritty_sdk.egg-info/top_level.txt +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/setup.cfg +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/tests/test_graph_emitter.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/tests/test_integration_broker_dispatch.py +0 -0
- {claritty_sdk-2.0.4 → claritty_sdk-2.2.0}/tests/test_manifest_triggers.py +0 -0
|
@@ -32,10 +32,11 @@ LLM client (existing)::
|
|
|
32
32
|
|
|
33
33
|
from claritty_sdk.llm import get_llm_client
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
Workflows and triggers are declarative — they live in ``app.yaml``
|
|
36
|
+
(``intelligence.yaml``) and run on ``claritty_sdk.runtime``. The v1
|
|
37
|
+
decorator/registry runtime (``@workflow``/``@uses_agent``/``@trigger_template``,
|
|
38
|
+
``AgentRegistry``/``WorkflowRegistry``/``TriggerTemplateRegistry``,
|
|
39
|
+
``WorkflowExecutor``) has been removed — there is one canonical (v2) runtime.
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
from . import llm
|
|
@@ -45,24 +46,9 @@ from .decorators import (
|
|
|
45
46
|
get_registered_tool,
|
|
46
47
|
list_registered_tool_ids,
|
|
47
48
|
tool,
|
|
48
|
-
trigger_template,
|
|
49
|
-
uses_agent,
|
|
50
|
-
workflow,
|
|
51
|
-
)
|
|
52
|
-
from .enums import ExecutionMode, TriggerTemplateType
|
|
53
|
-
from .executor import WorkflowExecutor # deprecated; legacy apps still import
|
|
54
|
-
from .graph import build_graph # deprecated; legacy apps still import
|
|
55
|
-
from .registry import (
|
|
56
|
-
AgentMeta,
|
|
57
|
-
AgentRegistry,
|
|
58
|
-
TriggerMeta,
|
|
59
|
-
TriggerTemplateRegistry,
|
|
60
|
-
WorkflowMeta,
|
|
61
|
-
WorkflowRegistry,
|
|
62
|
-
WorkflowStep,
|
|
63
49
|
)
|
|
50
|
+
from .graph import build_graph
|
|
64
51
|
from .runtime import get_user_context, use_user_context
|
|
65
|
-
from .trigger_manager import DynamicTriggerManager
|
|
66
52
|
|
|
67
53
|
__all__ = [
|
|
68
54
|
# v2 decorators
|
|
@@ -79,24 +65,11 @@ __all__ = [
|
|
|
79
65
|
"list_registered_tool_ids",
|
|
80
66
|
# llm
|
|
81
67
|
"llm",
|
|
82
|
-
#
|
|
83
|
-
"workflow",
|
|
84
|
-
"uses_agent",
|
|
85
|
-
"trigger_template",
|
|
86
|
-
"ExecutionMode",
|
|
87
|
-
"TriggerTemplateType",
|
|
88
|
-
"AgentRegistry",
|
|
89
|
-
"WorkflowRegistry",
|
|
90
|
-
"TriggerTemplateRegistry",
|
|
91
|
-
"AgentMeta",
|
|
92
|
-
"WorkflowMeta",
|
|
93
|
-
"WorkflowStep",
|
|
94
|
-
"TriggerMeta",
|
|
95
|
-
"WorkflowExecutor",
|
|
96
|
-
"DynamicTriggerManager",
|
|
68
|
+
# canvas graph (manifest-driven)
|
|
97
69
|
"build_graph",
|
|
70
|
+
# per-request user context
|
|
98
71
|
"get_user_context",
|
|
99
72
|
"use_user_context",
|
|
100
73
|
]
|
|
101
74
|
|
|
102
|
-
__version__ = "2.
|
|
75
|
+
__version__ = "2.2.0"
|
|
@@ -4,6 +4,13 @@ Phase 5.8 — the single CLI that runs in both pre-commit hooks and CI.
|
|
|
4
4
|
Hard-fails on any of:
|
|
5
5
|
|
|
6
6
|
* Invalid ``app.yaml`` (shape or cross-reference)
|
|
7
|
+
* Manifest SEMANTIC errors that pydantic shape-validation can't see —
|
|
8
|
+
the same rules the platform applies at deploy
|
|
9
|
+
(``manifest-doc-validate.ts``): ``${steps.*}``/``${input.*}`` piping,
|
|
10
|
+
dotted-tool↔integration coupling, team-roster existence, and the
|
|
11
|
+
RUNNABLE + FIRED workflow invariant. So an app that passes
|
|
12
|
+
``seed verify`` passes platform validation — no cryptic
|
|
13
|
+
post-upload "validation failed".
|
|
7
14
|
* Invalid ``backend/custom/tools/*/impl.py`` (signature, decorator,
|
|
8
15
|
banned imports, byte ceiling — same YAML rules the platform's
|
|
9
16
|
``CustomToolService`` uses)
|
|
@@ -14,7 +21,7 @@ Exit codes mirror ``cli_eval.py``::
|
|
|
14
21
|
|
|
15
22
|
0 everything passes
|
|
16
23
|
1 invalid manifest shape
|
|
17
|
-
2 manifest reference
|
|
24
|
+
2 manifest reference / semantic error
|
|
18
25
|
3 setup error (no app.yaml, no catalog)
|
|
19
26
|
4 secret-shaped string found
|
|
20
27
|
|
|
@@ -23,6 +30,7 @@ Usage::
|
|
|
23
30
|
claritty seed verify # current dir
|
|
24
31
|
claritty seed verify /path/to/seed # explicit root
|
|
25
32
|
claritty seed verify --catalog /path # override catalog root
|
|
33
|
+
claritty seed verify --lenient # skip the RUNNABLE+FIRED gate (mid-build)
|
|
26
34
|
claritty seed verify --skip-secrets # debug only — never use in CI
|
|
27
35
|
"""
|
|
28
36
|
|
|
@@ -259,6 +267,15 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
259
267
|
action="store_true",
|
|
260
268
|
help="Skip the secret-shape scanner. NEVER use in CI.",
|
|
261
269
|
)
|
|
270
|
+
parser.add_argument(
|
|
271
|
+
"--lenient",
|
|
272
|
+
action="store_true",
|
|
273
|
+
help=(
|
|
274
|
+
"Skip the strict RUNNABLE+FIRED workflow gate (an app must ship a "
|
|
275
|
+
"workflow that actually runs and is fired by a trigger). Use mid-build; "
|
|
276
|
+
"the deploy path runs strict."
|
|
277
|
+
),
|
|
278
|
+
)
|
|
262
279
|
args = parser.parse_args(argv)
|
|
263
280
|
|
|
264
281
|
seed_root = Path(args.seed_root).resolve()
|
|
@@ -305,6 +322,36 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
305
322
|
|
|
306
323
|
return 1 if isinstance(e, _Pyd) else 2
|
|
307
324
|
|
|
325
|
+
# 1b) manifest SEMANTIC rules — the platform's deploy-time gate, run
|
|
326
|
+
# LOCALLY so the developer sees every problem before upload (not a cryptic
|
|
327
|
+
# "validation failed" afterwards). Strict by default (matches the
|
|
328
|
+
# developer-submission gate); --lenient drops only the RUNNABLE+FIRED check.
|
|
329
|
+
import yaml as _yaml
|
|
330
|
+
|
|
331
|
+
from .manifest import validate_manifest_doc
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
raw_doc = _yaml.safe_load(app_yaml.read_text("utf-8")) or {}
|
|
335
|
+
except _yaml.YAMLError as e:
|
|
336
|
+
_fail(f"{app_yaml.name} is not valid YAML: {e}")
|
|
337
|
+
return 1
|
|
338
|
+
sem = validate_manifest_doc(
|
|
339
|
+
raw_doc, require_runnable_workflow=not args.lenient
|
|
340
|
+
)
|
|
341
|
+
if not sem.ok:
|
|
342
|
+
for err in sem.errors:
|
|
343
|
+
_fail(err)
|
|
344
|
+
_info(
|
|
345
|
+
"These are the SAME checks the platform runs at deploy. Fix them here "
|
|
346
|
+
"and the deploy will pass. (Run with --lenient mid-build to drop only "
|
|
347
|
+
"the runnable+fired-workflow gate.)"
|
|
348
|
+
)
|
|
349
|
+
return 2
|
|
350
|
+
_ok(
|
|
351
|
+
f"manifest semantics validated"
|
|
352
|
+
+ ("" if args.lenient else " (runnable + fired workflow)")
|
|
353
|
+
)
|
|
354
|
+
|
|
308
355
|
# 2) custom-tool rules
|
|
309
356
|
try:
|
|
310
357
|
rules = load_rules(catalog_root)
|
|
@@ -82,22 +82,11 @@ class BaseAgent:
|
|
|
82
82
|
``claritty_sdk.runtime.tool_loop.run_agent`` drives the LLM tool-use loop
|
|
83
83
|
for you and validates the output against ``app.yaml#agents[].output``.
|
|
84
84
|
|
|
85
|
-
Do NOT implement ``execute()`` — that was the
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
``system_prompt``/``prompt_file``.
|
|
85
|
+
Do NOT implement ``execute()`` — that was the v1 contract and has been
|
|
86
|
+
removed. Define a ``system_prompt`` (or ``prompt_file``); the v2 runtime's
|
|
87
|
+
``run_agent`` raises if an agent has neither.
|
|
89
88
|
"""
|
|
90
89
|
|
|
91
|
-
# Deprecated v1 entrypoint, kept only so the inherited symbol exists. The v2
|
|
92
|
-
# tool-use loop never calls this; defining your own execute() is rejected at
|
|
93
|
-
# build (materializer) and at boot (bootstrap).
|
|
94
|
-
async def execute(self, context: "AgentContext") -> "AgentResult": # pragma: no cover - deprecated
|
|
95
|
-
raise NotImplementedError(
|
|
96
|
-
"execute() is the deprecated v1 agent contract and is never called "
|
|
97
|
-
"by the v2 runtime. Define a `system_prompt` (or prompt_file) "
|
|
98
|
-
"instead; claritty_sdk.runtime.tool_loop.run_agent drives the loop."
|
|
99
|
-
)
|
|
100
|
-
|
|
101
90
|
|
|
102
91
|
class GenericAgent(BaseAgent):
|
|
103
92
|
"""Pure-YAML agent. The runtime binds this when an `app.yaml` agent declares
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""SDK decorators.
|
|
2
|
+
|
|
3
|
+
In the v2 (manifest-first) SDK, decorators are **binders**: they attach
|
|
4
|
+
a Python symbol to an ``app.yaml`` entry by id. They no longer carry
|
|
5
|
+
schema, integrations, model, timeout, or any other data the manifest
|
|
6
|
+
declares — that data lives in ``app.yaml`` and is loaded by
|
|
7
|
+
``claritty_sdk.runtime.bootstrap``.
|
|
8
|
+
|
|
9
|
+
Two decorators:
|
|
10
|
+
|
|
11
|
+
- ``@agent(id="...")`` on a ``BaseAgent`` subclass.
|
|
12
|
+
- ``@tool(id="...")`` on a plain function ``(input: dict, ctx: ToolCtx) -> dict``.
|
|
13
|
+
|
|
14
|
+
Workflows and triggers are declarative — they live in ``app.yaml#workflows``
|
|
15
|
+
and ``app.yaml#triggers`` and run on ``claritty_sdk.runtime``. The v1
|
|
16
|
+
``@workflow``/``@uses_agent``/``@trigger_template`` decorators (and the
|
|
17
|
+
``AgentRegistry``/``WorkflowRegistry``/``TriggerTemplateRegistry`` they fed)
|
|
18
|
+
have been removed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import warnings
|
|
24
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# @agent and @tool — manifest-first binders
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def agent(
|
|
33
|
+
*,
|
|
34
|
+
id: str,
|
|
35
|
+
# Legacy kwargs are still accepted (and ignored) so an app that hasn't
|
|
36
|
+
# stripped them from `@agent(...)` keeps importing — the data lives in
|
|
37
|
+
# app.yaml#agents. A manifest-first agent should pass only `id=`.
|
|
38
|
+
name: Optional[str] = None,
|
|
39
|
+
description: str = "",
|
|
40
|
+
category: Optional[str] = None,
|
|
41
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
42
|
+
outputs: Optional[Dict[str, Any]] = None,
|
|
43
|
+
timeout: Optional[int] = None,
|
|
44
|
+
integrations: Optional[List[Any]] = None,
|
|
45
|
+
**_extra: Any,
|
|
46
|
+
) -> Callable[[Type], Type]:
|
|
47
|
+
"""Bind a Python class to ``app.yaml#agents[id=...]``.
|
|
48
|
+
|
|
49
|
+
The class should subclass ``BaseAgent`` and expose ``system_prompt: str``
|
|
50
|
+
(or ``prompt_file``) and optionally ``before(ctx)`` / ``after(ctx, output)``
|
|
51
|
+
/ ``fallback(ctx)`` hooks. The Anthropic tool-use loop is run by
|
|
52
|
+
``claritty_sdk.runtime.tool_loop``; the v2 runtime loads the handler class
|
|
53
|
+
from the manifest's ``handler:`` field, so no registry is involved.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def decorate(cls: Type) -> Type:
|
|
57
|
+
# Lightweight marker for introspection/debugging only — the v2 runtime
|
|
58
|
+
# resolves handlers from the manifest, not from this attribute.
|
|
59
|
+
cls.__clarity_agent__ = {"id": id} # type: ignore[attr-defined]
|
|
60
|
+
return cls
|
|
61
|
+
|
|
62
|
+
return decorate
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Tool binding: minimalist registry kept here so the tool-use loop and the
|
|
66
|
+
# workflow engine can dispatch ``tool_use`` blocks to the bound function.
|
|
67
|
+
_TOOLS: Dict[str, Callable[..., Any]] = {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def tool(*, id: str, **_extra: Any) -> Callable[[Callable], Callable]:
|
|
71
|
+
"""Bind a Python function to ``app.yaml#tools[id=...]``.
|
|
72
|
+
|
|
73
|
+
The function must take ``(input: dict, ctx: ToolCtx) -> dict`` (or
|
|
74
|
+
``-> Awaitable[dict]`` — the runtime awaits if needed). The manifest
|
|
75
|
+
declares the input/output schema and the SDK validates both at
|
|
76
|
+
invocation time.
|
|
77
|
+
|
|
78
|
+
Example::
|
|
79
|
+
|
|
80
|
+
from claritty_sdk import tool, ToolCtx
|
|
81
|
+
|
|
82
|
+
@tool(id="db.save_task")
|
|
83
|
+
def save_task(input: dict, ctx: ToolCtx) -> dict:
|
|
84
|
+
task = ctx.db.tasks.insert(user_id=ctx.user_id, **input)
|
|
85
|
+
return {"task_id": task.id}
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def decorate(fn: Callable) -> Callable:
|
|
89
|
+
if id in _TOOLS and _TOOLS[id] is not fn:
|
|
90
|
+
# Re-registration during dev (hot reload) is fine; warn so
|
|
91
|
+
# collisions across modules are visible.
|
|
92
|
+
warnings.warn(
|
|
93
|
+
f"@tool(id={id!r}) is being re-registered — earlier "
|
|
94
|
+
"binder is overwritten.",
|
|
95
|
+
stacklevel=2,
|
|
96
|
+
)
|
|
97
|
+
fn.__clarity_tool_id__ = id # type: ignore[attr-defined]
|
|
98
|
+
_TOOLS[id] = fn
|
|
99
|
+
return fn
|
|
100
|
+
|
|
101
|
+
return decorate
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_registered_tool(tool_id: str) -> Optional[Callable[..., Any]]:
|
|
105
|
+
"""Look up a tool handler by id. Used by the workflow engine and the
|
|
106
|
+
tool-use loop to dispatch ``tool_use`` blocks."""
|
|
107
|
+
return _TOOLS.get(tool_id)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def list_registered_tool_ids() -> List[str]:
|
|
111
|
+
return sorted(_TOOLS.keys())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _clear_tool_registry() -> None:
|
|
115
|
+
"""Test hook — not part of the public surface."""
|
|
116
|
+
_TOOLS.clear()
|
|
@@ -11,8 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
import re
|
|
12
12
|
from typing import Any, Dict, List, Optional, Set
|
|
13
13
|
|
|
14
|
-
from .enums import ExecutionMode
|
|
15
|
-
from .registry import AgentRegistry, TriggerTemplateRegistry, WorkflowRegistry
|
|
16
14
|
|
|
17
15
|
GRAPH_VERSION = 1
|
|
18
16
|
|
|
@@ -105,6 +103,50 @@ def _resource_nodes_and_edges(
|
|
|
105
103
|
"data": {"kind": "tool"},
|
|
106
104
|
}
|
|
107
105
|
)
|
|
106
|
+
|
|
107
|
+
# Tools that broker an integration server-side (e.g. a `publish` tool that
|
|
108
|
+
# posts through `linkedin`). Surface the integration wired to the tool node so
|
|
109
|
+
# tool-only automations show what they connect to — mirrors the agent loop
|
|
110
|
+
# above. The tool node is emitted here if no agent already declared it.
|
|
111
|
+
for t in manifest.tools or []:
|
|
112
|
+
tool_ints = list(getattr(t, "integrations", None) or [])
|
|
113
|
+
if not tool_ints:
|
|
114
|
+
continue
|
|
115
|
+
t_node = _tool_node_id(t.id)
|
|
116
|
+
if t.id not in seen_tool:
|
|
117
|
+
seen_tool.add(t.id)
|
|
118
|
+
nodes.append(
|
|
119
|
+
{
|
|
120
|
+
"id": t_node,
|
|
121
|
+
"type": "tool",
|
|
122
|
+
"name": t.id,
|
|
123
|
+
"data": {
|
|
124
|
+
"toolId": t.id,
|
|
125
|
+
"source": getattr(t, "source", None),
|
|
126
|
+
"handler": getattr(t, "handler", None),
|
|
127
|
+
"integrations": tool_ints,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
for iid in tool_ints:
|
|
132
|
+
if iid not in seen_int:
|
|
133
|
+
seen_int.add(iid)
|
|
134
|
+
nodes.append(
|
|
135
|
+
{
|
|
136
|
+
"id": _integration_node_id(iid),
|
|
137
|
+
"type": "integration",
|
|
138
|
+
"name": iid,
|
|
139
|
+
"data": {"integrationId": iid},
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
edges.append(
|
|
143
|
+
{
|
|
144
|
+
"id": f"{t_node}->{_integration_node_id(iid)}",
|
|
145
|
+
"source": t_node,
|
|
146
|
+
"target": _integration_node_id(iid),
|
|
147
|
+
"data": {"kind": "integration"},
|
|
148
|
+
}
|
|
149
|
+
)
|
|
108
150
|
return nodes, edges
|
|
109
151
|
|
|
110
152
|
|
|
@@ -280,9 +322,12 @@ def build_graph_from_manifest(manifest: Any) -> Dict[str, Any]:
|
|
|
280
322
|
|
|
281
323
|
|
|
282
324
|
def build_graph() -> Dict[str, Any]:
|
|
283
|
-
"""Build the graph contract
|
|
284
|
-
YAML workflows/triggers + each agent's tools/integrations)
|
|
285
|
-
|
|
325
|
+
"""Build the canvas graph contract from the loaded v2 manifest (which
|
|
326
|
+
carries YAML workflows/triggers + each agent's tools/integrations).
|
|
327
|
+
|
|
328
|
+
Manifest-first is the only contract: if no app has been booted (no manifest
|
|
329
|
+
loaded), there is nothing to graph, so return an empty graph rather than
|
|
330
|
+
crashing the discovery endpoint."""
|
|
286
331
|
|
|
287
332
|
# Lazy import to avoid a circular import at module load (bootstrap imports
|
|
288
333
|
# the manifest + engine, not graph).
|
|
@@ -295,106 +340,10 @@ def build_graph() -> Dict[str, Any]:
|
|
|
295
340
|
if boot is not None and getattr(boot, "manifest", None) is not None:
|
|
296
341
|
return build_graph_from_manifest(boot.manifest)
|
|
297
342
|
|
|
298
|
-
nodes: List[Dict[str, Any]] = []
|
|
299
|
-
edges: List[Dict[str, Any]] = []
|
|
300
|
-
|
|
301
|
-
for a in AgentRegistry.list_agents():
|
|
302
|
-
# Normalize integrations (AgentMeta stores them as objects with an id).
|
|
303
|
-
raw_ints = getattr(a, "integrations", None) or []
|
|
304
|
-
ints = [
|
|
305
|
-
(i if isinstance(i, str) else getattr(i, "id", None) or getattr(i, "service", None))
|
|
306
|
-
for i in raw_ints
|
|
307
|
-
]
|
|
308
|
-
nodes.append(
|
|
309
|
-
{
|
|
310
|
-
"id": _agent_node_id(a.id),
|
|
311
|
-
"type": "agent",
|
|
312
|
-
"name": a.name,
|
|
313
|
-
"data": {
|
|
314
|
-
"agentId": a.id,
|
|
315
|
-
"category": a.category,
|
|
316
|
-
"description": a.description,
|
|
317
|
-
"inputs": a.inputs,
|
|
318
|
-
"outputs": a.outputs,
|
|
319
|
-
"tools": list(getattr(a, "tools", None) or []),
|
|
320
|
-
"integrations": [i for i in ints if i],
|
|
321
|
-
},
|
|
322
|
-
}
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
for t in TriggerTemplateRegistry.list_templates():
|
|
326
|
-
nodes.append(
|
|
327
|
-
{
|
|
328
|
-
"id": _trigger_node_id(t.id),
|
|
329
|
-
"type": "trigger",
|
|
330
|
-
"name": t.name,
|
|
331
|
-
"data": {
|
|
332
|
-
"triggerId": t.id,
|
|
333
|
-
"templateType": getattr(t.template_type, "value", t.template_type),
|
|
334
|
-
"workflowId": t.workflow_id,
|
|
335
|
-
"category": t.category,
|
|
336
|
-
},
|
|
337
|
-
}
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
workflows_out: List[Dict[str, Any]] = []
|
|
341
|
-
for wf in WorkflowRegistry.list_workflows():
|
|
342
|
-
steps = wf.steps or []
|
|
343
|
-
workflows_out.append(
|
|
344
|
-
{
|
|
345
|
-
"id": wf.id,
|
|
346
|
-
"name": wf.name,
|
|
347
|
-
"executionMode": getattr(wf.execution_mode, "value", wf.execution_mode),
|
|
348
|
-
"steps": [
|
|
349
|
-
{"agentId": s.agent_id, "outputKey": s.output_key, "inputFrom": s.input_from}
|
|
350
|
-
for s in steps
|
|
351
|
-
],
|
|
352
|
-
}
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
# Agent→agent edges. For SEQUENTIAL workflows, chain consecutive steps
|
|
356
|
-
# (honoring explicit input_from); PARALLEL workflows have no inter-agent
|
|
357
|
-
# edges (only the trigger fans into each agent).
|
|
358
|
-
sequential = wf.execution_mode == ExecutionMode.SEQUENTIAL
|
|
359
|
-
prev_node = None
|
|
360
|
-
for step in steps:
|
|
361
|
-
cur = _agent_node_id(step.agent_id)
|
|
362
|
-
src = None
|
|
363
|
-
if step.input_from:
|
|
364
|
-
match = next((x for x in steps if x.output_key == step.input_from), None)
|
|
365
|
-
if match:
|
|
366
|
-
src = _agent_node_id(match.agent_id)
|
|
367
|
-
elif sequential and prev_node:
|
|
368
|
-
src = prev_node
|
|
369
|
-
if src and src != cur:
|
|
370
|
-
edges.append(
|
|
371
|
-
{
|
|
372
|
-
"id": f"{wf.id}:{src}->{cur}",
|
|
373
|
-
"source": src,
|
|
374
|
-
"target": cur,
|
|
375
|
-
"data": {"workflowId": wf.id},
|
|
376
|
-
}
|
|
377
|
-
)
|
|
378
|
-
prev_node = cur
|
|
379
|
-
|
|
380
|
-
# Trigger→entry-agent edges.
|
|
381
|
-
if steps:
|
|
382
|
-
entry = _agent_node_id(steps[0].agent_id)
|
|
383
|
-
for t in TriggerTemplateRegistry.list_templates():
|
|
384
|
-
if t.workflow_id == wf.id:
|
|
385
|
-
edges.append(
|
|
386
|
-
{
|
|
387
|
-
"id": f"trigger:{t.id}->{entry}",
|
|
388
|
-
"source": _trigger_node_id(t.id),
|
|
389
|
-
"target": entry,
|
|
390
|
-
"data": {"workflowId": wf.id, "trigger": True},
|
|
391
|
-
}
|
|
392
|
-
)
|
|
393
|
-
|
|
394
343
|
return {
|
|
395
344
|
"version": GRAPH_VERSION,
|
|
396
|
-
"nodes":
|
|
397
|
-
"edges":
|
|
398
|
-
"workflows":
|
|
399
|
-
"source": "
|
|
345
|
+
"nodes": [],
|
|
346
|
+
"edges": [],
|
|
347
|
+
"workflows": [],
|
|
348
|
+
"source": "manifest",
|
|
400
349
|
}
|
|
@@ -168,6 +168,7 @@ class LlmClient:
|
|
|
168
168
|
max_tokens: Optional[int] = None,
|
|
169
169
|
system: Optional[str] = None,
|
|
170
170
|
extra_headers: Optional[dict] = None,
|
|
171
|
+
thinking: Optional[dict] = None,
|
|
171
172
|
) -> "ChatToolsResult":
|
|
172
173
|
"""OpenAI-Chat-Completions tool calling.
|
|
173
174
|
|
|
@@ -181,7 +182,9 @@ class LlmClient:
|
|
|
181
182
|
the proxy uses to enforce a per-run $ ceiling and surface a 402
|
|
182
183
|
``WORKFLOW_BUDGET_EXCEEDED`` envelope when exceeded.
|
|
183
184
|
"""
|
|
184
|
-
body = self._build_body(
|
|
185
|
+
body = self._build_body(
|
|
186
|
+
messages, temperature, max_tokens, system, stream=False, thinking=thinking
|
|
187
|
+
)
|
|
185
188
|
body["tools"] = tools
|
|
186
189
|
if tool_choice is not None:
|
|
187
190
|
body["tool_choice"] = tool_choice
|
|
@@ -272,6 +275,7 @@ class LlmClient:
|
|
|
272
275
|
system: Optional[str],
|
|
273
276
|
*,
|
|
274
277
|
stream: bool,
|
|
278
|
+
thinking: Optional[dict] = None,
|
|
275
279
|
) -> dict:
|
|
276
280
|
# Merge the active agent's user context (the platform's per-agent
|
|
277
281
|
# "how to do your job" instructions) into the system prompt so it
|
|
@@ -304,7 +308,12 @@ class LlmClient:
|
|
|
304
308
|
out["name"] = m["name"]
|
|
305
309
|
msgs.append(out)
|
|
306
310
|
body: dict = {"model": self.model, "messages": msgs}
|
|
307
|
-
if
|
|
311
|
+
if thinking is not None:
|
|
312
|
+
# Anthropic extended thinking. The metered proxy forwards this to the
|
|
313
|
+
# Anthropic `thinking` param. Anthropic requires temperature unset
|
|
314
|
+
# (=1) while thinking is on, so we DON'T send temperature here.
|
|
315
|
+
body["thinking"] = thinking
|
|
316
|
+
elif temperature is not None:
|
|
308
317
|
body["temperature"] = temperature
|
|
309
318
|
if max_tokens is not None:
|
|
310
319
|
body["max_tokens"] = max_tokens
|
|
@@ -365,7 +374,29 @@ def _raise_for_status(res: httpx.Response) -> None:
|
|
|
365
374
|
if res.is_success:
|
|
366
375
|
return
|
|
367
376
|
try:
|
|
368
|
-
payload = res.json()
|
|
377
|
+
payload: Any = res.json()
|
|
369
378
|
except Exception:
|
|
370
|
-
|
|
379
|
+
text = res.text or ""
|
|
380
|
+
# A non-JSON body means the response did NOT come from the clarity-api
|
|
381
|
+
# proxy (which always returns a JSON error envelope). The overwhelmingly
|
|
382
|
+
# common cause is a proxy URL pointing at the wrong host — e.g. the
|
|
383
|
+
# frontend (app.claritty.ai / :3000) instead of the API
|
|
384
|
+
# (api.claritty.ai / :4000) — so `/api/v1/chat/completions` 404/500s as
|
|
385
|
+
# an HTML page. Turn that opaque HTML dump into an actionable error that
|
|
386
|
+
# names the exact URL and the likely fix.
|
|
387
|
+
looks_like_html = "<html" in text[:512].lower() or "<!doctype" in text[:512].lower()
|
|
388
|
+
if looks_like_html:
|
|
389
|
+
payload = {
|
|
390
|
+
"message": (
|
|
391
|
+
f"the LLM proxy at {res.request.url} returned an HTML "
|
|
392
|
+
f"{res.status_code} page, not a JSON error — this host is "
|
|
393
|
+
"almost certainly NOT the clarity-api proxy. Check "
|
|
394
|
+
"CLARITTY_LLM_PROXY_URL / CLARITTY_PLATFORM_URL: it must be "
|
|
395
|
+
"the API host (e.g. https://api.claritty.ai), not the "
|
|
396
|
+
"frontend (app.claritty.ai) or a local web port (:3000)."
|
|
397
|
+
),
|
|
398
|
+
"non_json_body": text[:500],
|
|
399
|
+
}
|
|
400
|
+
else:
|
|
401
|
+
payload = text
|
|
371
402
|
raise LlmProxyError(res.status_code, payload)
|