velophex 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- velophex/__init__.py +38 -0
- velophex/_json.py +297 -0
- velophex/agents/__init__.py +130 -0
- velophex/agents/_errors.py +75 -0
- velophex/agents/_loop.py +436 -0
- velophex/agents/_tools.py +168 -0
- velophex/agents/_tracing.py +87 -0
- velophex/agents/testing.py +249 -0
- velophex/cli/__init__.py +5 -0
- velophex/cli/main.py +548 -0
- velophex/cli/publish.py +152 -0
- velophex/cli/sync.py +90 -0
- velophex/cli/templates.py +126 -0
- velophex/contracts/__init__.py +103 -0
- velophex/contracts/_model.py +26 -0
- velophex/contracts/agents.py +110 -0
- velophex/contracts/diagnostics.py +157 -0
- velophex/contracts/executor.py +1921 -0
- velophex/contracts/package.py +391 -0
- velophex/contracts/project.py +1008 -0
- velophex/contracts/runtimes.py +226 -0
- velophex/contracts/signin.py +53 -0
- velophex/contracts/validation.py +158 -0
- velophex/entrypoints.py +329 -0
- velophex/errors.py +44 -0
- velophex/packaging.py +465 -0
- velophex/py.typed +0 -0
- velophex/runtime/__init__.py +471 -0
- velophex/runtime/_session.py +192 -0
- velophex/runtime/client.py +957 -0
- velophex/runtime/models.py +326 -0
- velophex/signing.py +425 -0
- velophex/worker/__init__.py +27 -0
- velophex/worker/__main__.py +118 -0
- velophex/worker/framing.py +248 -0
- velophex/worker/loader.py +105 -0
- velophex/worker/outbox.py +147 -0
- velophex/worker/session.py +519 -0
- velophex-0.2.0.dist-info/METADATA +313 -0
- velophex-0.2.0.dist-info/RECORD +43 -0
- velophex-0.2.0.dist-info/WHEEL +4 -0
- velophex-0.2.0.dist-info/entry_points.txt +2 -0
- velophex-0.2.0.dist-info/licenses/LICENSE +21 -0
velophex/cli/sync.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Keeps ``project.json`` in step with the decorated handlers.
|
|
2
|
+
|
|
3
|
+
The entry point schemas in ``project.json`` are what the Orchestrator validates job inputs
|
|
4
|
+
against and what the start form is generated from, so they must not be hand-maintained: a
|
|
5
|
+
schema that has drifted from the handler fails on the robot, not in the editor. ``velophex
|
|
6
|
+
pack`` therefore imports each handler and rewrites its entry point before it packs.
|
|
7
|
+
|
|
8
|
+
Importing runs the author's own module, which is the same thing ``velophex run`` does; the
|
|
9
|
+
synchronisation is skipped with ``--no-sync`` for a project whose handlers cannot be imported
|
|
10
|
+
on the machine that packs (a Windows-only automation packed on a build agent, say).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from velophex.contracts.project import EntryPoint, ProjectFiles, ProjectManifest, RuntimeSpec
|
|
19
|
+
from velophex.contracts.runtimes import RuntimeKind
|
|
20
|
+
from velophex.worker.loader import PackageLoadError, load_entry_point
|
|
21
|
+
|
|
22
|
+
__all__ = ["DEFAULT_PYTHON_VERSION", "SyncResult", "synchronize"]
|
|
23
|
+
|
|
24
|
+
#: ADR-P008: the platform ships CPython 3.12 and nothing else.
|
|
25
|
+
DEFAULT_PYTHON_VERSION = ">=3.12,<3.13"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True)
|
|
29
|
+
class SyncResult:
|
|
30
|
+
"""The manifest after synchronisation, and what could not be refreshed."""
|
|
31
|
+
|
|
32
|
+
manifest: ProjectManifest
|
|
33
|
+
warnings: list[str]
|
|
34
|
+
changed: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def synchronize(
|
|
38
|
+
project_directory: str | Path,
|
|
39
|
+
manifest: ProjectManifest,
|
|
40
|
+
*,
|
|
41
|
+
import_handlers: bool = True,
|
|
42
|
+
) -> SyncResult:
|
|
43
|
+
"""Return *manifest* with the Python runtime settings and entry point schemas refreshed."""
|
|
44
|
+
warnings: list[str] = []
|
|
45
|
+
runtime = _runtime(manifest.runtime)
|
|
46
|
+
entry_points = list(manifest.entry_points)
|
|
47
|
+
|
|
48
|
+
if import_handlers:
|
|
49
|
+
for index, entry in enumerate(entry_points):
|
|
50
|
+
refreshed, warning = _refresh(project_directory, entry)
|
|
51
|
+
entry_points[index] = refreshed
|
|
52
|
+
|
|
53
|
+
if warning:
|
|
54
|
+
warnings.append(warning)
|
|
55
|
+
|
|
56
|
+
updated = manifest.model_copy(update={"runtime": runtime, "entry_points": entry_points}).normalized()
|
|
57
|
+
|
|
58
|
+
return SyncResult(manifest=updated, warnings=warnings, changed=updated != manifest.normalized())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _runtime(runtime: RuntimeSpec) -> RuntimeSpec:
|
|
62
|
+
return runtime.model_copy(
|
|
63
|
+
update={
|
|
64
|
+
"kind": RuntimeKind.PYTHON,
|
|
65
|
+
"python_version": runtime.python_version or DEFAULT_PYTHON_VERSION,
|
|
66
|
+
"lock_file": runtime.lock_file or ProjectFiles.PYTHON_LOCK_FILE_NAME,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _refresh(project_directory: str | Path, entry: EntryPoint) -> tuple[EntryPoint, str | None]:
|
|
72
|
+
try:
|
|
73
|
+
loaded = load_entry_point(project_directory, entry.id)
|
|
74
|
+
except PackageLoadError as error:
|
|
75
|
+
return entry, f"Entry point '{entry.id}' was left as it is: {error}"
|
|
76
|
+
|
|
77
|
+
definition = loaded.definition
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
entry.model_copy(
|
|
81
|
+
update={
|
|
82
|
+
"handler": definition.handler,
|
|
83
|
+
"display_name": entry.display_name or definition.display_name,
|
|
84
|
+
"description": entry.description or definition.description,
|
|
85
|
+
"input": definition.input_schema,
|
|
86
|
+
"output": definition.output_schema,
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
None,
|
|
90
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""The project ``velophex init`` writes.
|
|
2
|
+
|
|
3
|
+
A new automation is a uv project with a ``src/`` layout, one decorated handler and a
|
|
4
|
+
``project.json`` that already says what the platform needs to place and run it. It is
|
|
5
|
+
deliberately small: the schemas come from the handler's signature, so there is nothing in
|
|
6
|
+
``project.json`` for the author to keep in step by hand.
|
|
7
|
+
|
|
8
|
+
``uv.lock`` is *not* written here. It is produced by ``uv sync``, which is also what pins the
|
|
9
|
+
``velophex`` SDK into the lock file - and a Python package without ``uv.lock`` is refused both
|
|
10
|
+
by ``velophex pack`` and by the Orchestrator inspector (ADR-P008).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from uuid import UUID, uuid4
|
|
16
|
+
|
|
17
|
+
from velophex.cli.sync import DEFAULT_PYTHON_VERSION
|
|
18
|
+
from velophex.contracts.project import (
|
|
19
|
+
CURRENT_SCHEMA_VERSION,
|
|
20
|
+
EntryPoint,
|
|
21
|
+
ExecutionSettings,
|
|
22
|
+
ProjectManifest,
|
|
23
|
+
RuntimeSpec,
|
|
24
|
+
)
|
|
25
|
+
from velophex.contracts.runtimes import RuntimeKind
|
|
26
|
+
|
|
27
|
+
__all__ = ["handler_module", "module_name", "new_project_manifest", "pyproject_toml", "python_version_marker"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def module_name(project_name: str) -> str:
|
|
31
|
+
"""The importable module name for a project name such as ``Invoice.Ocr``."""
|
|
32
|
+
cleaned = "".join(character if character.isalnum() else "_" for character in project_name).strip("_").lower()
|
|
33
|
+
return cleaned or "automation"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def python_version_marker() -> str:
|
|
37
|
+
"""The ``requires-python`` marker the platform supports."""
|
|
38
|
+
return DEFAULT_PYTHON_VERSION
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def new_project_manifest(name: str, version: str = "1.0.0", project_id: UUID | None = None) -> ProjectManifest:
|
|
42
|
+
"""The ``project.json`` of a new Python automation."""
|
|
43
|
+
module = module_name(name)
|
|
44
|
+
|
|
45
|
+
return ProjectManifest(
|
|
46
|
+
schemaVersion=CURRENT_SCHEMA_VERSION,
|
|
47
|
+
projectId=project_id or uuid4(),
|
|
48
|
+
name=name,
|
|
49
|
+
version=version,
|
|
50
|
+
description=f"{name} automation.",
|
|
51
|
+
outputType="process",
|
|
52
|
+
runtime=RuntimeSpec(kind=RuntimeKind.PYTHON, pythonVersion=DEFAULT_PYTHON_VERSION, lockFile="uv.lock"),
|
|
53
|
+
main=f"src/{module}/main.py",
|
|
54
|
+
entryPoints=[
|
|
55
|
+
EntryPoint(id="main", path=f"src/{module}/main.py", handler=f"{module}.main:run"),
|
|
56
|
+
],
|
|
57
|
+
execution=ExecutionSettings(),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def pyproject_toml(name: str, version: str, sdk_specifier: str, sdk_wheel: str | None = None) -> str:
|
|
62
|
+
"""The ``pyproject.toml`` of a new automation.
|
|
63
|
+
|
|
64
|
+
With *sdk_wheel* (a path relative to the project) the SDK comes from that vendored wheel
|
|
65
|
+
instead of an index: ``uv.lock`` then records the relative path and the wheel's SHA-256,
|
|
66
|
+
and ``velophex pack`` carries the wheel inside the package, so a Robot's
|
|
67
|
+
``uv sync --frozen`` restores it from the package itself and never asks an index for a
|
|
68
|
+
package named ``velophex`` (PY-01, PY-02).
|
|
69
|
+
"""
|
|
70
|
+
module = module_name(name)
|
|
71
|
+
sources = (
|
|
72
|
+
f"""
|
|
73
|
+
[tool.uv.sources]
|
|
74
|
+
# Vendored: packed with the automation, so a Robot restores the SDK without any index.
|
|
75
|
+
velophex = {{ path = "{sdk_wheel}" }}
|
|
76
|
+
"""
|
|
77
|
+
if sdk_wheel
|
|
78
|
+
else ""
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return f"""[project]
|
|
82
|
+
name = "{module}"
|
|
83
|
+
version = "{version}"
|
|
84
|
+
description = "{name} automation."
|
|
85
|
+
requires-python = "{DEFAULT_PYTHON_VERSION}"
|
|
86
|
+
dependencies = [
|
|
87
|
+
"velophex{sdk_specifier}",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[build-system]
|
|
91
|
+
requires = ["hatchling"]
|
|
92
|
+
build-backend = "hatchling.build"
|
|
93
|
+
|
|
94
|
+
[tool.hatch.build.targets.wheel]
|
|
95
|
+
packages = ["src/{module}"]
|
|
96
|
+
{sources}"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def handler_module(name: str) -> str:
|
|
100
|
+
"""The starting handler of a new automation."""
|
|
101
|
+
return f'''"""Entry points of the {name} automation."""
|
|
102
|
+
|
|
103
|
+
import logging
|
|
104
|
+
|
|
105
|
+
import velophex
|
|
106
|
+
from velophex import runtime
|
|
107
|
+
|
|
108
|
+
log = logging.getLogger(__name__)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@velophex.entrypoint(id="main", display_name="Run")
|
|
112
|
+
def run(message: str = "Hello from VeloPhex") -> dict[str, str]:
|
|
113
|
+
"""Say hello.
|
|
114
|
+
|
|
115
|
+
Every parameter becomes an input in project.json, typed from its annotation, and the
|
|
116
|
+
return annotation becomes the output schema - there is nothing to keep in step by hand.
|
|
117
|
+
"""
|
|
118
|
+
log.info("%s", message)
|
|
119
|
+
|
|
120
|
+
# Long-running work polls this so a Stop from the Orchestrator ends the run cleanly
|
|
121
|
+
# instead of waiting for the kill.
|
|
122
|
+
if runtime.stop_requested():
|
|
123
|
+
raise velophex.BusinessError("The run was stopped before it did anything.")
|
|
124
|
+
|
|
125
|
+
return {{"message": message}}
|
|
126
|
+
'''
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Python models of the VeloPhex platform contracts.
|
|
2
|
+
|
|
3
|
+
One definition per contract, mirroring ``VeloPhex.Platform.Contracts`` and the JSON Schemas
|
|
4
|
+
in ``VeloPhex.Platform.Contracts/schemas``:
|
|
5
|
+
|
|
6
|
+
* :mod:`velophex.contracts.project` - ``project.json`` (ADR-P002)
|
|
7
|
+
* :mod:`velophex.contracts.package` - ``velophex/package-manifest.json`` v2 (ADR-P003)
|
|
8
|
+
* :mod:`velophex.contracts.executor` - the executor protocol messages (ADR-P005)
|
|
9
|
+
* :mod:`velophex.contracts.runtimes` - runtime kinds, phases, levels and fault kinds
|
|
10
|
+
* :mod:`velophex.contracts.signin` - the states a sign-in passes through (D1)
|
|
11
|
+
* :mod:`velophex.contracts.validation` - the lexical rules the schemas express
|
|
12
|
+
|
|
13
|
+
Contracts change only through the ``velophex-contract-change`` skill, which updates every
|
|
14
|
+
consumer together; nothing here may drift on its own.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from velophex.contracts.diagnostics import (
|
|
18
|
+
ContractError,
|
|
19
|
+
Diagnostic,
|
|
20
|
+
PackageDiagnosticCodes,
|
|
21
|
+
ProjectDiagnosticCodes,
|
|
22
|
+
Severity,
|
|
23
|
+
has_errors,
|
|
24
|
+
)
|
|
25
|
+
from velophex.contracts.package import (
|
|
26
|
+
PackageFile,
|
|
27
|
+
PackageLayout,
|
|
28
|
+
PackageManifest,
|
|
29
|
+
create_package_manifest,
|
|
30
|
+
describe_file,
|
|
31
|
+
read_package_manifest,
|
|
32
|
+
sha512_hex,
|
|
33
|
+
validate_package_manifest,
|
|
34
|
+
write_package_manifest,
|
|
35
|
+
)
|
|
36
|
+
from velophex.contracts.project import (
|
|
37
|
+
AutomationDependency,
|
|
38
|
+
DesignerSettings,
|
|
39
|
+
EntryPoint,
|
|
40
|
+
ExecutionSettings,
|
|
41
|
+
ExpressionLanguages,
|
|
42
|
+
OutputTypes,
|
|
43
|
+
ProjectFiles,
|
|
44
|
+
ProjectManifest,
|
|
45
|
+
RuntimeSpec,
|
|
46
|
+
read_project_manifest,
|
|
47
|
+
validate_project_manifest,
|
|
48
|
+
write_project_manifest,
|
|
49
|
+
)
|
|
50
|
+
from velophex.contracts.runtimes import (
|
|
51
|
+
AttemptOutcomes,
|
|
52
|
+
ExecutionPhases,
|
|
53
|
+
FaultCodes,
|
|
54
|
+
FaultKinds,
|
|
55
|
+
LogLevels,
|
|
56
|
+
LogStages,
|
|
57
|
+
PhaseStatuses,
|
|
58
|
+
RuntimeKind,
|
|
59
|
+
RuntimeStatuses,
|
|
60
|
+
SignalReasons,
|
|
61
|
+
)
|
|
62
|
+
from velophex.contracts.signin import SignInStates
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"AttemptOutcomes",
|
|
66
|
+
"AutomationDependency",
|
|
67
|
+
"ContractError",
|
|
68
|
+
"DesignerSettings",
|
|
69
|
+
"Diagnostic",
|
|
70
|
+
"EntryPoint",
|
|
71
|
+
"ExecutionPhases",
|
|
72
|
+
"ExecutionSettings",
|
|
73
|
+
"ExpressionLanguages",
|
|
74
|
+
"FaultCodes",
|
|
75
|
+
"FaultKinds",
|
|
76
|
+
"LogLevels",
|
|
77
|
+
"LogStages",
|
|
78
|
+
"OutputTypes",
|
|
79
|
+
"PackageDiagnosticCodes",
|
|
80
|
+
"PackageFile",
|
|
81
|
+
"PackageLayout",
|
|
82
|
+
"PackageManifest",
|
|
83
|
+
"PhaseStatuses",
|
|
84
|
+
"ProjectDiagnosticCodes",
|
|
85
|
+
"ProjectFiles",
|
|
86
|
+
"ProjectManifest",
|
|
87
|
+
"RuntimeKind",
|
|
88
|
+
"RuntimeSpec",
|
|
89
|
+
"RuntimeStatuses",
|
|
90
|
+
"Severity",
|
|
91
|
+
"SignInStates",
|
|
92
|
+
"SignalReasons",
|
|
93
|
+
"create_package_manifest",
|
|
94
|
+
"describe_file",
|
|
95
|
+
"has_errors",
|
|
96
|
+
"read_package_manifest",
|
|
97
|
+
"read_project_manifest",
|
|
98
|
+
"sha512_hex",
|
|
99
|
+
"validate_package_manifest",
|
|
100
|
+
"validate_project_manifest",
|
|
101
|
+
"write_package_manifest",
|
|
102
|
+
"write_project_manifest",
|
|
103
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared base for contract models.
|
|
2
|
+
|
|
3
|
+
Contract documents travel between five codebases, so an unknown member is data from a newer
|
|
4
|
+
writer rather than a mistake: models keep it (``extra="allow"``) and emit it again after the
|
|
5
|
+
members they know, which is where ``[JsonExtensionData]`` puts it in .NET.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict
|
|
13
|
+
|
|
14
|
+
from velophex._json import omit_none
|
|
15
|
+
|
|
16
|
+
__all__ = ["ContractModel", "omit_none"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContractModel(BaseModel):
|
|
20
|
+
"""Base for contract models: camelCase on the wire, unknown members preserved."""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow", arbitrary_types_allowed=True)
|
|
23
|
+
|
|
24
|
+
def extras(self) -> dict[str, Any]:
|
|
25
|
+
"""Members this version of the SDK does not know, in the order they were read."""
|
|
26
|
+
return dict(self.__pydantic_extra__ or {})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""The agent definition, version 1 (``schemas/agent-definition.v1.schema.json``, contracts 0.8.37).
|
|
2
|
+
|
|
3
|
+
Mirrors ``VeloPhex.Platform.Contracts.Agents``. Version 1 changes additively only, so members a
|
|
4
|
+
newer writer adds are kept (``extra="allow"``) rather than refused, and an absent member means the
|
|
5
|
+
default below.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Final
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from velophex.contracts._model import ContractModel
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DEFAULT_BINDING",
|
|
18
|
+
"DEFAULT_MAX_STEPS",
|
|
19
|
+
"DEFAULT_MAX_TOKENS",
|
|
20
|
+
"DEFAULT_TEMPERATURE",
|
|
21
|
+
"DEFAULT_TIMEOUT_SECONDS",
|
|
22
|
+
"SPEC_VERSION",
|
|
23
|
+
"AgentDefinition",
|
|
24
|
+
"AgentGuardrails",
|
|
25
|
+
"AgentModel",
|
|
26
|
+
"AgentModelParameters",
|
|
27
|
+
"AgentTool",
|
|
28
|
+
"AgentToolApprovals",
|
|
29
|
+
"AgentToolKinds",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
SPEC_VERSION: Final = 1
|
|
33
|
+
DEFAULT_MAX_STEPS: Final = 12
|
|
34
|
+
DEFAULT_MAX_TOKENS: Final = 50_000
|
|
35
|
+
DEFAULT_TIMEOUT_SECONDS: Final = 900
|
|
36
|
+
DEFAULT_TEMPERATURE: Final = 0.2
|
|
37
|
+
DEFAULT_BINDING: Final = "agent:default"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AgentToolKinds:
|
|
41
|
+
"""The ``kind`` of an agent tool."""
|
|
42
|
+
|
|
43
|
+
PROCESS: Final = "process"
|
|
44
|
+
MCP: Final = "mcp"
|
|
45
|
+
HTTP: Final = "http"
|
|
46
|
+
VECTOR_SEARCH: Final = "vector_search"
|
|
47
|
+
ACTION_TASK: Final = "action_task"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AgentToolApprovals:
|
|
51
|
+
"""The ``approval`` of an agent tool."""
|
|
52
|
+
|
|
53
|
+
NONE: Final = "none"
|
|
54
|
+
BEFORE_CALL: Final = "before_call"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AgentModelParameters(ContractModel):
|
|
58
|
+
"""Sampling parameters of every model call."""
|
|
59
|
+
|
|
60
|
+
temperature: float | None = None
|
|
61
|
+
max_tokens: int | None = Field(default=None, alias="maxTokens")
|
|
62
|
+
top_p: float | None = Field(default=None, alias="topP")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AgentModel(ContractModel):
|
|
66
|
+
"""The model binding (``<purpose>:<name>``) and its parameters."""
|
|
67
|
+
|
|
68
|
+
binding: str = DEFAULT_BINDING
|
|
69
|
+
parameters: AgentModelParameters | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AgentTool(ContractModel):
|
|
73
|
+
"""One tool the model may call."""
|
|
74
|
+
|
|
75
|
+
kind: str
|
|
76
|
+
ref: str
|
|
77
|
+
name: str
|
|
78
|
+
description: str
|
|
79
|
+
entry_point: str | None = Field(default=None, alias="entryPoint")
|
|
80
|
+
input_schema: dict[str, Any] | None = Field(default=None, alias="inputSchema")
|
|
81
|
+
approval: str = AgentToolApprovals.NONE
|
|
82
|
+
top_k: int | None = Field(default=None, alias="topK")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AgentGuardrails(ContractModel):
|
|
86
|
+
"""Limits of one run."""
|
|
87
|
+
|
|
88
|
+
max_steps: int = Field(default=DEFAULT_MAX_STEPS, alias="maxSteps")
|
|
89
|
+
max_tokens: int = Field(default=DEFAULT_MAX_TOKENS, alias="maxTokens")
|
|
90
|
+
max_cost: float | None = Field(default=None, alias="maxCost")
|
|
91
|
+
timeout_seconds: int = Field(default=DEFAULT_TIMEOUT_SECONDS, alias="timeoutSeconds")
|
|
92
|
+
blocked_tools: list[str] = Field(default_factory=list, alias="blockedTools")
|
|
93
|
+
output_validation: str = Field(default="schema", alias="outputValidation")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AgentDefinition(ContractModel):
|
|
97
|
+
"""What an agent is: instructions, model, schemas, tools and limits."""
|
|
98
|
+
|
|
99
|
+
spec_version: int = Field(default=SPEC_VERSION, alias="specVersion")
|
|
100
|
+
key: str
|
|
101
|
+
name: str
|
|
102
|
+
description: str | None = None
|
|
103
|
+
instructions: str
|
|
104
|
+
model: AgentModel = Field(default_factory=AgentModel)
|
|
105
|
+
input: dict[str, Any] | None = None
|
|
106
|
+
output: dict[str, Any] | None = None
|
|
107
|
+
tools: list[AgentTool] = Field(default_factory=list)
|
|
108
|
+
guardrails: AgentGuardrails = Field(default_factory=AgentGuardrails)
|
|
109
|
+
memory: dict[str, Any] | None = None
|
|
110
|
+
loop: str | None = None
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Contract diagnostics, with the same codes and JSON pointers the .NET validators emit.
|
|
2
|
+
|
|
3
|
+
A developer who sees ``VPC1012`` from ``velophex pack`` and ``VPC1012`` from the Orchestrator
|
|
4
|
+
upload is looking at the same rule, so the codes are copied from
|
|
5
|
+
``VeloPhex.Platform.Contracts`` rather than invented here. Codes are stable; a retired code
|
|
6
|
+
is never reused.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Final
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ContractError",
|
|
17
|
+
"Diagnostic",
|
|
18
|
+
"PackageDiagnosticCodes",
|
|
19
|
+
"ProjectDiagnosticCodes",
|
|
20
|
+
"Severity",
|
|
21
|
+
"has_errors",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Not StrEnum: that changes str(Severity.ERROR) from "Severity.ERROR" to "Error", which is a
|
|
26
|
+
# behaviour change for anything that formats the member rather than its value.
|
|
27
|
+
class Severity(str, Enum): # noqa: UP042
|
|
28
|
+
"""How badly a diagnostic breaks the contract."""
|
|
29
|
+
|
|
30
|
+
ERROR = "Error"
|
|
31
|
+
WARNING = "Warning"
|
|
32
|
+
INFORMATION = "Information"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class Diagnostic:
|
|
37
|
+
"""One rule violation, located by a JSON pointer-ish path such as ``$.entryPoints[0].id``."""
|
|
38
|
+
|
|
39
|
+
code: str
|
|
40
|
+
path: str
|
|
41
|
+
message: str
|
|
42
|
+
severity: Severity = Severity.ERROR
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def error(cls, code: str, path: str, message: str) -> Diagnostic:
|
|
46
|
+
"""Create an error diagnostic."""
|
|
47
|
+
return cls(code, path, message, Severity.ERROR)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def warning(cls, code: str, path: str, message: str) -> Diagnostic:
|
|
51
|
+
"""Create a warning diagnostic."""
|
|
52
|
+
return cls(code, path, message, Severity.WARNING)
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return f"{self.severity.value} {self.code} at {self.path}: {self.message}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def has_errors(diagnostics: list[Diagnostic]) -> bool:
|
|
59
|
+
"""True when at least one diagnostic is an error."""
|
|
60
|
+
return any(diagnostic.severity is Severity.ERROR for diagnostic in diagnostics)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ContractError(Exception):
|
|
64
|
+
"""Raised when a contract document cannot be read or does not validate."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, diagnostics: list[Diagnostic]) -> None:
|
|
67
|
+
super().__init__("; ".join(str(diagnostic) for diagnostic in diagnostics) or "The document is invalid.")
|
|
68
|
+
self.diagnostics = diagnostics
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ProjectDiagnosticCodes:
|
|
72
|
+
"""Diagnostic codes for ``project.json`` (mirrors ``ProjectDiagnosticCodes`` in .NET)."""
|
|
73
|
+
|
|
74
|
+
INVALID_JSON: Final = "VPC1000"
|
|
75
|
+
UNSUPPORTED_SCHEMA_VERSION: Final = "VPC1001"
|
|
76
|
+
MISSING_PROJECT_ID: Final = "VPC1002"
|
|
77
|
+
INVALID_NAME: Final = "VPC1003"
|
|
78
|
+
INVALID_VERSION: Final = "VPC1004"
|
|
79
|
+
UNKNOWN_OUTPUT_TYPE: Final = "VPC1005"
|
|
80
|
+
UNKNOWN_RUNTIME: Final = "VPC1006"
|
|
81
|
+
UNSUPPORTED_EXPRESSION_LANGUAGE: Final = "VPC1007"
|
|
82
|
+
MISSING_PYTHON_VERSION: Final = "VPC1008"
|
|
83
|
+
MISSING_LOCK_FILE: Final = "VPC1009"
|
|
84
|
+
INVALID_WORKFLOW_ENTRY_PATH: Final = "VPC1010"
|
|
85
|
+
UNEXPECTED_HANDLER: Final = "VPC1011"
|
|
86
|
+
INVALID_PYTHON_HANDLER: Final = "VPC1012"
|
|
87
|
+
MISSING_ENTRY_POINTS: Final = "VPC1013"
|
|
88
|
+
INVALID_ENTRY_POINT_ID: Final = "VPC1014"
|
|
89
|
+
UNSAFE_PATH: Final = "VPC1015"
|
|
90
|
+
INVALID_ARGUMENT_SCHEMA: Final = "VPC1016"
|
|
91
|
+
MAIN_NOT_AN_ENTRY_POINT: Final = "VPC1017"
|
|
92
|
+
INVALID_DEPENDENCY: Final = "VPC1018"
|
|
93
|
+
INVALID_AUTOMATION_DEPENDENCY: Final = "VPC1019"
|
|
94
|
+
SELF_DEPENDENCY: Final = "VPC1020"
|
|
95
|
+
INVALID_TIMEOUT: Final = "VPC1021"
|
|
96
|
+
INVALID_CAPABILITY: Final = "VPC1022"
|
|
97
|
+
DOCUMENT_TOO_LARGE: Final = "VPC1023"
|
|
98
|
+
DUPLICATE_ENTRY_POINT: Final = "VPC1024"
|
|
99
|
+
NO_RUN_MODE_ALLOWED: Final = "VPC1025"
|
|
100
|
+
|
|
101
|
+
#: A dependency's value is present but is not a NuGet version range.
|
|
102
|
+
INVALID_DEPENDENCY_VERSION_RANGE: Final = "VPC1026"
|
|
103
|
+
|
|
104
|
+
#: An automation dependency's version is present but is not a NuGet version range.
|
|
105
|
+
INVALID_AUTOMATION_DEPENDENCY_VERSION_RANGE: Final = "VPC1027"
|
|
106
|
+
|
|
107
|
+
#: The version is SemVer 2.0 but carries build metadata (``1.0.0+build.7``), which NuGet drops
|
|
108
|
+
#: from a package's identity; the manifest and the feed would name different versions.
|
|
109
|
+
VERSION_HAS_BUILD_METADATA: Final = "VPC1028"
|
|
110
|
+
|
|
111
|
+
#: ``expressions`` names an unknown policy, or an extra type that is not a type name or is on
|
|
112
|
+
#: the expression floor (engine ADR 0036).
|
|
113
|
+
INVALID_EXPRESSION_POLICY: Final = "VPC1029"
|
|
114
|
+
|
|
115
|
+
#: A global or constant has a name that is not an identifier, repeats another global's or
|
|
116
|
+
#: constant's name (ignoring case, as Visual Basic does), has no type, or a constant has no
|
|
117
|
+
#: value (contracts 0.8.11).
|
|
118
|
+
INVALID_PROJECT_SYMBOL: Final = "VPC1030"
|
|
119
|
+
|
|
120
|
+
#: ``exceptionHandlerWorkflow`` is not a safe relative ``.xaml`` path, or the project is not a
|
|
121
|
+
#: VeloPhex workflow project (engine ADR 0037 section 5, contracts 0.8.16).
|
|
122
|
+
INVALID_EXCEPTION_HANDLER_WORKFLOW: Final = "VPC1031"
|
|
123
|
+
|
|
124
|
+
#: An entry point's ``testCase`` is in a ``library`` project, or its ``dataSource`` is not a
|
|
125
|
+
#: safe relative ``.csv``, ``.json`` or ``.xlsx[#Sheet]`` path (0.8.30), or names one in a
|
|
126
|
+
#: project that is not a VeloPhex workflow project (contracts 0.8.25).
|
|
127
|
+
INVALID_TEST_CASE: Final = "VPC1032"
|
|
128
|
+
|
|
129
|
+
#: ``icon`` is not a safe relative ``.png`` or ``.svg`` path (contracts 0.8.36). The file's
|
|
130
|
+
#: presence and size are checked when the package is built.
|
|
131
|
+
INVALID_ICON: Final = "VPC1033"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class PackageDiagnosticCodes:
|
|
135
|
+
"""Diagnostic codes for automation packages (mirrors ``PackageDiagnosticCodes`` in .NET)."""
|
|
136
|
+
|
|
137
|
+
INVALID_JSON: Final = "VPC2000"
|
|
138
|
+
UNSUPPORTED_FORMAT: Final = "VPC2001"
|
|
139
|
+
UNSUPPORTED_MANIFEST_VERSION: Final = "VPC2002"
|
|
140
|
+
PACKAGE_ID_MISMATCH: Final = "VPC2003"
|
|
141
|
+
PACKAGE_VERSION_MISMATCH: Final = "VPC2004"
|
|
142
|
+
RUNTIME_MISMATCH: Final = "VPC2005"
|
|
143
|
+
REQUIRED_RUNTIMES_MISMATCH: Final = "VPC2006"
|
|
144
|
+
INVALID_FILE_ENTRY: Final = "VPC2007"
|
|
145
|
+
DUPLICATE_FILE: Final = "VPC2008"
|
|
146
|
+
FILES_NOT_SORTED: Final = "VPC2009"
|
|
147
|
+
DOCUMENT_TOO_LARGE: Final = "VPC2010"
|
|
148
|
+
MISSING_PROJECT_MANIFEST: Final = "VPC2011"
|
|
149
|
+
MISSING_FILE: Final = "VPC2012"
|
|
150
|
+
UNLISTED_FILE: Final = "VPC2013"
|
|
151
|
+
HASH_MISMATCH: Final = "VPC2014"
|
|
152
|
+
SIZE_MISMATCH: Final = "VPC2015"
|
|
153
|
+
MISSING_ENTRY_POINT_FILE: Final = "VPC2016"
|
|
154
|
+
MISSING_PYTHON_LOCK_FILE: Final = "VPC2017"
|
|
155
|
+
|
|
156
|
+
#: ``expressionPolicy`` is invalid, has no tier version, or does not carry its own fingerprint.
|
|
157
|
+
INVALID_EXPRESSION_POLICY: Final = "VPC2018"
|