yamlgraph 0.3.9__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.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
examples/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Examples package - sample projects demonstrating framework features."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Schemas for impl-agent discovery and planning.
|
|
2
|
+
|
|
3
|
+
These models define the structure for todo-driven discovery workflow.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DiscoveryTask(BaseModel):
|
|
14
|
+
"""A single discovery task for impl-agent.
|
|
15
|
+
|
|
16
|
+
Represents one investigation step in the discovery plan.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
id: int = Field(description="Unique task identifier")
|
|
20
|
+
task: str = Field(description="Human-readable description of what to discover")
|
|
21
|
+
tool: str = Field(description="Name of the tool to call")
|
|
22
|
+
args: dict[str, Any] = Field(description="Arguments to pass to the tool")
|
|
23
|
+
rationale: str = Field(description="Why this discovery task matters")
|
|
24
|
+
status: Literal["pending", "done", "failed"] = Field(
|
|
25
|
+
default="pending",
|
|
26
|
+
description="Current status of the task",
|
|
27
|
+
)
|
|
28
|
+
priority: int = Field(
|
|
29
|
+
default=1,
|
|
30
|
+
description="Priority (1=high, 2=medium, 3=low)",
|
|
31
|
+
ge=1,
|
|
32
|
+
le=3,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DiscoveryResult(BaseModel):
|
|
37
|
+
"""Result of executing a discovery task.
|
|
38
|
+
|
|
39
|
+
Captures both successful results and error information.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
task_id: int = Field(description="ID of the task that was executed")
|
|
43
|
+
tool: str = Field(description="Name of the tool that was called")
|
|
44
|
+
success: bool = Field(description="Whether the tool call succeeded")
|
|
45
|
+
result: Any = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
description="Result from the tool (if successful)",
|
|
48
|
+
)
|
|
49
|
+
error: str | None = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
description="Error message (if failed)",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DiscoveryPlan(BaseModel):
|
|
56
|
+
"""Complete discovery plan with ordered tasks.
|
|
57
|
+
|
|
58
|
+
Generated by the plan_discovery LLM node.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
tasks: list[DiscoveryTask] = Field(
|
|
62
|
+
default_factory=list,
|
|
63
|
+
description="Ordered list of discovery tasks to execute",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DiscoveryFindings(BaseModel):
|
|
68
|
+
"""Accumulated discovery results.
|
|
69
|
+
|
|
70
|
+
Collected by the execute_discovery map node.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
results: list[DiscoveryResult] = Field(
|
|
74
|
+
default_factory=list,
|
|
75
|
+
description="Results from all executed discovery tasks",
|
|
76
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for codegen example."""
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Tests for AI helper tools (context compression, diff preview, similarity)."""
|
|
2
|
+
|
|
3
|
+
from examples.codegen.tools.ai_helpers import (
|
|
4
|
+
diff_preview,
|
|
5
|
+
find_similar_code,
|
|
6
|
+
summarize_module,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestSummarizeModule:
|
|
11
|
+
"""Tests for summarize_module function."""
|
|
12
|
+
|
|
13
|
+
def test_summarizes_real_module(self):
|
|
14
|
+
"""Summarizes a real module with classes and functions."""
|
|
15
|
+
result = summarize_module("yamlgraph/executor.py")
|
|
16
|
+
|
|
17
|
+
assert "error" not in result
|
|
18
|
+
assert "summary" in result
|
|
19
|
+
assert isinstance(result["summary"], str)
|
|
20
|
+
# Should be significantly shorter than original file
|
|
21
|
+
assert len(result["summary"]) < 2000
|
|
22
|
+
|
|
23
|
+
def test_includes_class_names(self):
|
|
24
|
+
"""Summary includes class names."""
|
|
25
|
+
result = summarize_module("yamlgraph/models/schemas.py")
|
|
26
|
+
|
|
27
|
+
assert "error" not in result
|
|
28
|
+
# Should mention key classes
|
|
29
|
+
assert "class" in result["summary"].lower() or "Class" in result["summary"]
|
|
30
|
+
|
|
31
|
+
def test_includes_function_signatures(self):
|
|
32
|
+
"""Summary includes function signatures (not bodies)."""
|
|
33
|
+
result = summarize_module("yamlgraph/executor.py")
|
|
34
|
+
|
|
35
|
+
assert "error" not in result
|
|
36
|
+
# Should have function defs
|
|
37
|
+
assert "def " in result["summary"]
|
|
38
|
+
# Bodies are stripped - should be much shorter than reading whole file
|
|
39
|
+
|
|
40
|
+
def test_includes_docstring(self):
|
|
41
|
+
"""Summary includes module docstring."""
|
|
42
|
+
result = summarize_module("yamlgraph/executor.py")
|
|
43
|
+
|
|
44
|
+
assert "error" not in result
|
|
45
|
+
# Module docstring should be preserved
|
|
46
|
+
assert len(result["summary"]) > 50 # Not empty
|
|
47
|
+
|
|
48
|
+
def test_returns_error_for_invalid_file(self):
|
|
49
|
+
"""Returns error for non-existent file."""
|
|
50
|
+
result = summarize_module("nonexistent/file.py")
|
|
51
|
+
|
|
52
|
+
assert "error" in result
|
|
53
|
+
|
|
54
|
+
def test_max_length_parameter(self):
|
|
55
|
+
"""Respects max_length parameter."""
|
|
56
|
+
result = summarize_module("yamlgraph/executor.py", max_length=500)
|
|
57
|
+
|
|
58
|
+
assert "error" not in result
|
|
59
|
+
assert len(result["summary"]) <= 550 # Some tolerance
|
|
60
|
+
|
|
61
|
+
def test_returns_line_count(self):
|
|
62
|
+
"""Returns original line count for context."""
|
|
63
|
+
result = summarize_module("yamlgraph/executor.py")
|
|
64
|
+
|
|
65
|
+
assert "error" not in result
|
|
66
|
+
assert "original_lines" in result
|
|
67
|
+
assert result["original_lines"] > 50
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestDiffPreview:
|
|
71
|
+
"""Tests for diff_preview function."""
|
|
72
|
+
|
|
73
|
+
def test_shows_add_diff(self):
|
|
74
|
+
"""Shows diff for adding a line."""
|
|
75
|
+
result = diff_preview(
|
|
76
|
+
file_path="yamlgraph/__init__.py",
|
|
77
|
+
line=1,
|
|
78
|
+
action="ADD",
|
|
79
|
+
new_code="# New comment",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert "error" not in result
|
|
83
|
+
assert "diff" in result
|
|
84
|
+
assert "+" in result["diff"] # Added line marker
|
|
85
|
+
|
|
86
|
+
def test_shows_modify_diff(self):
|
|
87
|
+
"""Shows diff for modifying a line."""
|
|
88
|
+
# Read first line to modify it
|
|
89
|
+
result = diff_preview(
|
|
90
|
+
file_path="yamlgraph/__init__.py",
|
|
91
|
+
line=1,
|
|
92
|
+
action="MODIFY",
|
|
93
|
+
new_code='"""Modified docstring."""',
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
assert "error" not in result
|
|
97
|
+
assert "diff" in result
|
|
98
|
+
# Should show both old (-) and new (+)
|
|
99
|
+
|
|
100
|
+
def test_shows_delete_diff(self):
|
|
101
|
+
"""Shows diff for deleting a line."""
|
|
102
|
+
result = diff_preview(
|
|
103
|
+
file_path="yamlgraph/__init__.py",
|
|
104
|
+
line=1,
|
|
105
|
+
action="DELETE",
|
|
106
|
+
new_code="",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assert "error" not in result
|
|
110
|
+
assert "diff" in result
|
|
111
|
+
assert "-" in result["diff"] # Deleted line marker
|
|
112
|
+
|
|
113
|
+
def test_returns_error_for_invalid_file(self):
|
|
114
|
+
"""Returns error for non-existent file."""
|
|
115
|
+
result = diff_preview(
|
|
116
|
+
file_path="nonexistent/file.py",
|
|
117
|
+
line=1,
|
|
118
|
+
action="ADD",
|
|
119
|
+
new_code="test",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
assert "error" in result
|
|
123
|
+
|
|
124
|
+
def test_returns_error_for_invalid_line(self):
|
|
125
|
+
"""Returns error for line beyond file length."""
|
|
126
|
+
result = diff_preview(
|
|
127
|
+
file_path="yamlgraph/__init__.py",
|
|
128
|
+
line=99999,
|
|
129
|
+
action="MODIFY",
|
|
130
|
+
new_code="test",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert "error" in result
|
|
134
|
+
|
|
135
|
+
def test_validates_syntax_of_result(self):
|
|
136
|
+
"""Optionally validates syntax of resulting code."""
|
|
137
|
+
result = diff_preview(
|
|
138
|
+
file_path="yamlgraph/__init__.py",
|
|
139
|
+
line=1,
|
|
140
|
+
action="MODIFY",
|
|
141
|
+
new_code="def broken(:", # Invalid syntax - colon after open paren
|
|
142
|
+
validate_syntax=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Should still return diff but flag syntax issue
|
|
146
|
+
assert "syntax_valid" in result
|
|
147
|
+
assert result["syntax_valid"] is False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestFindSimilarCode:
|
|
151
|
+
"""Tests for find_similar_code function."""
|
|
152
|
+
|
|
153
|
+
def test_finds_similar_functions(self):
|
|
154
|
+
"""Finds functions with similar structure."""
|
|
155
|
+
# Look for functions similar to git_blame (simple tool pattern)
|
|
156
|
+
result = find_similar_code(
|
|
157
|
+
file_path="examples/codegen/tools/git_tools.py",
|
|
158
|
+
symbol_name="git_blame",
|
|
159
|
+
project_path="examples/codegen/tools",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert "error" not in result
|
|
163
|
+
assert "similar" in result
|
|
164
|
+
assert isinstance(result["similar"], list)
|
|
165
|
+
|
|
166
|
+
def test_returns_file_and_line(self):
|
|
167
|
+
"""Each result includes file and line."""
|
|
168
|
+
result = find_similar_code(
|
|
169
|
+
file_path="examples/codegen/tools/git_tools.py",
|
|
170
|
+
symbol_name="git_blame",
|
|
171
|
+
project_path="examples/codegen/tools",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert "error" not in result
|
|
175
|
+
for item in result["similar"]:
|
|
176
|
+
assert "file" in item
|
|
177
|
+
assert "name" in item
|
|
178
|
+
assert "line" in item
|
|
179
|
+
|
|
180
|
+
def test_includes_similarity_reason(self):
|
|
181
|
+
"""Results explain why they're similar."""
|
|
182
|
+
result = find_similar_code(
|
|
183
|
+
file_path="examples/codegen/tools/git_tools.py",
|
|
184
|
+
symbol_name="git_blame",
|
|
185
|
+
project_path="examples/codegen/tools",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
assert "error" not in result
|
|
189
|
+
for item in result["similar"]:
|
|
190
|
+
assert "reason" in item
|
|
191
|
+
|
|
192
|
+
def test_returns_error_for_invalid_file(self):
|
|
193
|
+
"""Returns error for non-existent file."""
|
|
194
|
+
result = find_similar_code(
|
|
195
|
+
file_path="nonexistent/file.py",
|
|
196
|
+
symbol_name="foo",
|
|
197
|
+
project_path="yamlgraph",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
assert "error" in result
|
|
201
|
+
|
|
202
|
+
def test_returns_error_for_invalid_symbol(self):
|
|
203
|
+
"""Returns error for non-existent symbol."""
|
|
204
|
+
result = find_similar_code(
|
|
205
|
+
file_path="yamlgraph/executor.py",
|
|
206
|
+
symbol_name="nonexistent_function_xyz",
|
|
207
|
+
project_path="yamlgraph",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert "error" in result
|
|
211
|
+
|
|
212
|
+
def test_max_results_parameter(self):
|
|
213
|
+
"""Respects max_results parameter."""
|
|
214
|
+
result = find_similar_code(
|
|
215
|
+
file_path="examples/codegen/tools/git_tools.py",
|
|
216
|
+
symbol_name="git_blame",
|
|
217
|
+
project_path="examples/codegen/tools",
|
|
218
|
+
max_results=2,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
assert "error" not in result
|
|
222
|
+
assert len(result["similar"]) <= 2
|
|
223
|
+
|
|
224
|
+
def test_includes_code_snippet(self):
|
|
225
|
+
"""Results include code snippet."""
|
|
226
|
+
result = find_similar_code(
|
|
227
|
+
file_path="examples/codegen/tools/git_tools.py",
|
|
228
|
+
symbol_name="git_blame",
|
|
229
|
+
project_path="examples/codegen/tools",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
assert "error" not in result
|
|
233
|
+
for item in result["similar"]:
|
|
234
|
+
assert "snippet" in item
|
|
235
|
+
assert "def " in item["snippet"]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Tests for AST-based code analysis tools."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from examples.codegen.tools.ast_analysis import get_module_structure
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestGetModuleStructure:
|
|
10
|
+
"""Tests for get_module_structure function."""
|
|
11
|
+
|
|
12
|
+
def test_extracts_classes_with_line_numbers(self):
|
|
13
|
+
"""Classes include name, bases, methods, line numbers."""
|
|
14
|
+
# Use a known file in the project
|
|
15
|
+
result = get_module_structure("yamlgraph/models/schemas.py")
|
|
16
|
+
|
|
17
|
+
assert "error" not in result
|
|
18
|
+
assert "classes" in result
|
|
19
|
+
assert len(result["classes"]) > 0
|
|
20
|
+
|
|
21
|
+
# Each class should have required fields
|
|
22
|
+
for cls in result["classes"]:
|
|
23
|
+
assert "name" in cls
|
|
24
|
+
assert "line" in cls
|
|
25
|
+
assert "end_line" in cls
|
|
26
|
+
assert isinstance(cls["line"], int)
|
|
27
|
+
assert isinstance(cls["end_line"], int)
|
|
28
|
+
assert cls["end_line"] >= cls["line"]
|
|
29
|
+
|
|
30
|
+
def test_extracts_functions_with_signature(self):
|
|
31
|
+
"""Functions include name, args, returns, decorators."""
|
|
32
|
+
result = get_module_structure("yamlgraph/executor.py")
|
|
33
|
+
|
|
34
|
+
assert "error" not in result
|
|
35
|
+
assert "functions" in result
|
|
36
|
+
assert len(result["functions"]) > 0
|
|
37
|
+
|
|
38
|
+
# Each function should have required fields
|
|
39
|
+
for func in result["functions"]:
|
|
40
|
+
assert "name" in func
|
|
41
|
+
assert "args" in func
|
|
42
|
+
assert "line" in func
|
|
43
|
+
assert "end_line" in func
|
|
44
|
+
assert isinstance(func["args"], list)
|
|
45
|
+
|
|
46
|
+
def test_extracts_imports(self):
|
|
47
|
+
"""Import statements are extracted."""
|
|
48
|
+
result = get_module_structure("yamlgraph/executor.py")
|
|
49
|
+
|
|
50
|
+
assert "error" not in result
|
|
51
|
+
assert "imports" in result
|
|
52
|
+
assert len(result["imports"]) > 0
|
|
53
|
+
|
|
54
|
+
def test_extracts_module_docstring(self):
|
|
55
|
+
"""Module-level docstring is extracted."""
|
|
56
|
+
result = get_module_structure("yamlgraph/executor.py")
|
|
57
|
+
|
|
58
|
+
assert "error" not in result
|
|
59
|
+
assert "docstring" in result
|
|
60
|
+
# executor.py should have a docstring
|
|
61
|
+
assert result["docstring"] is not None
|
|
62
|
+
|
|
63
|
+
def test_handles_missing_file(self):
|
|
64
|
+
"""Returns error for non-existent file."""
|
|
65
|
+
result = get_module_structure("nonexistent_file_12345.py")
|
|
66
|
+
|
|
67
|
+
assert "error" in result
|
|
68
|
+
assert "not found" in result["error"].lower()
|
|
69
|
+
|
|
70
|
+
def test_handles_syntax_error(self):
|
|
71
|
+
"""Returns error for file with syntax errors."""
|
|
72
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
73
|
+
f.write("def broken(\n") # Invalid syntax
|
|
74
|
+
temp_path = f.name
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
result = get_module_structure(temp_path)
|
|
78
|
+
assert "error" in result
|
|
79
|
+
assert "syntax" in result["error"].lower()
|
|
80
|
+
finally:
|
|
81
|
+
Path(temp_path).unlink()
|
|
82
|
+
|
|
83
|
+
def test_includes_file_path_in_result(self):
|
|
84
|
+
"""Result includes the file path."""
|
|
85
|
+
result = get_module_structure("yamlgraph/executor.py")
|
|
86
|
+
|
|
87
|
+
assert "error" not in result
|
|
88
|
+
assert "file" in result
|
|
89
|
+
assert "executor.py" in result["file"]
|
|
90
|
+
|
|
91
|
+
def test_extracts_class_methods(self):
|
|
92
|
+
"""Class methods are listed."""
|
|
93
|
+
result = get_module_structure("yamlgraph/models/schemas.py")
|
|
94
|
+
|
|
95
|
+
assert "error" not in result
|
|
96
|
+
# At least some classes should have methods
|
|
97
|
+
# Note: Pydantic models may not have explicit methods
|
|
98
|
+
assert "methods" in result["classes"][0]
|
|
99
|
+
|
|
100
|
+
def test_extracts_class_bases(self):
|
|
101
|
+
"""Class base classes are extracted."""
|
|
102
|
+
result = get_module_structure("yamlgraph/models/schemas.py")
|
|
103
|
+
|
|
104
|
+
assert "error" not in result
|
|
105
|
+
# Pydantic models inherit from BaseModel
|
|
106
|
+
pydantic_classes = [
|
|
107
|
+
c for c in result["classes"] if "BaseModel" in c.get("bases", [])
|
|
108
|
+
]
|
|
109
|
+
assert len(pydantic_classes) > 0
|
|
110
|
+
|
|
111
|
+
def test_extracts_function_decorators(self):
|
|
112
|
+
"""Function decorators are extracted."""
|
|
113
|
+
# Create a temp file with decorators
|
|
114
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
115
|
+
f.write("""
|
|
116
|
+
def simple_decorator(func):
|
|
117
|
+
return func
|
|
118
|
+
|
|
119
|
+
@simple_decorator
|
|
120
|
+
def decorated_function():
|
|
121
|
+
pass
|
|
122
|
+
""")
|
|
123
|
+
temp_path = f.name
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
result = get_module_structure(temp_path)
|
|
127
|
+
assert "error" not in result
|
|
128
|
+
decorated = [
|
|
129
|
+
f for f in result["functions"] if f["name"] == "decorated_function"
|
|
130
|
+
]
|
|
131
|
+
assert len(decorated) == 1
|
|
132
|
+
assert len(decorated[0]["decorators"]) > 0
|
|
133
|
+
finally:
|
|
134
|
+
Path(temp_path).unlink()
|
|
135
|
+
|
|
136
|
+
def test_extracts_function_return_type(self):
|
|
137
|
+
"""Function return type annotations are extracted."""
|
|
138
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
139
|
+
f.write("""
|
|
140
|
+
def typed_function(x: int, y: str) -> bool:
|
|
141
|
+
return True
|
|
142
|
+
""")
|
|
143
|
+
temp_path = f.name
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
result = get_module_structure(temp_path)
|
|
147
|
+
assert "error" not in result
|
|
148
|
+
typed = [f for f in result["functions"] if f["name"] == "typed_function"]
|
|
149
|
+
assert len(typed) == 1
|
|
150
|
+
assert typed[0]["returns"] == "bool"
|
|
151
|
+
assert typed[0]["args"] == ["x", "y"]
|
|
152
|
+
finally:
|
|
153
|
+
Path(temp_path).unlink()
|
|
154
|
+
|
|
155
|
+
def test_extracts_function_docstring(self):
|
|
156
|
+
"""Function docstrings are extracted."""
|
|
157
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
158
|
+
f.write('''
|
|
159
|
+
def documented_function():
|
|
160
|
+
"""This is the docstring."""
|
|
161
|
+
pass
|
|
162
|
+
''')
|
|
163
|
+
temp_path = f.name
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
result = get_module_structure(temp_path)
|
|
167
|
+
assert "error" not in result
|
|
168
|
+
documented = [
|
|
169
|
+
f for f in result["functions"] if f["name"] == "documented_function"
|
|
170
|
+
]
|
|
171
|
+
assert len(documented) == 1
|
|
172
|
+
assert documented[0]["docstring"] == "This is the docstring."
|
|
173
|
+
finally:
|
|
174
|
+
Path(temp_path).unlink()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Tests for code-analysis graph."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestCodeAnalysisGraphStructure:
|
|
7
|
+
"""Tests for code-analysis.yaml graph structure."""
|
|
8
|
+
|
|
9
|
+
def test_graph_file_exists(self):
|
|
10
|
+
"""Graph file should exist."""
|
|
11
|
+
graph_path = Path("graphs/code-analysis.yaml")
|
|
12
|
+
assert graph_path.exists(), "graphs/code-analysis.yaml not found"
|
|
13
|
+
|
|
14
|
+
def test_graph_loads_successfully(self):
|
|
15
|
+
"""Graph should load without errors."""
|
|
16
|
+
from yamlgraph.graph_loader import load_and_compile
|
|
17
|
+
|
|
18
|
+
graph = load_and_compile("graphs/code-analysis.yaml")
|
|
19
|
+
assert graph is not None
|
|
20
|
+
|
|
21
|
+
def test_graph_has_required_nodes(self):
|
|
22
|
+
"""Graph should have run_analysis and generate_recommendations nodes."""
|
|
23
|
+
from yamlgraph.graph_loader import load_graph_config
|
|
24
|
+
|
|
25
|
+
config = load_graph_config("graphs/code-analysis.yaml")
|
|
26
|
+
assert "run_analysis" in config.nodes
|
|
27
|
+
assert "generate_recommendations" in config.nodes
|
|
28
|
+
|
|
29
|
+
def test_graph_has_analysis_tools(self):
|
|
30
|
+
"""Graph should define analysis tools."""
|
|
31
|
+
from yamlgraph.graph_loader import load_graph_config
|
|
32
|
+
|
|
33
|
+
config = load_graph_config("graphs/code-analysis.yaml")
|
|
34
|
+
tools = config.tools
|
|
35
|
+
|
|
36
|
+
# Should have at least these tools
|
|
37
|
+
expected_tools = ["run_ruff", "run_tests", "run_bandit"]
|
|
38
|
+
for tool in expected_tools:
|
|
39
|
+
assert tool in tools, f"Missing tool: {tool}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TestCodeAnalysisPrompts:
|
|
43
|
+
"""Tests for code-analysis prompts."""
|
|
44
|
+
|
|
45
|
+
def test_analyzer_prompt_exists(self):
|
|
46
|
+
"""Analyzer prompt should exist."""
|
|
47
|
+
prompt_path = Path("prompts/code-analysis/analyzer.yaml")
|
|
48
|
+
assert prompt_path.exists(), "prompts/code-analysis/analyzer.yaml not found"
|
|
49
|
+
|
|
50
|
+
def test_recommend_prompt_exists(self):
|
|
51
|
+
"""Recommend prompt should exist."""
|
|
52
|
+
prompt_path = Path("prompts/code-analysis/recommend.yaml")
|
|
53
|
+
assert prompt_path.exists(), "prompts/code-analysis/recommend.yaml not found"
|
|
54
|
+
|
|
55
|
+
def test_analyzer_prompt_has_system_and_user(self):
|
|
56
|
+
"""Analyzer prompt should have system and user sections."""
|
|
57
|
+
import yaml
|
|
58
|
+
|
|
59
|
+
with open("prompts/code-analysis/analyzer.yaml") as f:
|
|
60
|
+
prompt = yaml.safe_load(f)
|
|
61
|
+
|
|
62
|
+
assert "system" in prompt, "Missing system prompt"
|
|
63
|
+
assert "user" in prompt, "Missing user prompt"
|
|
64
|
+
|
|
65
|
+
def test_recommend_prompt_has_system_and_user(self):
|
|
66
|
+
"""Recommend prompt should have system and user sections."""
|
|
67
|
+
import yaml
|
|
68
|
+
|
|
69
|
+
with open("prompts/code-analysis/recommend.yaml") as f:
|
|
70
|
+
prompt = yaml.safe_load(f)
|
|
71
|
+
|
|
72
|
+
assert "system" in prompt, "Missing system prompt"
|
|
73
|
+
assert "user" in prompt, "Missing user prompt"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestCodeAnalysisTools:
|
|
77
|
+
"""Tests for shell tool commands."""
|
|
78
|
+
|
|
79
|
+
def test_ruff_tool_command_valid(self):
|
|
80
|
+
"""Ruff tool should have valid command structure."""
|
|
81
|
+
from yamlgraph.graph_loader import load_graph_config
|
|
82
|
+
|
|
83
|
+
config = load_graph_config("graphs/code-analysis.yaml")
|
|
84
|
+
ruff_tool = config.tools.get("run_ruff", {})
|
|
85
|
+
|
|
86
|
+
assert "command" in ruff_tool
|
|
87
|
+
assert "ruff" in ruff_tool["command"]
|
|
88
|
+
|
|
89
|
+
def test_tests_tool_command_valid(self):
|
|
90
|
+
"""Tests tool should have valid command structure."""
|
|
91
|
+
from yamlgraph.graph_loader import load_graph_config
|
|
92
|
+
|
|
93
|
+
config = load_graph_config("graphs/code-analysis.yaml")
|
|
94
|
+
tests_tool = config.tools.get("run_tests", {})
|
|
95
|
+
|
|
96
|
+
assert "command" in tests_tool
|
|
97
|
+
assert "pytest" in tests_tool["command"]
|
|
98
|
+
|
|
99
|
+
def test_bandit_tool_command_valid(self):
|
|
100
|
+
"""Bandit tool should have valid command structure."""
|
|
101
|
+
from yamlgraph.graph_loader import load_graph_config
|
|
102
|
+
|
|
103
|
+
config = load_graph_config("graphs/code-analysis.yaml")
|
|
104
|
+
bandit_tool = config.tools.get("run_bandit", {})
|
|
105
|
+
|
|
106
|
+
assert "command" in bandit_tool
|
|
107
|
+
assert "bandit" in bandit_tool["command"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestCodeAnalysisCompilation:
|
|
111
|
+
"""Tests for graph compilation."""
|
|
112
|
+
|
|
113
|
+
def test_graph_compiles_with_checkpointer(self):
|
|
114
|
+
"""Graph should compile with SQLite checkpointer."""
|
|
115
|
+
from langgraph.checkpoint.sqlite import SqliteSaver
|
|
116
|
+
|
|
117
|
+
from yamlgraph.graph_loader import load_and_compile
|
|
118
|
+
|
|
119
|
+
graph = load_and_compile("graphs/code-analysis.yaml")
|
|
120
|
+
|
|
121
|
+
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
|
122
|
+
compiled = graph.compile(checkpointer=checkpointer)
|
|
123
|
+
assert compiled is not None
|
|
124
|
+
|
|
125
|
+
def test_graph_has_entry_point(self):
|
|
126
|
+
"""Graph should have START -> run_analysis edge."""
|
|
127
|
+
from yamlgraph.graph_loader import load_graph_config
|
|
128
|
+
|
|
129
|
+
config = load_graph_config("graphs/code-analysis.yaml")
|
|
130
|
+
|
|
131
|
+
# Find edge from START
|
|
132
|
+
start_edges = [e for e in config.edges if e.get("from") == "START"]
|
|
133
|
+
assert len(start_edges) > 0, "No edge from START"
|
|
134
|
+
assert start_edges[0]["to"] == "run_analysis"
|