init
This commit is contained in:
+208
@@ -0,0 +1,208 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# TypeScript/Build Output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Logs and Runtime Data
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Testing and Coverage
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# IDE/Editor Specific
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS Specific
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from wf_core import RuntimeContext, Workflow, execute_workflow
|
||||||
|
|
||||||
|
|
||||||
|
workflow = Workflow.model_validate(
|
||||||
|
{
|
||||||
|
"name": "drive_summary_demo",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folder_id": {"type": "string"},
|
||||||
|
"should_email": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
"required": ["folder_id", "should_email"],
|
||||||
|
},
|
||||||
|
"state_schema": {
|
||||||
|
"fields": {
|
||||||
|
"folder_id": {"type": "string"},
|
||||||
|
"should_email": {"type": "boolean"},
|
||||||
|
"documents": {"type": "array", "merge_strategy": "replace"},
|
||||||
|
"summary": {"type": "string", "merge_strategy": "replace"},
|
||||||
|
"email_status": {"type": "string", "merge_strategy": "replace"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"summary": {"type": "string"},
|
||||||
|
"email_status": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["summary", "email_status"],
|
||||||
|
},
|
||||||
|
"node_defs": [
|
||||||
|
{
|
||||||
|
"name": "drive_list_files",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"folder_id": {"type": "string"}},
|
||||||
|
"required": ["folder_id"],
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"documents": {"type": "array"}},
|
||||||
|
"required": ["documents"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "summarize_documents",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"documents": {"type": "array"}},
|
||||||
|
"required": ["documents"],
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"summary": {"type": "string"}},
|
||||||
|
"required": ["summary"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "send_email",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"summary": {"type": "string"}},
|
||||||
|
"required": ["summary"],
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"email_status": {"type": "string"}},
|
||||||
|
"required": ["email_status"],
|
||||||
|
},
|
||||||
|
"outcomes": ["sent"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mark_email_skipped",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"email_status": {"type": "string"}},
|
||||||
|
"required": ["email_status"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"start": "list_files",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "list_files",
|
||||||
|
"type": "node",
|
||||||
|
"node": "drive_list_files",
|
||||||
|
"desc": "List files from a Google Drive folder",
|
||||||
|
"in_map": {"input.folder_id": "folder_id"},
|
||||||
|
"out_map": {"documents": "state.documents"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "summarize",
|
||||||
|
"type": "node",
|
||||||
|
"node": "summarize_documents",
|
||||||
|
"desc": "Summarize all retrieved documents",
|
||||||
|
"in_map": {"state.documents": "documents"},
|
||||||
|
"out_map": {"summary": "state.summary"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "should_email",
|
||||||
|
"type": "condition",
|
||||||
|
"check": {
|
||||||
|
"op": "eq",
|
||||||
|
"left": {"path": "state.should_email"},
|
||||||
|
"right": {"value": True},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "send_email",
|
||||||
|
"type": "node",
|
||||||
|
"node": "send_email",
|
||||||
|
"desc": "Send the summary by email",
|
||||||
|
"in_map": {"state.summary": "summary"},
|
||||||
|
"out_map": {"email_status": "state.email_status"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "skip_email",
|
||||||
|
"type": "node",
|
||||||
|
"node": "mark_email_skipped",
|
||||||
|
"desc": "Record that email delivery was skipped",
|
||||||
|
"out_map": {"email_status": "state.email_status"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"from": "list_files", "outcome": "ok", "to": "summarize"},
|
||||||
|
{"from": "summarize", "outcome": "ok", "to": "should_email"},
|
||||||
|
{"from": "should_email", "outcome": "true", "to": "send_email"},
|
||||||
|
{"from": "should_email", "outcome": "false", "to": "skip_email"},
|
||||||
|
{"from": "send_email", "outcome": "sent", "to": "__end__"},
|
||||||
|
{"from": "skip_email", "outcome": "ok", "to": "__end__"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
report = workflow.validate_structure()
|
||||||
|
report.raise_for_errors()
|
||||||
|
|
||||||
|
print("Workflow is structurally valid.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def drive_list_files(
|
||||||
|
payload: dict[str, object], ctx: RuntimeContext
|
||||||
|
) -> dict[str, object]:
|
||||||
|
folder_id = payload["folder_id"]
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {
|
||||||
|
"documents": [
|
||||||
|
f"{folder_id}/meeting-notes.md",
|
||||||
|
f"{folder_id}/weekly-report.md",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_documents(
|
||||||
|
payload: dict[str, object], ctx: RuntimeContext
|
||||||
|
) -> dict[str, object]:
|
||||||
|
documents = payload["documents"]
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {"summary": f"Summarized {len(documents)} documents from Drive."},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(payload: dict[str, object], ctx: RuntimeContext) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"outcome": "sent",
|
||||||
|
"output": {"email_status": f"sent: {payload['summary']}"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mark_email_skipped(
|
||||||
|
payload: dict[str, object], ctx: RuntimeContext
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"outcome": "ok",
|
||||||
|
"output": {"email_status": "skipped"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
registry = {
|
||||||
|
"drive_list_files": drive_list_files,
|
||||||
|
"summarize_documents": summarize_documents,
|
||||||
|
"send_email": send_email,
|
||||||
|
"mark_email_skipped": mark_email_skipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = execute_workflow(
|
||||||
|
workflow,
|
||||||
|
{"folder_id": "demo-folder", "should_email": False},
|
||||||
|
registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[project]
|
||||||
|
name = "lda-w"
|
||||||
|
version = "0.0.1"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = [
|
||||||
|
"mcp[cli,rich]>=1.27.0",
|
||||||
|
"pydantic>=2.13.3",
|
||||||
|
]
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
# lda.chat - Running Design Notes
|
||||||
|
|
||||||
|
## What is this
|
||||||
|
|
||||||
|
`lda.chat` is an AI agent that turns natural language requests into executable
|
||||||
|
workspace workflows. The LLM plans. The executor runs. Tools do the real work.
|
||||||
|
|
||||||
|
Target users are knowledge workers doing repetitive digital tasks like
|
||||||
|
collecting, summarizing, reporting, and monitoring without writing code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thesis scope vs product
|
||||||
|
|
||||||
|
**Thesis:** prove the architecture end to end.
|
||||||
|
|
||||||
|
- a few working MCP tools
|
||||||
|
- LLM plans a workflow from natural language
|
||||||
|
- executor validates and runs it
|
||||||
|
- results come back in structured form
|
||||||
|
|
||||||
|
**Real product:** everything around it.
|
||||||
|
|
||||||
|
- broad MCP bridge layer
|
||||||
|
- per-tool auth handling
|
||||||
|
- tool registry / marketplace
|
||||||
|
- scheduling, monitoring, multi-user support
|
||||||
|
|
||||||
|
The thesis should demonstrate the core split cleanly, not solve the whole platform.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
User request
|
||||||
|
-> LLM planner
|
||||||
|
-> Workflow JSON
|
||||||
|
-> Deterministic executor
|
||||||
|
-> MCP tools / subworkflows
|
||||||
|
-> Shared workflow state
|
||||||
|
```
|
||||||
|
|
||||||
|
Principles:
|
||||||
|
|
||||||
|
- the LLM plans, but does not execute
|
||||||
|
- the executor executes, but does not think
|
||||||
|
- nodes do work
|
||||||
|
- edges are dumb routing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow model
|
||||||
|
|
||||||
|
A workflow is a directed graph with explicit schemas and explicit control flow.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Workflow
|
||||||
|
.name
|
||||||
|
.input_schema
|
||||||
|
.state_schema
|
||||||
|
.output_schema
|
||||||
|
.node_defs[]
|
||||||
|
.start
|
||||||
|
.nodes[]
|
||||||
|
.edges[]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema boundaries
|
||||||
|
|
||||||
|
- `input_schema` validates the run input once
|
||||||
|
- input is copied into mutable workflow state at run start
|
||||||
|
- `state_schema` defines typed workflow memory and merge behavior
|
||||||
|
- `output_schema` is derived from final state at the end
|
||||||
|
- internal state may contain values that never appear in output
|
||||||
|
|
||||||
|
This separation matters because a workflow can later be reused as a node.
|
||||||
|
|
||||||
|
### Reusable node definition
|
||||||
|
|
||||||
|
Nodes are defined independently from any one graph.
|
||||||
|
|
||||||
|
```text
|
||||||
|
NodeDef
|
||||||
|
.name
|
||||||
|
.input_schema
|
||||||
|
.output_schema
|
||||||
|
.outcomes[] // ex: ["ok"], ["approved", "rejected"]
|
||||||
|
.retry?
|
||||||
|
.timeout_seconds?
|
||||||
|
```
|
||||||
|
|
||||||
|
Even a simple node still declares its outcomes explicitly.
|
||||||
|
|
||||||
|
### Node use inside a graph
|
||||||
|
|
||||||
|
Graph use-sites bind reusable nodes into graph state.
|
||||||
|
|
||||||
|
```text
|
||||||
|
NodeUse
|
||||||
|
.id
|
||||||
|
.type: "node"
|
||||||
|
.node // reference to NodeDef
|
||||||
|
.desc?
|
||||||
|
.in_map // graph path -> node input field
|
||||||
|
.out_map // node output field -> graph state path
|
||||||
|
.retry? // optional override
|
||||||
|
.timeout_seconds? // optional override
|
||||||
|
```
|
||||||
|
|
||||||
|
The node does not get ambient access to all workflow state. It receives:
|
||||||
|
|
||||||
|
- mapped input
|
||||||
|
- runtime context
|
||||||
|
|
||||||
|
This keeps nodes reusable instead of graph-coupled.
|
||||||
|
|
||||||
|
### Control-flow nodes
|
||||||
|
|
||||||
|
#### **ConditionNode**
|
||||||
|
|
||||||
|
```text
|
||||||
|
.id
|
||||||
|
.type: "condition"
|
||||||
|
.check
|
||||||
|
```
|
||||||
|
|
||||||
|
Conditions are structured JSON, not freeform code strings.
|
||||||
|
|
||||||
|
#### **ForeachNode**
|
||||||
|
|
||||||
|
```text
|
||||||
|
.id
|
||||||
|
.type: "foreach"
|
||||||
|
.over
|
||||||
|
.as
|
||||||
|
.mode // serial | parallel
|
||||||
|
.on_item_error // fail | collect | skip
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **JoinNode**
|
||||||
|
|
||||||
|
```text
|
||||||
|
.id
|
||||||
|
.type: "join"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edges
|
||||||
|
|
||||||
|
Routing is outcome-based.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Edge
|
||||||
|
.from
|
||||||
|
.outcome
|
||||||
|
.to
|
||||||
|
```
|
||||||
|
|
||||||
|
- source nodes declare which outcomes are possible
|
||||||
|
- edges map those outcomes to next nodes
|
||||||
|
- terminal routing can go to builtin `__end__`
|
||||||
|
|
||||||
|
Reaching an undeclared or unwired outcome is runtime failure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node result contract
|
||||||
|
|
||||||
|
At the engine boundary, every node result should look uniform:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NodeResult
|
||||||
|
.outcome
|
||||||
|
.output
|
||||||
|
.meta?
|
||||||
|
.extra?
|
||||||
|
```
|
||||||
|
|
||||||
|
Two channels stay separate:
|
||||||
|
|
||||||
|
- `outcome` is for control flow
|
||||||
|
- `output` is for typed business data
|
||||||
|
|
||||||
|
This lets richer nodes branch directly, while dumb MCP tools can still be adapted.
|
||||||
|
If a tool only returns data, a later `ConditionNode` can decide routing.
|
||||||
|
|
||||||
|
Extra undeclared returned keys:
|
||||||
|
|
||||||
|
- do not affect typed execution
|
||||||
|
- are preserved in trace
|
||||||
|
- are not visible to later nodes unless a future raw-trace access feature is added
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Condition model
|
||||||
|
|
||||||
|
The condition language should stay small and structured.
|
||||||
|
|
||||||
|
Supported v1 operators:
|
||||||
|
|
||||||
|
- `and`
|
||||||
|
- `or`
|
||||||
|
- `not`
|
||||||
|
- `eq`
|
||||||
|
- `ne`
|
||||||
|
- `gt`
|
||||||
|
- `lt`
|
||||||
|
- `exists`
|
||||||
|
|
||||||
|
Operands are paths or literals, such as:
|
||||||
|
|
||||||
|
- `state.summary_count`
|
||||||
|
- `context.retry_count`
|
||||||
|
- string, number, bool, null literals
|
||||||
|
|
||||||
|
This is less work than inventing a mini-language and generally easier for LLMs to emit reliably.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State and merge semantics
|
||||||
|
|
||||||
|
State fields carry metadata, especially merge behavior.
|
||||||
|
|
||||||
|
```text
|
||||||
|
StateField
|
||||||
|
.type
|
||||||
|
.merge_strategy // replace | append | merge_object
|
||||||
|
.trace?
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- merge behavior belongs to the state field, not the node
|
||||||
|
- declared fields get typed merge semantics
|
||||||
|
- undeclared keys behave like plain map entries
|
||||||
|
- concurrent writes to the same untyped key should fail
|
||||||
|
- concurrent writes to the same typed key should fail unless that field's merge strategy allows it
|
||||||
|
|
||||||
|
This keeps parallel and foreach behavior predictable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution semantics
|
||||||
|
|
||||||
|
Executor steps:
|
||||||
|
|
||||||
|
1. Validate workflow structure
|
||||||
|
2. Validate workflow input
|
||||||
|
3. Copy input into state
|
||||||
|
4. Start at the declared `start` node
|
||||||
|
5. Resolve node input snapshot from `in_map`
|
||||||
|
6. Execute node with mapped input plus runtime context
|
||||||
|
7. Validate typed node output
|
||||||
|
8. Commit mapped output into state
|
||||||
|
9. Route by returned outcome
|
||||||
|
10. Stop when routing reaches `__end__`
|
||||||
|
11. Derive and validate final output from state
|
||||||
|
|
||||||
|
Commit rules:
|
||||||
|
|
||||||
|
- no partial commit before success
|
||||||
|
- validation failure is node failure
|
||||||
|
- execution failure is not a normal business outcome
|
||||||
|
|
||||||
|
Runtime failure means things like:
|
||||||
|
|
||||||
|
- timeout
|
||||||
|
- transport error
|
||||||
|
- MCP failure
|
||||||
|
- invalid node output
|
||||||
|
|
||||||
|
Expected business branching should use declared outcomes instead.
|
||||||
|
|
||||||
|
### Retry
|
||||||
|
|
||||||
|
Default retry behavior should be strict.
|
||||||
|
|
||||||
|
- retry can be declared on the reusable node definition
|
||||||
|
- graph use-site can override it
|
||||||
|
- retries should reuse the original resolved input snapshot by default
|
||||||
|
- failfast is the default engine behavior
|
||||||
|
|
||||||
|
### Foreach
|
||||||
|
|
||||||
|
`foreach` should be explicit, not hidden.
|
||||||
|
|
||||||
|
- execution mode is graph-declared: `serial` or `parallel`
|
||||||
|
- item failure policy is graph-declared: `fail`, `collect`, or `skip`
|
||||||
|
- accumulation follows target state field merge strategy
|
||||||
|
|
||||||
|
### Join
|
||||||
|
|
||||||
|
Join is barrier-only.
|
||||||
|
|
||||||
|
- it does not do custom merge logic
|
||||||
|
- it waits only for branches actually activated in this run
|
||||||
|
- it should not wait for statically possible but never-taken branches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Before execution, the validator should be able to check:
|
||||||
|
|
||||||
|
- workflow has a valid `start`
|
||||||
|
- node def names are unique
|
||||||
|
- node ids are unique
|
||||||
|
- every referenced node definition exists
|
||||||
|
- every edge source exists
|
||||||
|
- every edge destination exists or is `__end__`
|
||||||
|
- every edge outcome is declared by its source node
|
||||||
|
- each source + outcome pair is wired at most once
|
||||||
|
- every reachable outcome is wired
|
||||||
|
- every `in_map` destination exists in node input schema
|
||||||
|
- every `out_map` source exists in node output schema
|
||||||
|
- every condition path references valid `input`, `state`, or `context`
|
||||||
|
- every `foreach.over` path references valid `input` or `state`
|
||||||
|
|
||||||
|
At runtime, the executor should still check:
|
||||||
|
|
||||||
|
- returned outcome is declared
|
||||||
|
- typed output matches schema
|
||||||
|
- merge conflicts
|
||||||
|
- required mapped outputs are present
|
||||||
|
- final output validates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Registry and composition
|
||||||
|
|
||||||
|
A saved workflow can be registered as a reusable node.
|
||||||
|
|
||||||
|
That gives:
|
||||||
|
|
||||||
|
- workflow as function
|
||||||
|
- graph composition
|
||||||
|
- one consistent node contract whether the implementation is MCP-backed or workflow-backed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Thesis demo direction
|
||||||
|
|
||||||
|
A good thesis demo is narrow but real, for example:
|
||||||
|
|
||||||
|
- use an external MCP such as Google Drive
|
||||||
|
- have the planner generate a workflow
|
||||||
|
- validate it against schema
|
||||||
|
- execute the workflow once end to end
|
||||||
|
- return a structured result or report
|
||||||
|
|
||||||
|
That proves the planner/executor/tool split without pretending to solve general automation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
- how strict should output derivation be: direct projection vs explicit final output mapping?
|
||||||
|
- what is the best trace model for raw extra node output?
|
||||||
|
- how should isolated pure-Python execution fit later: node type, tool backend, or separate functional API?
|
||||||
|
- how should retry policy interact with side-effecting tools that are not safely idempotent?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Backend: Python
|
||||||
|
- Validation and schema: Pydantic
|
||||||
|
- Tool execution: MCP
|
||||||
|
- Storage: SQLite or PostgreSQL
|
||||||
|
- Future isolated compute: restricted Python sandbox, possibly remote
|
||||||
+480
@@ -0,0 +1,480 @@
|
|||||||
|
# Graph as struct? workflow as struct
|
||||||
|
|
||||||
|
One graph is one input output
|
||||||
|
|
||||||
|
i decide to make edge dumb as shit. Nodes do all the work
|
||||||
|
|
||||||
|
struct needs to define in, out and ALL states. any output from a node is either writing to state or writing to a key of it.
|
||||||
|
|
||||||
|
metadata for states to hide in tracing
|
||||||
|
|
||||||
|
whatever language works
|
||||||
|
|
||||||
|
## plan
|
||||||
|
|
||||||
|
dumbass stupid plan fuhh ahh plan
|
||||||
|
|
||||||
|
```text
|
||||||
|
.in -> pydantic.create_model
|
||||||
|
.out -> pydantic.create_model
|
||||||
|
.state // langgraph partial update style
|
||||||
|
.steps[] // either nodes only or all everything (all everything seems cooler I LIKE ENUMS but it could be stupih)
|
||||||
|
.steps[]:
|
||||||
|
.type IN node, edge
|
||||||
|
node:
|
||||||
|
.tool // ours, so do ts well
|
||||||
|
.args
|
||||||
|
.desc?
|
||||||
|
.bind? // either update .state or .state[.bind]
|
||||||
|
edge:
|
||||||
|
.from
|
||||||
|
.to
|
||||||
|
```
|
||||||
|
|
||||||
|
### next node
|
||||||
|
|
||||||
|
GENUINELY HOW do i convey next node?
|
||||||
|
do i pass next node in arg? args.next.{this cond, that cond}
|
||||||
|
|
||||||
|
function do need to know about where its run. Who called. So it can decide where it goes next. how? idk maybe a context struct that has the full next list from .edges[]?
|
||||||
|
|
||||||
|
or can it just reference any next node? but how? who tells it what node?
|
||||||
|
|
||||||
|
function 100% can select what node it wants to go next if supported. Since this is MY stuff, a function I make can do that.
|
||||||
|
|
||||||
|
### registry
|
||||||
|
|
||||||
|
past graph can turn into a function. so a graph is lowk same thing as a fn.
|
||||||
|
|
||||||
|
### input
|
||||||
|
|
||||||
|
how in the FUCK do i convey input!
|
||||||
|
|
||||||
|
this is how:
|
||||||
|
|
||||||
|
since input is state, this function kinda doesnt care about args. Args comes from state.
|
||||||
|
|
||||||
|
a function cares about certain keys (args) looking this shape, the least i can do is a remap on node {"specialized graph state key": "generic fn input key"}
|
||||||
|
|
||||||
|
same as output {"generic fn answer key": "specialized graph state key"}
|
||||||
|
|
||||||
|
### this is lowk constricted
|
||||||
|
|
||||||
|
But the counterpart is freeform code...
|
||||||
|
|
||||||
|
### more problems
|
||||||
|
|
||||||
|
batch? foreach?
|
||||||
|
|
||||||
|
control flow.
|
||||||
|
|
||||||
|
maybe special nodes that does control flow. Like ensure: something that only after all nodes connected to it ran does it run the nodes it connects to?
|
||||||
|
|
||||||
|
if bool = exist, maybe truth table node... if .state.key1 and .state.key2
|
||||||
|
|
||||||
|
## Alternative plan! god damn
|
||||||
|
|
||||||
|
that was looking like langgraphs Graph API, we could have Functional API being a taddddd simpler: functions imported and run in a sealed ahh box.
|
||||||
|
|
||||||
|
functional api is actually peam. its like async.
|
||||||
|
|
||||||
|
## inspo
|
||||||
|
|
||||||
|
my little use of langgraph
|
||||||
|
my nonexistent knowledge of openai fn schema
|
||||||
|
claude
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spec direction v1
|
||||||
|
|
||||||
|
This is the cleaner version of the idea above. Raw notes stay above. This part is the engine contract.
|
||||||
|
|
||||||
|
### Core shape
|
||||||
|
|
||||||
|
One workflow has:
|
||||||
|
|
||||||
|
- `input` schema
|
||||||
|
- `state` schema
|
||||||
|
- `output` schema
|
||||||
|
- `nodes`
|
||||||
|
- `edges`
|
||||||
|
- explicit `start`
|
||||||
|
- builtin end node like `__end__`
|
||||||
|
|
||||||
|
Run flow is:
|
||||||
|
|
||||||
|
1. validate workflow structure
|
||||||
|
2. validate workflow input
|
||||||
|
3. copy input into state
|
||||||
|
4. start at declared start node
|
||||||
|
5. execute nodes and route by outcomes
|
||||||
|
6. derive final output from state
|
||||||
|
|
||||||
|
### Schema boundaries
|
||||||
|
|
||||||
|
- `input` is validated once at run start
|
||||||
|
- `state` is the mutable shared memory of the workflow
|
||||||
|
- `output` is derived from final state, not written directly during execution
|
||||||
|
- internal state can contain data that never appears in output
|
||||||
|
- state may be partially open, but only declared state fields get typed merge semantics
|
||||||
|
|
||||||
|
### Node definition
|
||||||
|
|
||||||
|
A reusable node/tool definition has:
|
||||||
|
|
||||||
|
- input schema
|
||||||
|
- output schema
|
||||||
|
- declared outcomes
|
||||||
|
|
||||||
|
Even if the only outcome is `ok`, it is still declared.
|
||||||
|
|
||||||
|
Graph use-sites bind a node explicitly:
|
||||||
|
|
||||||
|
- map graph state into node input
|
||||||
|
- map node typed output back into graph state
|
||||||
|
- wire each declared outcome to an edge or to `__end__`
|
||||||
|
|
||||||
|
Missing required mappings or missing reachable outcomes mean bad graph schema.
|
||||||
|
|
||||||
|
### Node result contract
|
||||||
|
|
||||||
|
Node result has two separate channels:
|
||||||
|
|
||||||
|
- control flow: `outcome`
|
||||||
|
- data: typed `output`
|
||||||
|
|
||||||
|
Routing is driven by `outcome`.
|
||||||
|
State update is driven by mapped `output`.
|
||||||
|
|
||||||
|
Extra undeclared returned keys are:
|
||||||
|
|
||||||
|
- ignored by typed execution semantics
|
||||||
|
- preserved in trace
|
||||||
|
- unavailable to later nodes unless there is some explicit future raw-trace access mechanism
|
||||||
|
|
||||||
|
This keeps dumb MCP tools usable without weakening typed graph execution.
|
||||||
|
|
||||||
|
### Failure model
|
||||||
|
|
||||||
|
- runtime failure is not a normal business outcome
|
||||||
|
- crashes, timeout, transport failure, validation failure are executor-level failure
|
||||||
|
- explicit error outcomes are allowed for expected business cases
|
||||||
|
- infallible nodes may only declare `ok`
|
||||||
|
- node execution either returns one final valid result or fails
|
||||||
|
- no partial state commit before success
|
||||||
|
|
||||||
|
### Commit and validation
|
||||||
|
|
||||||
|
- node input is resolved from state before execution
|
||||||
|
- node sees mapped input plus runtime context, not ambient full state
|
||||||
|
- node output is type-validated before commit
|
||||||
|
- if output validation fails, the node fails
|
||||||
|
- commit happens once, after successful validation
|
||||||
|
|
||||||
|
### Routing semantics
|
||||||
|
|
||||||
|
- edges are dumb
|
||||||
|
- nodes declare what outcomes can happen
|
||||||
|
- graph declares where each outcome can go
|
||||||
|
- reaching an undeclared outcome is runtime failure
|
||||||
|
- reaching an unwired outcome is runtime failure
|
||||||
|
- branching from node business outcomes is preferred over fake follow-up condition nodes
|
||||||
|
|
||||||
|
Condition nodes still exist for cases where:
|
||||||
|
|
||||||
|
- a dumb tool only returns data
|
||||||
|
- routing depends on shared state
|
||||||
|
- routing depends on runtime context
|
||||||
|
|
||||||
|
### Condition nodes
|
||||||
|
|
||||||
|
Condition logic should be structured JSON, not freeform string code.
|
||||||
|
|
||||||
|
Small v1 operator set:
|
||||||
|
|
||||||
|
- `and`
|
||||||
|
- `or`
|
||||||
|
- `not`
|
||||||
|
- `eq`
|
||||||
|
- `ne`
|
||||||
|
- `gt`
|
||||||
|
- `lt`
|
||||||
|
- `exists`
|
||||||
|
|
||||||
|
Operands should be paths and literals, such as:
|
||||||
|
|
||||||
|
- `state.foo`
|
||||||
|
- `context.retry_count`
|
||||||
|
- string/number/bool/null literals
|
||||||
|
|
||||||
|
### Runtime context
|
||||||
|
|
||||||
|
Nodes and condition evaluation can read runtime context.
|
||||||
|
|
||||||
|
Useful context fields:
|
||||||
|
|
||||||
|
- current node id
|
||||||
|
- retry count
|
||||||
|
- prior outcome
|
||||||
|
- loop item metadata
|
||||||
|
- activated incoming edge
|
||||||
|
|
||||||
|
### Merge semantics
|
||||||
|
|
||||||
|
Merge behavior belongs to declared state fields, not to nodes.
|
||||||
|
|
||||||
|
Minimal v1 merge strategies:
|
||||||
|
|
||||||
|
- `replace`
|
||||||
|
- `append`
|
||||||
|
- `merge_object`
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- declared state fields get typed merge behavior
|
||||||
|
- undeclared keys are plain map data
|
||||||
|
- undeclared keys default to simple replace only
|
||||||
|
- concurrent writes to the same untyped key should fail
|
||||||
|
- concurrent writes to the same typed key should fail unless the field merge strategy allows it
|
||||||
|
|
||||||
|
If fancy accumulation is needed, declare the field in state schema.
|
||||||
|
|
||||||
|
### Foreach semantics
|
||||||
|
|
||||||
|
`foreach` is explicit in the graph.
|
||||||
|
|
||||||
|
It should support:
|
||||||
|
|
||||||
|
- `mode = serial | parallel`
|
||||||
|
- `on_item_error = fail | collect | skip`
|
||||||
|
|
||||||
|
Each iteration gets:
|
||||||
|
|
||||||
|
- local item scope
|
||||||
|
- shared read access through mapped input
|
||||||
|
- explicit output mapping back into state
|
||||||
|
|
||||||
|
Iteration accumulation follows the target state field merge strategy.
|
||||||
|
|
||||||
|
### Join semantics
|
||||||
|
|
||||||
|
Join is barrier only.
|
||||||
|
|
||||||
|
- join does not do custom merge logic
|
||||||
|
- state should already hold the branch writes
|
||||||
|
- join waits only for activated incoming branches in this run
|
||||||
|
- join should not wait for statically possible but never-taken branches
|
||||||
|
|
||||||
|
### Retry semantics
|
||||||
|
|
||||||
|
Default retry behavior should be strict and deterministic.
|
||||||
|
|
||||||
|
- retry policy can exist on node definition and be overridden at graph use-site
|
||||||
|
- same for timeout later
|
||||||
|
- retries should reuse the original resolved input snapshot by default
|
||||||
|
- failfast is the default engine behavior
|
||||||
|
- more permissive collection behavior should be opt-in at graph level, especially in `foreach`
|
||||||
|
|
||||||
|
### Reuse and composition
|
||||||
|
|
||||||
|
- workflow input/output separation makes a workflow reusable as a node
|
||||||
|
- graph output should look like normal node typed output
|
||||||
|
- a saved workflow can be registered and called like any other node/tool
|
||||||
|
|
||||||
|
### Concrete schema shape
|
||||||
|
|
||||||
|
The next model pass should aim for a shape like this.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Workflow
|
||||||
|
.name
|
||||||
|
.input_schema
|
||||||
|
.state_schema
|
||||||
|
.output_schema
|
||||||
|
.start
|
||||||
|
.nodes[]
|
||||||
|
.edges[]
|
||||||
|
```
|
||||||
|
|
||||||
|
Reusable node definition:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NodeDef
|
||||||
|
.name
|
||||||
|
.input_schema
|
||||||
|
.output_schema
|
||||||
|
.outcomes[] // ex: ["ok"], ["approved", "rejected"], ...
|
||||||
|
.retry? // default policy on the reusable definition
|
||||||
|
.timeout? // later
|
||||||
|
```
|
||||||
|
|
||||||
|
Graph use-site node:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NodeUse
|
||||||
|
.id
|
||||||
|
.type: "node"
|
||||||
|
.node // reference to NodeDef / registry entry
|
||||||
|
.desc?
|
||||||
|
.in_map // graph state/input path -> node input field
|
||||||
|
.out_map // node output field -> graph state path
|
||||||
|
.retry? // optional override
|
||||||
|
.timeout? // optional override
|
||||||
|
```
|
||||||
|
|
||||||
|
Condition node:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ConditionNode
|
||||||
|
.id
|
||||||
|
.type: "condition"
|
||||||
|
.check // structured JSON condition tree
|
||||||
|
```
|
||||||
|
|
||||||
|
Foreach node:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ForeachNode
|
||||||
|
.id
|
||||||
|
.type: "foreach"
|
||||||
|
.over // state path to iterable
|
||||||
|
.as // loop variable alias
|
||||||
|
.mode // serial | parallel
|
||||||
|
.on_item_error // fail | collect | skip
|
||||||
|
```
|
||||||
|
|
||||||
|
Join node:
|
||||||
|
|
||||||
|
```text
|
||||||
|
JoinNode
|
||||||
|
.id
|
||||||
|
.type: "join"
|
||||||
|
```
|
||||||
|
|
||||||
|
Edge:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Edge
|
||||||
|
.from
|
||||||
|
.outcome // declared outcome name from source node
|
||||||
|
.to
|
||||||
|
```
|
||||||
|
|
||||||
|
Builtin end:
|
||||||
|
|
||||||
|
```text
|
||||||
|
__end__
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps routing uniform:
|
||||||
|
|
||||||
|
- source node emits an outcome
|
||||||
|
- edge matches that outcome
|
||||||
|
- destination node runs next
|
||||||
|
|
||||||
|
### Node result envelope
|
||||||
|
|
||||||
|
At the engine boundary, every node should look like it returns one uniform envelope, even if a dumb MCP tool needs an adapter underneath.
|
||||||
|
|
||||||
|
```text
|
||||||
|
NodeResult
|
||||||
|
.outcome // one of the declared outcomes
|
||||||
|
.output // typed business payload
|
||||||
|
.meta? // reserved executor metadata later
|
||||||
|
.extra? // raw undeclared fields preserved in trace only
|
||||||
|
```
|
||||||
|
|
||||||
|
Practical notes:
|
||||||
|
|
||||||
|
- `outcome` is reserved for control flow
|
||||||
|
- `output` is reserved for typed data
|
||||||
|
- extra raw keys should not silently mix with typed output
|
||||||
|
- if an adapter wraps a dumb tool, it can translate raw tool return -> `NodeResult`
|
||||||
|
|
||||||
|
### Mapping rules
|
||||||
|
|
||||||
|
`in_map` and `out_map` are explicit and first-class.
|
||||||
|
|
||||||
|
`in_map`:
|
||||||
|
|
||||||
|
- source is graph `input`, `state`, or future `context` path
|
||||||
|
- destination is a declared node input field
|
||||||
|
- missing required source path is a graph/runtime error
|
||||||
|
- optional destination fields may be omitted
|
||||||
|
|
||||||
|
`out_map`:
|
||||||
|
|
||||||
|
- source is a declared node output field
|
||||||
|
- destination is a graph state path
|
||||||
|
- mapping to undeclared state keys is allowed, but loses typed merge behavior
|
||||||
|
- mapping a required output field that does not exist at runtime is node failure
|
||||||
|
|
||||||
|
No broad automatch for v1.
|
||||||
|
|
||||||
|
### State schema metadata
|
||||||
|
|
||||||
|
Declared state fields need metadata beyond plain type.
|
||||||
|
|
||||||
|
At minimum each declared field may carry:
|
||||||
|
|
||||||
|
```text
|
||||||
|
StateField
|
||||||
|
.type
|
||||||
|
.merge_strategy // replace | append | merge_object
|
||||||
|
.trace? // whether to include in trace by default
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- merge behavior is owned by state field metadata
|
||||||
|
- trace visibility should also live here later
|
||||||
|
- undeclared keys behave like plain map entries with no special metadata
|
||||||
|
|
||||||
|
### Output derivation
|
||||||
|
|
||||||
|
Workflow output should be built explicitly from state, not by magic.
|
||||||
|
|
||||||
|
Two valid directions:
|
||||||
|
|
||||||
|
- `output_schema` is a projection from named state paths
|
||||||
|
- or `output_schema` is validated against a final explicit output mapping
|
||||||
|
|
||||||
|
Either way:
|
||||||
|
|
||||||
|
- output is not mutated during execution
|
||||||
|
- output validation happens at the end
|
||||||
|
- missing required output fields mean workflow failure
|
||||||
|
|
||||||
|
### Validation checklist
|
||||||
|
|
||||||
|
Before execution, validator should be able to check:
|
||||||
|
|
||||||
|
- workflow has a valid `start`
|
||||||
|
- all node ids are unique
|
||||||
|
- every referenced node definition exists
|
||||||
|
- every edge source exists
|
||||||
|
- every edge destination exists or is `__end__`
|
||||||
|
- every edge outcome is declared by its source node type
|
||||||
|
- every reachable declared outcome is wired
|
||||||
|
- every `in_map` destination exists in node input schema
|
||||||
|
- every `out_map` source exists in node output schema
|
||||||
|
- every condition node uses valid operators and operand shapes
|
||||||
|
|
||||||
|
At runtime, executor should still check:
|
||||||
|
|
||||||
|
- node returned a declared outcome
|
||||||
|
- node returned typed output matching its schema
|
||||||
|
- merge conflicts
|
||||||
|
- missing required mapped outputs
|
||||||
|
- final output validates
|
||||||
|
|
||||||
|
### Non-goals for v1
|
||||||
|
|
||||||
|
- recursive subgraphs
|
||||||
|
- broad implicit automatch for node input/output bindings
|
||||||
|
- ambient full-state access inside nodes
|
||||||
|
- freeform code conditions
|
||||||
|
- hidden magical merge behavior
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-doc"
|
||||||
|
version = "0.0.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "26.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.4.22"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "47.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx-sse"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.13"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema"
|
||||||
|
version = "4.26.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "jsonschema-specifications" },
|
||||||
|
{ name = "referencing" },
|
||||||
|
{ name = "rpds-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema-specifications"
|
||||||
|
version = "2025.9.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "referencing" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lda-w"
|
||||||
|
version = "0.0.1"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mcp", extra = ["cli", "rich"] },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1.27.0" },
|
||||||
|
{ name = "pydantic", specifier = ">=2.13.3" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mcp"
|
||||||
|
version = "1.27.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "httpx-sse" },
|
||||||
|
{ name = "jsonschema" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pydantic-settings" },
|
||||||
|
{ name = "pyjwt", extra = ["crypto"] },
|
||||||
|
{ name = "python-multipart" },
|
||||||
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "sse-starlette" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
cli = [
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "typer" },
|
||||||
|
]
|
||||||
|
rich = [
|
||||||
|
{ name = "rich" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.13.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.46.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-settings"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.20.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyjwt"
|
||||||
|
version = "2.12.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
crypto = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-multipart"
|
||||||
|
version = "0.0.26"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pywin32"
|
||||||
|
version = "311"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "referencing"
|
||||||
|
version = "0.37.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "rpds-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "15.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpds-py"
|
||||||
|
version = "0.30.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellingham"
|
||||||
|
version = "1.5.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sse-starlette"
|
||||||
|
version = "3.3.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "starlette"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer"
|
||||||
|
version = "0.24.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-doc" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "shellingham" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvicorn"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from .model import (
|
||||||
|
ConditionNode,
|
||||||
|
Edge,
|
||||||
|
ForeachNode,
|
||||||
|
JoinNode,
|
||||||
|
NodeDef,
|
||||||
|
NodeResult,
|
||||||
|
NodeUse,
|
||||||
|
StateField,
|
||||||
|
StateSchema,
|
||||||
|
Workflow,
|
||||||
|
)
|
||||||
|
from .runtime import RuntimeContext, WorkflowExecutionError, execute_workflow
|
||||||
|
from .validate import ValidationIssue, ValidationReport, validate_workflow
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ConditionNode",
|
||||||
|
"Edge",
|
||||||
|
"ForeachNode",
|
||||||
|
"JoinNode",
|
||||||
|
"NodeDef",
|
||||||
|
"NodeResult",
|
||||||
|
"NodeUse",
|
||||||
|
"StateField",
|
||||||
|
"StateSchema",
|
||||||
|
"RuntimeContext",
|
||||||
|
"ValidationIssue",
|
||||||
|
"ValidationReport",
|
||||||
|
"Workflow",
|
||||||
|
"WorkflowExecutionError",
|
||||||
|
"execute_workflow",
|
||||||
|
"validate_workflow",
|
||||||
|
]
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated, Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaRef(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
title: str | None = None
|
||||||
|
type: str | None = None
|
||||||
|
properties: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
required: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class StateField(BaseModel):
|
||||||
|
type: str
|
||||||
|
merge_strategy: Literal["replace", "append", "merge_object"] = "replace"
|
||||||
|
trace: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class StateSchema(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
fields: dict[str, StateField] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeDef(BaseModel):
|
||||||
|
name: str
|
||||||
|
input_schema: SchemaRef
|
||||||
|
output_schema: SchemaRef
|
||||||
|
outcomes: list[str] = Field(min_length=1)
|
||||||
|
retry: int | None = Field(None, ge=0)
|
||||||
|
timeout_seconds: int | None = Field(None, gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeUse(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: Literal["node"]
|
||||||
|
node: str
|
||||||
|
desc: str | None = None
|
||||||
|
in_map: dict[str, str] = Field(default_factory=dict)
|
||||||
|
out_map: dict[str, str] = Field(default_factory=dict)
|
||||||
|
retry: int | None = Field(None, ge=0)
|
||||||
|
timeout_seconds: int | None = Field(None, gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
class PathOperand(BaseModel):
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
class LiteralOperand(BaseModel):
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
Operand = Annotated[PathOperand | LiteralOperand, Field(discriminator=None)]
|
||||||
|
|
||||||
|
|
||||||
|
class ExistsCondition(BaseModel):
|
||||||
|
op: Literal["exists"]
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
class NotCondition(BaseModel):
|
||||||
|
op: Literal["not"]
|
||||||
|
arg: "Condition"
|
||||||
|
|
||||||
|
|
||||||
|
class VariadicCondition(BaseModel):
|
||||||
|
op: Literal["and", "or"]
|
||||||
|
args: list["Condition"] = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryCondition(BaseModel):
|
||||||
|
op: Literal["eq", "ne", "gt", "lt"]
|
||||||
|
left: PathOperand | LiteralOperand
|
||||||
|
right: PathOperand | LiteralOperand
|
||||||
|
|
||||||
|
|
||||||
|
Condition = Annotated[
|
||||||
|
ExistsCondition | NotCondition | VariadicCondition | BinaryCondition,
|
||||||
|
Field(discriminator="op"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionNode(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: Literal["condition"]
|
||||||
|
check: Condition
|
||||||
|
|
||||||
|
|
||||||
|
class ForeachNode(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: Literal["foreach"]
|
||||||
|
over: str
|
||||||
|
as_: str = Field(alias="as")
|
||||||
|
mode: Literal["serial", "parallel"] = "serial"
|
||||||
|
on_item_error: Literal["fail", "collect", "skip"] = "fail"
|
||||||
|
|
||||||
|
|
||||||
|
class JoinNode(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: Literal["join"]
|
||||||
|
|
||||||
|
|
||||||
|
Step = Annotated[
|
||||||
|
NodeUse | ConditionNode | ForeachNode | JoinNode,
|
||||||
|
Field(discriminator="type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Edge(BaseModel):
|
||||||
|
from_: str = Field(alias="from")
|
||||||
|
outcome: str
|
||||||
|
to: str
|
||||||
|
|
||||||
|
|
||||||
|
class Workflow(BaseModel):
|
||||||
|
name: str
|
||||||
|
input_schema: SchemaRef
|
||||||
|
state_schema: StateSchema
|
||||||
|
output_schema: SchemaRef
|
||||||
|
node_defs: list[NodeDef] = Field(default_factory=list)
|
||||||
|
start: str
|
||||||
|
nodes: list[Step]
|
||||||
|
edges: list[Edge]
|
||||||
|
|
||||||
|
def validate_structure(self):
|
||||||
|
from .validate import validate_workflow
|
||||||
|
|
||||||
|
return validate_workflow(self)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeResult(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
outcome: str
|
||||||
|
output: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
meta: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
|
||||||
|
print(json.dumps(Workflow.model_json_schema(), indent=2))
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .model import (
|
||||||
|
BinaryCondition,
|
||||||
|
Condition,
|
||||||
|
ConditionNode,
|
||||||
|
ExistsCondition,
|
||||||
|
ForeachNode,
|
||||||
|
JoinNode,
|
||||||
|
LiteralOperand,
|
||||||
|
NodeDef,
|
||||||
|
NodeResult,
|
||||||
|
NodeUse,
|
||||||
|
NotCondition,
|
||||||
|
PathOperand,
|
||||||
|
VariadicCondition,
|
||||||
|
Workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowExecutionError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class RuntimeContext:
|
||||||
|
current_node_id: str
|
||||||
|
retry_count: int = 0
|
||||||
|
prior_outcome: str | None = None
|
||||||
|
activated_incoming_edge: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
NodeHandler = Callable[[dict[str, Any], RuntimeContext], NodeResult | dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def execute_workflow(
|
||||||
|
workflow: Workflow,
|
||||||
|
workflow_input: dict[str, Any],
|
||||||
|
registry: dict[str, NodeHandler],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
report = workflow.validate_structure()
|
||||||
|
report.raise_for_errors()
|
||||||
|
|
||||||
|
_validate_payload_against_schema(workflow.input_schema, workflow_input, "workflow input")
|
||||||
|
|
||||||
|
node_defs = {node_def.name: node_def for node_def in workflow.node_defs}
|
||||||
|
nodes_by_id = {node.id: node for node in workflow.nodes}
|
||||||
|
edge_map = {(edge.from_, edge.outcome): edge.to for edge in workflow.edges}
|
||||||
|
|
||||||
|
state = dict(workflow_input)
|
||||||
|
current_node_id = workflow.start
|
||||||
|
prior_outcome: str | None = None
|
||||||
|
activated_incoming_edge: str | None = None
|
||||||
|
|
||||||
|
while current_node_id != "__end__":
|
||||||
|
step = nodes_by_id[current_node_id]
|
||||||
|
|
||||||
|
if isinstance(step, NodeUse):
|
||||||
|
node_def = node_defs[step.node]
|
||||||
|
outcome = _execute_node_use(
|
||||||
|
step,
|
||||||
|
node_def,
|
||||||
|
state,
|
||||||
|
workflow_input,
|
||||||
|
registry,
|
||||||
|
prior_outcome,
|
||||||
|
activated_incoming_edge,
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
elif isinstance(step, ConditionNode):
|
||||||
|
predicate = _eval_condition(step.check, state, workflow_input, prior_outcome)
|
||||||
|
outcome = "true" if predicate else "false"
|
||||||
|
elif isinstance(step, JoinNode):
|
||||||
|
outcome = "done"
|
||||||
|
elif isinstance(step, ForeachNode):
|
||||||
|
raise WorkflowExecutionError("foreach execution is not implemented yet")
|
||||||
|
else:
|
||||||
|
raise WorkflowExecutionError(f"unsupported step type {step.type!r}")
|
||||||
|
|
||||||
|
next_node_id = edge_map.get((current_node_id, outcome))
|
||||||
|
if next_node_id is None:
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"no edge found for node {current_node_id!r} and outcome {outcome!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
prior_outcome = outcome
|
||||||
|
activated_incoming_edge = current_node_id
|
||||||
|
current_node_id = next_node_id
|
||||||
|
|
||||||
|
final_output = _project_output(workflow, state)
|
||||||
|
_validate_payload_against_schema(workflow.output_schema, final_output, "workflow output")
|
||||||
|
return {"state": state, "output": final_output}
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_node_use(
|
||||||
|
node: NodeUse,
|
||||||
|
node_def: NodeDef,
|
||||||
|
state: dict[str, Any],
|
||||||
|
workflow_input: dict[str, Any],
|
||||||
|
registry: dict[str, NodeHandler],
|
||||||
|
prior_outcome: str | None,
|
||||||
|
activated_incoming_edge: str | None,
|
||||||
|
workflow: Workflow,
|
||||||
|
) -> str:
|
||||||
|
handler = registry.get(node.node)
|
||||||
|
if handler is None:
|
||||||
|
raise WorkflowExecutionError(f"no handler registered for node def {node.node!r}")
|
||||||
|
|
||||||
|
resolved_input = {
|
||||||
|
destination_field: _resolve_path(source_path, state, workflow_input, {})
|
||||||
|
for source_path, destination_field in node.in_map.items()
|
||||||
|
}
|
||||||
|
_validate_payload_against_schema(
|
||||||
|
node_def.input_schema, resolved_input, f"node input for {node.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
context = RuntimeContext(
|
||||||
|
current_node_id=node.id,
|
||||||
|
prior_outcome=prior_outcome,
|
||||||
|
activated_incoming_edge=activated_incoming_edge,
|
||||||
|
)
|
||||||
|
raw_result = handler(resolved_input, context)
|
||||||
|
result = _coerce_node_result(raw_result)
|
||||||
|
|
||||||
|
if result.outcome not in node_def.outcomes:
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"node {node.id!r} returned undeclared outcome {result.outcome!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
_validate_payload_against_schema(
|
||||||
|
node_def.output_schema, result.output, f"node output for {node.id}"
|
||||||
|
)
|
||||||
|
_apply_output_map(workflow, node, result.output, state)
|
||||||
|
return result.outcome
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_node_result(raw_result: NodeResult | dict[str, Any]) -> NodeResult:
|
||||||
|
if isinstance(raw_result, NodeResult):
|
||||||
|
return raw_result
|
||||||
|
if "outcome" in raw_result and "output" in raw_result:
|
||||||
|
return NodeResult.model_validate(raw_result)
|
||||||
|
return NodeResult(outcome="ok", output=raw_result)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_output_map(
|
||||||
|
workflow: Workflow,
|
||||||
|
node: NodeUse,
|
||||||
|
node_output: dict[str, Any],
|
||||||
|
state: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
for source_field, destination_path in node.out_map.items():
|
||||||
|
if source_field not in node_output:
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"node {node.id!r} did not return required mapped field {source_field!r}"
|
||||||
|
)
|
||||||
|
value = node_output[source_field]
|
||||||
|
_write_state_value(workflow, state, destination_path, value)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_state_value(
|
||||||
|
workflow: Workflow, state: dict[str, Any], destination_path: str, value: Any
|
||||||
|
) -> None:
|
||||||
|
root, field_name, *rest = destination_path.split(".")
|
||||||
|
if root != "state":
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"executor only supports writes into state.*, got {destination_path!r}"
|
||||||
|
)
|
||||||
|
declared_field = workflow.state_schema.fields.get(field_name)
|
||||||
|
merge_strategy = declared_field.merge_strategy if declared_field else "replace"
|
||||||
|
key_path = [field_name, *rest]
|
||||||
|
|
||||||
|
if merge_strategy == "replace":
|
||||||
|
_set_nested_value(state, key_path, value)
|
||||||
|
return
|
||||||
|
|
||||||
|
current_value = _get_nested_value(state, key_path)
|
||||||
|
if merge_strategy == "append":
|
||||||
|
if current_value is None:
|
||||||
|
_set_nested_value(state, key_path, [value] if not isinstance(value, list) else value)
|
||||||
|
return
|
||||||
|
if not isinstance(current_value, list):
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"cannot append into non-list state path {destination_path!r}"
|
||||||
|
)
|
||||||
|
if isinstance(value, list):
|
||||||
|
current_value.extend(value)
|
||||||
|
else:
|
||||||
|
current_value.append(value)
|
||||||
|
return
|
||||||
|
|
||||||
|
if merge_strategy == "merge_object":
|
||||||
|
if current_value is None:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"cannot merge non-object value into {destination_path!r}"
|
||||||
|
)
|
||||||
|
_set_nested_value(state, key_path, dict(value))
|
||||||
|
return
|
||||||
|
if not isinstance(current_value, dict) or not isinstance(value, dict):
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"merge_object requires dict values at {destination_path!r}"
|
||||||
|
)
|
||||||
|
current_value.update(value)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise WorkflowExecutionError(f"unknown merge strategy {merge_strategy!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _project_output(workflow: Workflow, state: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
key: state[key]
|
||||||
|
for key in workflow.output_schema.properties
|
||||||
|
if key in state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_payload_against_schema(schema: Any, payload: Any, label: str) -> None:
|
||||||
|
if schema.type == "object":
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise WorkflowExecutionError(f"{label} must be an object")
|
||||||
|
for required_key in schema.required:
|
||||||
|
if required_key not in payload:
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"{label} is missing required field {required_key!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _eval_condition(
|
||||||
|
condition: Condition,
|
||||||
|
state: dict[str, Any],
|
||||||
|
workflow_input: dict[str, Any],
|
||||||
|
context_data: str | None,
|
||||||
|
) -> bool:
|
||||||
|
if isinstance(condition, ExistsCondition):
|
||||||
|
return _path_exists(condition.path, state, workflow_input, context_data)
|
||||||
|
if isinstance(condition, NotCondition):
|
||||||
|
return not _eval_condition(condition.arg, state, workflow_input, context_data)
|
||||||
|
if isinstance(condition, VariadicCondition):
|
||||||
|
values = [
|
||||||
|
_eval_condition(arg, state, workflow_input, context_data)
|
||||||
|
for arg in condition.args
|
||||||
|
]
|
||||||
|
return all(values) if condition.op == "and" else any(values)
|
||||||
|
if isinstance(condition, BinaryCondition):
|
||||||
|
left = _resolve_operand(condition.left, state, workflow_input, context_data)
|
||||||
|
right = _resolve_operand(condition.right, state, workflow_input, context_data)
|
||||||
|
if condition.op == "eq":
|
||||||
|
return left == right
|
||||||
|
if condition.op == "ne":
|
||||||
|
return left != right
|
||||||
|
if condition.op == "gt":
|
||||||
|
return left > right
|
||||||
|
if condition.op == "lt":
|
||||||
|
return left < right
|
||||||
|
raise WorkflowExecutionError(f"unsupported condition operator {condition.op!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_operand(
|
||||||
|
operand: PathOperand | LiteralOperand,
|
||||||
|
state: dict[str, Any],
|
||||||
|
workflow_input: dict[str, Any],
|
||||||
|
context_data: str | None,
|
||||||
|
) -> Any:
|
||||||
|
if isinstance(operand, LiteralOperand):
|
||||||
|
return operand.value
|
||||||
|
return _resolve_path(
|
||||||
|
operand.path,
|
||||||
|
state,
|
||||||
|
workflow_input,
|
||||||
|
{"prior_outcome": context_data},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _path_exists(
|
||||||
|
path: str,
|
||||||
|
state: dict[str, Any],
|
||||||
|
workflow_input: dict[str, Any],
|
||||||
|
context_data: str | None,
|
||||||
|
) -> bool:
|
||||||
|
try:
|
||||||
|
_resolve_path(path, state, workflow_input, {"prior_outcome": context_data})
|
||||||
|
except WorkflowExecutionError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_path(
|
||||||
|
path: str,
|
||||||
|
state: dict[str, Any],
|
||||||
|
workflow_input: dict[str, Any],
|
||||||
|
context: dict[str, Any],
|
||||||
|
) -> Any:
|
||||||
|
root, *parts = path.split(".")
|
||||||
|
if not parts:
|
||||||
|
raise WorkflowExecutionError(f"invalid path {path!r}")
|
||||||
|
|
||||||
|
if root == "state":
|
||||||
|
source = state
|
||||||
|
elif root == "input":
|
||||||
|
source = workflow_input
|
||||||
|
elif root == "context":
|
||||||
|
source = context
|
||||||
|
else:
|
||||||
|
raise WorkflowExecutionError(f"unknown path root {root!r}")
|
||||||
|
|
||||||
|
current: Any = source
|
||||||
|
for part in parts:
|
||||||
|
if not isinstance(current, dict) or part not in current:
|
||||||
|
raise WorkflowExecutionError(f"path {path!r} could not be resolved")
|
||||||
|
current = current[part]
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nested_value(state: dict[str, Any], path_parts: list[str]) -> Any:
|
||||||
|
current: Any = state
|
||||||
|
for part in path_parts:
|
||||||
|
if not isinstance(current, dict) or part not in current:
|
||||||
|
return None
|
||||||
|
current = current[part]
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def _set_nested_value(state: dict[str, Any], path_parts: list[str], value: Any) -> None:
|
||||||
|
current = state
|
||||||
|
for part in path_parts[:-1]:
|
||||||
|
next_value = current.get(part)
|
||||||
|
if next_value is None:
|
||||||
|
next_value = {}
|
||||||
|
current[part] = next_value
|
||||||
|
if not isinstance(next_value, dict):
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"cannot descend into non-object state field {part!r}"
|
||||||
|
)
|
||||||
|
current = next_value
|
||||||
|
current[path_parts[-1]] = value
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from .model import (
|
||||||
|
BinaryCondition,
|
||||||
|
Condition,
|
||||||
|
ConditionNode,
|
||||||
|
ExistsCondition,
|
||||||
|
ForeachNode,
|
||||||
|
LiteralOperand,
|
||||||
|
NodeDef,
|
||||||
|
NodeUse,
|
||||||
|
NotCondition,
|
||||||
|
PathOperand,
|
||||||
|
Step,
|
||||||
|
VariadicCondition,
|
||||||
|
Workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ValidationIssue:
|
||||||
|
path: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ValidationReport:
|
||||||
|
errors: list[ValidationIssue] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ok(self) -> bool:
|
||||||
|
return not self.errors
|
||||||
|
|
||||||
|
def add(self, path: str, message: str) -> None:
|
||||||
|
self.errors.append(ValidationIssue(path=path, message=message))
|
||||||
|
|
||||||
|
def raise_for_errors(self) -> None:
|
||||||
|
if not self.errors:
|
||||||
|
return
|
||||||
|
rendered = "\n".join(
|
||||||
|
f"- {issue.path}: {issue.message}" for issue in self.errors
|
||||||
|
)
|
||||||
|
raise ValueError(f"Workflow validation failed:\n{rendered}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_workflow(workflow: Workflow) -> ValidationReport:
|
||||||
|
report = ValidationReport()
|
||||||
|
|
||||||
|
node_defs: dict[str, NodeDef] = {}
|
||||||
|
for index, node_def in enumerate(workflow.node_defs):
|
||||||
|
if node_def.name in node_defs:
|
||||||
|
report.add(
|
||||||
|
f"node_defs[{index}].name", f"duplicate node def name {node_def.name!r}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
node_defs[node_def.name] = node_def
|
||||||
|
|
||||||
|
nodes_by_id = {}
|
||||||
|
state_root_fields = set(workflow.state_schema.fields)
|
||||||
|
input_root_fields = set(workflow.input_schema.properties)
|
||||||
|
|
||||||
|
for index, node in enumerate(workflow.nodes):
|
||||||
|
if node.id in nodes_by_id:
|
||||||
|
report.add(f"nodes[{index}].id", f"duplicate node id {node.id!r}")
|
||||||
|
else:
|
||||||
|
nodes_by_id[node.id] = node
|
||||||
|
|
||||||
|
if isinstance(node, NodeUse):
|
||||||
|
_validate_node_use(node, index, node_defs, workflow, report)
|
||||||
|
elif isinstance(node, ConditionNode):
|
||||||
|
_validate_condition_node(
|
||||||
|
node, index, report, state_root_fields, input_root_fields
|
||||||
|
)
|
||||||
|
elif isinstance(node, ForeachNode):
|
||||||
|
_validate_foreach_node(
|
||||||
|
node, index, report, state_root_fields, input_root_fields
|
||||||
|
)
|
||||||
|
|
||||||
|
if workflow.start not in nodes_by_id:
|
||||||
|
report.add("start", f"unknown start node {workflow.start!r}")
|
||||||
|
|
||||||
|
outgoing: dict[str, set[str]] = {}
|
||||||
|
edge_keys: set[tuple[str, str]] = set()
|
||||||
|
|
||||||
|
for index, edge in enumerate(workflow.edges):
|
||||||
|
edge_key = (edge.from_, edge.outcome)
|
||||||
|
if edge_key in edge_keys:
|
||||||
|
report.add(
|
||||||
|
f"edges[{index}]",
|
||||||
|
f"duplicate edge for source {edge.from_!r} and outcome {edge.outcome!r}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
edge_keys.add(edge_key)
|
||||||
|
|
||||||
|
source = nodes_by_id.get(edge.from_)
|
||||||
|
if source is None:
|
||||||
|
report.add(f"edges[{index}].from", f"unknown source node {edge.from_!r}")
|
||||||
|
else:
|
||||||
|
allowed = _declared_outcomes_for_step(source, node_defs)
|
||||||
|
if edge.outcome not in allowed:
|
||||||
|
report.add(
|
||||||
|
f"edges[{index}].outcome",
|
||||||
|
f"outcome {edge.outcome!r} is not declared by node {edge.from_!r}",
|
||||||
|
)
|
||||||
|
outgoing.setdefault(edge.from_, set()).add(edge.outcome)
|
||||||
|
|
||||||
|
if edge.to != "__end__":
|
||||||
|
if edge.to not in nodes_by_id:
|
||||||
|
report.add(
|
||||||
|
f"edges[{index}].to", f"unknown destination node {edge.to!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
reachable = _reachable_node_ids(workflow.start, workflow.edges, nodes_by_id)
|
||||||
|
|
||||||
|
for node_id in reachable:
|
||||||
|
node = nodes_by_id[node_id]
|
||||||
|
declared_outcomes = _declared_outcomes_for_step(node, node_defs)
|
||||||
|
wired = outgoing.get(node_id, set())
|
||||||
|
missing = declared_outcomes - wired
|
||||||
|
if missing:
|
||||||
|
report.add(
|
||||||
|
f"nodes[{node_id}]",
|
||||||
|
f"reachable node is missing edges for outcomes {sorted(missing)!r}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_node_use(
|
||||||
|
node: NodeUse,
|
||||||
|
index: int,
|
||||||
|
node_defs: dict[str, NodeDef],
|
||||||
|
workflow: Workflow,
|
||||||
|
report: ValidationReport,
|
||||||
|
) -> None:
|
||||||
|
node_def = node_defs.get(node.node)
|
||||||
|
if node_def is None:
|
||||||
|
report.add(f"nodes[{index}].node", f"unknown node def {node.node!r}")
|
||||||
|
return
|
||||||
|
|
||||||
|
input_fields = set(node_def.input_schema.properties)
|
||||||
|
output_fields = set(node_def.output_schema.properties)
|
||||||
|
state_fields = set(workflow.state_schema.fields)
|
||||||
|
input_root_fields = set(workflow.input_schema.properties)
|
||||||
|
|
||||||
|
for source_path, destination_field in node.in_map.items():
|
||||||
|
if destination_field not in input_fields:
|
||||||
|
report.add(
|
||||||
|
f"nodes[{index}].in_map[{source_path!r}]",
|
||||||
|
f"destination field {destination_field!r} is not declared in node input schema",
|
||||||
|
)
|
||||||
|
if not _is_valid_source_path(source_path, state_fields, input_root_fields):
|
||||||
|
report.add(
|
||||||
|
f"nodes[{index}].in_map[{source_path!r}]",
|
||||||
|
"source path must start with input. or state. and reference a declared root field",
|
||||||
|
)
|
||||||
|
|
||||||
|
for source_field, destination_path in node.out_map.items():
|
||||||
|
if source_field not in output_fields:
|
||||||
|
report.add(
|
||||||
|
f"nodes[{index}].out_map[{source_field!r}]",
|
||||||
|
f"source field {source_field!r} is not declared in node output schema",
|
||||||
|
)
|
||||||
|
if not _is_valid_destination_path(destination_path):
|
||||||
|
report.add(
|
||||||
|
f"nodes[{index}].out_map[{source_field!r}]",
|
||||||
|
"destination path must start with state.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_condition_node(
|
||||||
|
node: ConditionNode,
|
||||||
|
index: int,
|
||||||
|
report: ValidationReport,
|
||||||
|
state_root_fields: set[str],
|
||||||
|
input_root_fields: set[str],
|
||||||
|
) -> None:
|
||||||
|
if isinstance(node.check, VariadicCondition) and not node.check.args:
|
||||||
|
report.add(f"nodes[{index}].check.args", "condition args must not be empty")
|
||||||
|
_validate_condition_expr(
|
||||||
|
node.check,
|
||||||
|
f"nodes[{index}].check",
|
||||||
|
report,
|
||||||
|
state_root_fields,
|
||||||
|
input_root_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_foreach_node(
|
||||||
|
node: ForeachNode,
|
||||||
|
index: int,
|
||||||
|
report: ValidationReport,
|
||||||
|
state_root_fields: set[str],
|
||||||
|
input_root_fields: set[str],
|
||||||
|
) -> None:
|
||||||
|
if not _is_valid_source_path(node.over, state_root_fields, input_root_fields):
|
||||||
|
report.add(
|
||||||
|
f"nodes[{index}].over",
|
||||||
|
"foreach source path must start with input. or state. and reference a declared root field",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_condition_expr(
|
||||||
|
condition: Condition,
|
||||||
|
path: str,
|
||||||
|
report: ValidationReport,
|
||||||
|
state_root_fields: set[str],
|
||||||
|
input_root_fields: set[str],
|
||||||
|
) -> None:
|
||||||
|
if isinstance(condition, ExistsCondition):
|
||||||
|
if not _is_valid_condition_path(
|
||||||
|
condition.path, state_root_fields, input_root_fields
|
||||||
|
):
|
||||||
|
report.add(path, f"invalid condition path {condition.path!r}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(condition, NotCondition):
|
||||||
|
_validate_condition_expr(
|
||||||
|
condition.arg,
|
||||||
|
f"{path}.arg",
|
||||||
|
report,
|
||||||
|
state_root_fields,
|
||||||
|
input_root_fields,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(condition, VariadicCondition):
|
||||||
|
for index, arg in enumerate(condition.args):
|
||||||
|
_validate_condition_expr(
|
||||||
|
arg,
|
||||||
|
f"{path}.args[{index}]",
|
||||||
|
report,
|
||||||
|
state_root_fields,
|
||||||
|
input_root_fields,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(condition, BinaryCondition):
|
||||||
|
_validate_operand(
|
||||||
|
condition.left,
|
||||||
|
f"{path}.left",
|
||||||
|
report,
|
||||||
|
state_root_fields,
|
||||||
|
input_root_fields,
|
||||||
|
)
|
||||||
|
_validate_operand(
|
||||||
|
condition.right,
|
||||||
|
f"{path}.right",
|
||||||
|
report,
|
||||||
|
state_root_fields,
|
||||||
|
input_root_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_operand(
|
||||||
|
operand: PathOperand | LiteralOperand,
|
||||||
|
path: str,
|
||||||
|
report: ValidationReport,
|
||||||
|
state_root_fields: set[str],
|
||||||
|
input_root_fields: set[str],
|
||||||
|
) -> None:
|
||||||
|
if isinstance(operand, LiteralOperand):
|
||||||
|
return
|
||||||
|
if not _is_valid_condition_path(operand.path, state_root_fields, input_root_fields):
|
||||||
|
report.add(path, f"invalid operand path {operand.path!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _declared_outcomes_for_step(step: Step, node_defs: dict[str, NodeDef]) -> set[str]:
|
||||||
|
if isinstance(step, NodeUse):
|
||||||
|
node_def = node_defs.get(step.node)
|
||||||
|
return set(node_def.outcomes) if node_def else set()
|
||||||
|
if step.type == "condition":
|
||||||
|
return {"true", "false"}
|
||||||
|
if step.type == "foreach":
|
||||||
|
return {"done"}
|
||||||
|
if step.type == "join":
|
||||||
|
return {"done"}
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def _reachable_node_ids(start: str, edges, nodes_by_id: dict[str, Step]) -> set[str]:
|
||||||
|
if start not in nodes_by_id:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
adjacency: dict[str, list[str]] = {}
|
||||||
|
for edge in edges:
|
||||||
|
if edge.to == "__end__":
|
||||||
|
continue
|
||||||
|
adjacency.setdefault(edge.from_, []).append(edge.to)
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
stack = [start]
|
||||||
|
while stack:
|
||||||
|
node_id = stack.pop()
|
||||||
|
if node_id in seen:
|
||||||
|
continue
|
||||||
|
seen.add(node_id)
|
||||||
|
stack.extend(adjacency.get(node_id, []))
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_source_path(
|
||||||
|
path: str, state_root_fields: set[str], input_root_fields: set[str]
|
||||||
|
) -> bool:
|
||||||
|
if "." not in path:
|
||||||
|
return False
|
||||||
|
root, field_name, *_ = path.split(".")
|
||||||
|
if root == "state":
|
||||||
|
return field_name in state_root_fields
|
||||||
|
if root == "input":
|
||||||
|
return field_name in input_root_fields
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_condition_path(
|
||||||
|
path: str, state_root_fields: set[str], input_root_fields: set[str]
|
||||||
|
) -> bool:
|
||||||
|
if "." not in path:
|
||||||
|
return False
|
||||||
|
root, field_name, *_ = path.split(".")
|
||||||
|
if root == "context":
|
||||||
|
return True
|
||||||
|
if root == "state":
|
||||||
|
return field_name in state_root_fields
|
||||||
|
if root == "input":
|
||||||
|
return field_name in input_root_fields
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_destination_path(path: str) -> bool:
|
||||||
|
return path.startswith("state.") and len(path.split(".")) >= 2
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 33 KiB |
Reference in New Issue
Block a user