duragraph-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- duragraph/__init__.py +35 -0
- duragraph/cli/__init__.py +5 -0
- duragraph/cli/main.py +163 -0
- duragraph/edges.py +116 -0
- duragraph/graph.py +429 -0
- duragraph/nodes.py +252 -0
- duragraph/prompts/__init__.py +6 -0
- duragraph/prompts/decorators.py +43 -0
- duragraph/prompts/store.py +171 -0
- duragraph/py.typed +0 -0
- duragraph/types.py +100 -0
- duragraph/worker/__init__.py +5 -0
- duragraph/worker/worker.py +327 -0
- duragraph_python-0.1.0.dist-info/METADATA +224 -0
- duragraph_python-0.1.0.dist-info/RECORD +18 -0
- duragraph_python-0.1.0.dist-info/WHEEL +4 -0
- duragraph_python-0.1.0.dist-info/entry_points.txt +2 -0
- duragraph_python-0.1.0.dist-info/licenses/LICENSE +190 -0
duragraph/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""DuraGraph Python SDK - Reliable AI Workflow Orchestration."""
|
|
2
|
+
|
|
3
|
+
from duragraph.edges import edge
|
|
4
|
+
from duragraph.graph import Graph
|
|
5
|
+
from duragraph.nodes import (
|
|
6
|
+
entrypoint,
|
|
7
|
+
human_node,
|
|
8
|
+
llm_node,
|
|
9
|
+
node,
|
|
10
|
+
router_node,
|
|
11
|
+
tool_node,
|
|
12
|
+
)
|
|
13
|
+
from duragraph.types import AIMessage, HumanMessage, Message, State, ToolMessage
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Graph
|
|
19
|
+
"Graph",
|
|
20
|
+
# Node decorators
|
|
21
|
+
"node",
|
|
22
|
+
"llm_node",
|
|
23
|
+
"tool_node",
|
|
24
|
+
"router_node",
|
|
25
|
+
"human_node",
|
|
26
|
+
"entrypoint",
|
|
27
|
+
# Edge
|
|
28
|
+
"edge",
|
|
29
|
+
# Types
|
|
30
|
+
"State",
|
|
31
|
+
"Message",
|
|
32
|
+
"HumanMessage",
|
|
33
|
+
"AIMessage",
|
|
34
|
+
"ToolMessage",
|
|
35
|
+
]
|
duragraph/cli/main.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""DuraGraph CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> int:
|
|
9
|
+
"""Main CLI entry point."""
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="duragraph",
|
|
12
|
+
description="DuraGraph Python SDK CLI",
|
|
13
|
+
)
|
|
14
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
15
|
+
|
|
16
|
+
# init command
|
|
17
|
+
init_parser = subparsers.add_parser("init", help="Initialize a new DuraGraph project")
|
|
18
|
+
init_parser.add_argument("name", help="Project name")
|
|
19
|
+
init_parser.add_argument(
|
|
20
|
+
"--template",
|
|
21
|
+
choices=["minimal", "full"],
|
|
22
|
+
default="minimal",
|
|
23
|
+
help="Project template",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# dev command
|
|
27
|
+
dev_parser = subparsers.add_parser("dev", help="Run in development mode")
|
|
28
|
+
dev_parser.add_argument(
|
|
29
|
+
"--port",
|
|
30
|
+
type=int,
|
|
31
|
+
default=8000,
|
|
32
|
+
help="Port for local server",
|
|
33
|
+
)
|
|
34
|
+
dev_parser.add_argument(
|
|
35
|
+
"--reload",
|
|
36
|
+
action="store_true",
|
|
37
|
+
help="Enable auto-reload",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# deploy command
|
|
41
|
+
deploy_parser = subparsers.add_parser("deploy", help="Deploy to control plane")
|
|
42
|
+
deploy_parser.add_argument(
|
|
43
|
+
"--control-plane",
|
|
44
|
+
required=True,
|
|
45
|
+
help="Control plane URL",
|
|
46
|
+
)
|
|
47
|
+
deploy_parser.add_argument(
|
|
48
|
+
"--graph",
|
|
49
|
+
help="Specific graph to deploy (default: all)",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# visualize command
|
|
53
|
+
viz_parser = subparsers.add_parser("visualize", help="Visualize a graph")
|
|
54
|
+
viz_parser.add_argument("file", help="Python file containing the graph")
|
|
55
|
+
viz_parser.add_argument(
|
|
56
|
+
"--output",
|
|
57
|
+
"-o",
|
|
58
|
+
help="Output file (default: stdout)",
|
|
59
|
+
)
|
|
60
|
+
viz_parser.add_argument(
|
|
61
|
+
"--format",
|
|
62
|
+
choices=["mermaid", "dot", "json"],
|
|
63
|
+
default="mermaid",
|
|
64
|
+
help="Output format",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
args = parser.parse_args()
|
|
68
|
+
|
|
69
|
+
if args.command is None:
|
|
70
|
+
parser.print_help()
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
if args.command == "init":
|
|
74
|
+
return cmd_init(args.name, args.template)
|
|
75
|
+
elif args.command == "dev":
|
|
76
|
+
return cmd_dev(args.port, args.reload)
|
|
77
|
+
elif args.command == "deploy":
|
|
78
|
+
return cmd_deploy(args.control_plane, args.graph)
|
|
79
|
+
elif args.command == "visualize":
|
|
80
|
+
return cmd_visualize(args.file, args.output, args.format)
|
|
81
|
+
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_init(name: str, template: str) -> int:
|
|
86
|
+
"""Initialize a new project."""
|
|
87
|
+
project_dir = Path(name)
|
|
88
|
+
if project_dir.exists():
|
|
89
|
+
print(f"Error: Directory '{name}' already exists")
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
project_dir.mkdir(parents=True)
|
|
93
|
+
|
|
94
|
+
# Create basic structure
|
|
95
|
+
(project_dir / "src").mkdir()
|
|
96
|
+
(project_dir / "tests").mkdir()
|
|
97
|
+
|
|
98
|
+
# Create main agent file
|
|
99
|
+
agent_content = '''"""Example DuraGraph agent."""
|
|
100
|
+
|
|
101
|
+
from duragraph import Graph, llm_node, entrypoint
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@Graph(id="example_agent")
|
|
105
|
+
class ExampleAgent:
|
|
106
|
+
"""A simple example agent."""
|
|
107
|
+
|
|
108
|
+
@entrypoint
|
|
109
|
+
@llm_node(model="gpt-4o-mini")
|
|
110
|
+
def process(self, state):
|
|
111
|
+
"""Process the input."""
|
|
112
|
+
return state
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
agent = ExampleAgent()
|
|
117
|
+
result = agent.run({"message": "Hello, world!"})
|
|
118
|
+
print(result)
|
|
119
|
+
'''
|
|
120
|
+
(project_dir / "src" / "agent.py").write_text(agent_content)
|
|
121
|
+
|
|
122
|
+
# Create pyproject.toml
|
|
123
|
+
pyproject_content = f'''[project]
|
|
124
|
+
name = "{name}"
|
|
125
|
+
version = "0.1.0"
|
|
126
|
+
dependencies = ["duragraph"]
|
|
127
|
+
|
|
128
|
+
[tool.duragraph]
|
|
129
|
+
control_plane = "http://localhost:8081"
|
|
130
|
+
'''
|
|
131
|
+
(project_dir / "pyproject.toml").write_text(pyproject_content)
|
|
132
|
+
|
|
133
|
+
print(f"Created new DuraGraph project: {name}")
|
|
134
|
+
print("\nNext steps:")
|
|
135
|
+
print(f" cd {name}")
|
|
136
|
+
print(" uv sync")
|
|
137
|
+
print(" duragraph dev")
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cmd_dev(port: int, reload: bool) -> int:
|
|
142
|
+
"""Run in development mode."""
|
|
143
|
+
print(f"Starting development server on port {port}...")
|
|
144
|
+
print("Development mode not yet implemented")
|
|
145
|
+
return 1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_deploy(control_plane: str, graph: str | None) -> int:
|
|
149
|
+
"""Deploy to control plane."""
|
|
150
|
+
print(f"Deploying to {control_plane}...")
|
|
151
|
+
print("Deployment not yet implemented")
|
|
152
|
+
return 1
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cmd_visualize(file: str, output: str | None, format: str) -> int:
|
|
156
|
+
"""Visualize a graph."""
|
|
157
|
+
print(f"Visualizing {file}...")
|
|
158
|
+
print("Visualization not yet implemented")
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
sys.exit(main())
|
duragraph/edges.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Edge definitions for DuraGraph workflows."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Edge:
|
|
8
|
+
"""Represents an edge between nodes in a graph."""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
source: str,
|
|
13
|
+
target: str | dict[str, str],
|
|
14
|
+
condition: Callable[[dict[str, Any]], bool] | None = None,
|
|
15
|
+
):
|
|
16
|
+
self.source = source
|
|
17
|
+
self.target = target
|
|
18
|
+
self.condition = condition
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, Any]:
|
|
21
|
+
"""Convert edge to dictionary representation."""
|
|
22
|
+
result: dict[str, Any] = {
|
|
23
|
+
"source": self.source,
|
|
24
|
+
"target": self.target,
|
|
25
|
+
}
|
|
26
|
+
if self.condition is not None:
|
|
27
|
+
result["conditional"] = True
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EdgeBuilder:
|
|
32
|
+
"""Builder for creating edges with fluent API."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, source: str):
|
|
35
|
+
self.source = source
|
|
36
|
+
self._edges: list[Edge] = []
|
|
37
|
+
|
|
38
|
+
def to(
|
|
39
|
+
self,
|
|
40
|
+
target: str,
|
|
41
|
+
*,
|
|
42
|
+
condition: Callable[[dict[str, Any]], bool] | None = None,
|
|
43
|
+
) -> "EdgeBuilder":
|
|
44
|
+
"""Add an edge to a target node.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
target: Name of the target node.
|
|
48
|
+
condition: Optional condition function that takes state and returns bool.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Self for chaining.
|
|
52
|
+
"""
|
|
53
|
+
self._edges.append(Edge(self.source, target, condition))
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def to_conditional(
|
|
57
|
+
self,
|
|
58
|
+
mapping: dict[str, str],
|
|
59
|
+
) -> "EdgeBuilder":
|
|
60
|
+
"""Add conditional edges based on router output.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
mapping: Dictionary mapping router output values to node names.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Self for chaining.
|
|
67
|
+
"""
|
|
68
|
+
self._edges.append(Edge(self.source, mapping))
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def build(self) -> list[Edge]:
|
|
72
|
+
"""Build and return all edges."""
|
|
73
|
+
return self._edges
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def edge(source: str) -> EdgeBuilder:
|
|
77
|
+
"""Create an edge builder starting from a source node.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
source: Name of the source node.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
EdgeBuilder for fluent edge construction.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
# Simple edge
|
|
87
|
+
edge("classify").to("respond")
|
|
88
|
+
|
|
89
|
+
# Conditional edge
|
|
90
|
+
edge("router").to_conditional({
|
|
91
|
+
"billing": "billing_handler",
|
|
92
|
+
"support": "support_handler",
|
|
93
|
+
})
|
|
94
|
+
"""
|
|
95
|
+
return EdgeBuilder(source)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class NodeProxy:
|
|
99
|
+
"""Proxy object for node methods that enables >> operator for edge definition."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, name: str, graph: Any):
|
|
102
|
+
self._name = name
|
|
103
|
+
self._graph = graph
|
|
104
|
+
|
|
105
|
+
def __rshift__(self, other: "NodeProxy") -> "NodeProxy":
|
|
106
|
+
"""Define an edge using >> operator.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
classify >> respond
|
|
110
|
+
"""
|
|
111
|
+
if self._graph is not None:
|
|
112
|
+
self._graph._add_edge(self._name, other._name)
|
|
113
|
+
return other
|
|
114
|
+
|
|
115
|
+
def __repr__(self) -> str:
|
|
116
|
+
return f"NodeProxy({self._name})"
|