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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Code navigation tools for finding relevant files.
|
|
2
|
+
|
|
3
|
+
Provides package-level module discovery and search capabilities.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from examples.codegen.tools.ast_analysis import get_module_structure
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_package_modules(package_path: str) -> list[dict]:
|
|
17
|
+
"""List all Python modules in a package with high-level summaries.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
package_path: Path to package directory
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of module summaries with file, docstring, classes, functions.
|
|
24
|
+
Returns empty list if path doesn't exist or has no Python files.
|
|
25
|
+
"""
|
|
26
|
+
path = Path(package_path)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
results = []
|
|
31
|
+
for py_file in sorted(path.rglob("*.py")):
|
|
32
|
+
# Skip __pycache__ directories
|
|
33
|
+
if "__pycache__" in str(py_file):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
structure = get_module_structure(str(py_file))
|
|
37
|
+
|
|
38
|
+
# Skip files with errors (syntax errors, etc.)
|
|
39
|
+
if "error" in structure:
|
|
40
|
+
logger.warning(f"Skipping {py_file}: {structure['error']}")
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
results.append(
|
|
44
|
+
{
|
|
45
|
+
"file": str(py_file),
|
|
46
|
+
"docstring": structure.get("docstring"),
|
|
47
|
+
"classes": [c["name"] for c in structure.get("classes", [])],
|
|
48
|
+
"functions": [f["name"] for f in structure.get("functions", [])],
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return results
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Dependency analysis tools for implementation agent.
|
|
2
|
+
|
|
3
|
+
Provides import analysis and reverse dependency tracking.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_imports(file_path: str) -> dict:
|
|
12
|
+
"""Extract all imports from a Python file.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
file_path: Path to the Python file
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
dict with 'imports' list, each containing:
|
|
19
|
+
- module: The imported module name
|
|
20
|
+
- names: List of imported names (None for 'import X')
|
|
21
|
+
- alias: Alias if 'as' was used (optional)
|
|
22
|
+
or dict with error key if failed
|
|
23
|
+
"""
|
|
24
|
+
path = Path(file_path)
|
|
25
|
+
if not path.exists():
|
|
26
|
+
return {"error": f"File not found: {file_path}"}
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
source = path.read_text()
|
|
30
|
+
tree = ast.parse(source)
|
|
31
|
+
except SyntaxError as e:
|
|
32
|
+
return {"error": f"Syntax error in {file_path}: {e}"}
|
|
33
|
+
|
|
34
|
+
imports = []
|
|
35
|
+
|
|
36
|
+
for node in ast.walk(tree):
|
|
37
|
+
if isinstance(node, ast.Import):
|
|
38
|
+
# import X, Y, Z
|
|
39
|
+
for alias in node.names:
|
|
40
|
+
imports.append(
|
|
41
|
+
{
|
|
42
|
+
"module": alias.name,
|
|
43
|
+
"names": None,
|
|
44
|
+
"alias": alias.asname,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
elif isinstance(node, ast.ImportFrom):
|
|
48
|
+
# from X import Y, Z
|
|
49
|
+
module = node.module or ""
|
|
50
|
+
names = []
|
|
51
|
+
for alias in node.names:
|
|
52
|
+
name_info = {"name": alias.name}
|
|
53
|
+
if alias.asname:
|
|
54
|
+
name_info["alias"] = alias.asname
|
|
55
|
+
names.append(name_info)
|
|
56
|
+
imports.append(
|
|
57
|
+
{
|
|
58
|
+
"module": module,
|
|
59
|
+
"names": names,
|
|
60
|
+
"level": node.level, # Relative import level (0 = absolute)
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return {"imports": imports}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_dependents(module_path: str, project_path: str) -> dict:
|
|
68
|
+
"""Find all files that import a given module.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
module_path: Module path to search for (e.g., 'yamlgraph.executor')
|
|
72
|
+
project_path: Root path of the project to search in
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
dict with 'dependents' list of file paths that import the module
|
|
76
|
+
or dict with error key if failed
|
|
77
|
+
"""
|
|
78
|
+
path = Path(project_path)
|
|
79
|
+
if not path.exists():
|
|
80
|
+
return {"error": f"Project path not found: {project_path}"}
|
|
81
|
+
|
|
82
|
+
# Convert module path to patterns for grep
|
|
83
|
+
# e.g., 'yamlgraph.executor' -> search for 'from yamlgraph.executor' or 'import yamlgraph.executor'
|
|
84
|
+
full_module = module_path
|
|
85
|
+
|
|
86
|
+
# Build grep patterns
|
|
87
|
+
patterns = [
|
|
88
|
+
f"from {full_module} import",
|
|
89
|
+
f"from {full_module}\\b",
|
|
90
|
+
f"import {full_module}\\b",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# For single-word modules like 'logging', also search simpler patterns
|
|
94
|
+
if "." not in module_path:
|
|
95
|
+
patterns = [
|
|
96
|
+
f"import {module_path}\\b",
|
|
97
|
+
f"from {module_path} import",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
dependents = set()
|
|
101
|
+
|
|
102
|
+
for pattern in patterns:
|
|
103
|
+
try:
|
|
104
|
+
result = subprocess.run(
|
|
105
|
+
["grep", "-rl", "-E", pattern, "--include=*.py", str(path)],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=30,
|
|
109
|
+
)
|
|
110
|
+
# grep returns 1 if no matches (not an error)
|
|
111
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
112
|
+
for file_path in result.stdout.strip().split("\n"):
|
|
113
|
+
if file_path.strip():
|
|
114
|
+
dependents.add(file_path.strip())
|
|
115
|
+
except subprocess.TimeoutExpired:
|
|
116
|
+
return {"error": "Search timed out"}
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return {"error": str(e)}
|
|
119
|
+
|
|
120
|
+
return {"dependents": sorted(dependents)}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Example discovery tools for implementation agent.
|
|
2
|
+
|
|
3
|
+
Find usage examples and error handling patterns across the codebase.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def find_example(
|
|
13
|
+
symbol_name: str,
|
|
14
|
+
project_path: str,
|
|
15
|
+
max_examples: int = 3,
|
|
16
|
+
) -> dict:
|
|
17
|
+
"""Find real usage examples of a function/class.
|
|
18
|
+
|
|
19
|
+
Prioritizes:
|
|
20
|
+
1. Test files (most complete examples)
|
|
21
|
+
2. Main modules (production usage)
|
|
22
|
+
3. Examples folder
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
symbol_name: Name of the symbol to find examples for
|
|
26
|
+
project_path: Root path to search
|
|
27
|
+
max_examples: Maximum examples to return
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
dict with 'examples' list of {file, line, snippet}
|
|
31
|
+
or dict with 'error' key if failed
|
|
32
|
+
"""
|
|
33
|
+
project = Path(project_path)
|
|
34
|
+
if not project.exists():
|
|
35
|
+
return {"error": f"Project path not found: {project_path}"}
|
|
36
|
+
|
|
37
|
+
examples = []
|
|
38
|
+
test_examples = []
|
|
39
|
+
other_examples = []
|
|
40
|
+
|
|
41
|
+
# Search all Python files
|
|
42
|
+
for py_file in project.rglob("*.py"):
|
|
43
|
+
try:
|
|
44
|
+
source = py_file.read_text()
|
|
45
|
+
except (OSError, UnicodeDecodeError):
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Skip if symbol is defined here (we want usages, not definitions)
|
|
49
|
+
if _is_definition(source, symbol_name):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
# Find all occurrences
|
|
53
|
+
lines = source.splitlines()
|
|
54
|
+
for i, line in enumerate(lines, 1):
|
|
55
|
+
if symbol_name in line and _is_usage(line, symbol_name):
|
|
56
|
+
# Extract context (3 lines before and after)
|
|
57
|
+
start = max(0, i - 3)
|
|
58
|
+
end = min(len(lines), i + 3)
|
|
59
|
+
snippet = "\n".join(lines[start:end])
|
|
60
|
+
|
|
61
|
+
example = {
|
|
62
|
+
"file": str(py_file),
|
|
63
|
+
"line": i,
|
|
64
|
+
"snippet": snippet,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Prioritize test files
|
|
68
|
+
if "test" in py_file.name.lower():
|
|
69
|
+
test_examples.append(example)
|
|
70
|
+
else:
|
|
71
|
+
other_examples.append(example)
|
|
72
|
+
|
|
73
|
+
# Combine: tests first, then others
|
|
74
|
+
examples = test_examples + other_examples
|
|
75
|
+
|
|
76
|
+
return {"examples": examples[:max_examples]}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_definition(source: str, symbol_name: str) -> bool:
|
|
80
|
+
"""Check if this file defines the symbol."""
|
|
81
|
+
try:
|
|
82
|
+
tree = ast.parse(source)
|
|
83
|
+
except SyntaxError:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
for node in ast.walk(tree):
|
|
87
|
+
if (
|
|
88
|
+
isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
|
|
89
|
+
and node.name == symbol_name
|
|
90
|
+
):
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_usage(line: str, symbol_name: str) -> bool:
|
|
97
|
+
"""Check if line contains actual usage (not import or def)."""
|
|
98
|
+
stripped = line.strip()
|
|
99
|
+
|
|
100
|
+
# Skip definitions
|
|
101
|
+
if stripped.startswith("def ") and f"def {symbol_name}" in stripped:
|
|
102
|
+
return False
|
|
103
|
+
if stripped.startswith("class ") and f"class {symbol_name}" in stripped:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# Skip import-only lines
|
|
107
|
+
if stripped.startswith("from ") and stripped.endswith(f"import {symbol_name}"):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# Must be a call or reference
|
|
111
|
+
return f"{symbol_name}(" in line or f"{symbol_name}." in line
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_error_handling(project_path: str) -> dict:
|
|
115
|
+
"""Analyze error handling patterns in a project.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
project_path: Root path to analyze
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
dict with:
|
|
122
|
+
- exceptions: List of exception types used
|
|
123
|
+
- patterns: List of pattern names found
|
|
124
|
+
- logging: Dict with logging info
|
|
125
|
+
or dict with 'error' key if failed
|
|
126
|
+
"""
|
|
127
|
+
project = Path(project_path)
|
|
128
|
+
if not project.exists():
|
|
129
|
+
return {"error": f"Project path not found: {project_path}"}
|
|
130
|
+
|
|
131
|
+
exceptions: set[str] = set()
|
|
132
|
+
patterns: list[str] = []
|
|
133
|
+
uses_logging = False
|
|
134
|
+
uses_dict_error = False
|
|
135
|
+
|
|
136
|
+
for py_file in project.rglob("*.py"):
|
|
137
|
+
try:
|
|
138
|
+
source = py_file.read_text()
|
|
139
|
+
except (OSError, UnicodeDecodeError):
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
tree = ast.parse(source)
|
|
144
|
+
except SyntaxError:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Find exception types
|
|
148
|
+
for node in ast.walk(tree):
|
|
149
|
+
if isinstance(node, ast.ExceptHandler) and node.type:
|
|
150
|
+
exc_types = _extract_exception_types(node.type)
|
|
151
|
+
exceptions.update(exc_types)
|
|
152
|
+
|
|
153
|
+
# Check for dict error pattern
|
|
154
|
+
if 'return {"error"' in source or "return {'error'" in source:
|
|
155
|
+
uses_dict_error = True
|
|
156
|
+
|
|
157
|
+
# Check for logging
|
|
158
|
+
if "logger.error" in source or "logging.error" in source:
|
|
159
|
+
uses_logging = True
|
|
160
|
+
|
|
161
|
+
# Build patterns list
|
|
162
|
+
if uses_dict_error:
|
|
163
|
+
patterns.append("dict_error")
|
|
164
|
+
if exceptions:
|
|
165
|
+
patterns.append("try_except")
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"exceptions": sorted(exceptions),
|
|
169
|
+
"patterns": patterns,
|
|
170
|
+
"logging": {"uses_logging": uses_logging},
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _extract_exception_types(node: ast.expr) -> list[str]:
|
|
175
|
+
"""Extract exception type names from except clause."""
|
|
176
|
+
types = []
|
|
177
|
+
|
|
178
|
+
if isinstance(node, ast.Name):
|
|
179
|
+
types.append(node.id)
|
|
180
|
+
elif isinstance(node, ast.Tuple):
|
|
181
|
+
for elt in node.elts:
|
|
182
|
+
if isinstance(elt, ast.Name):
|
|
183
|
+
types.append(elt.id)
|
|
184
|
+
elif isinstance(node, ast.Attribute):
|
|
185
|
+
# e.g., requests.exceptions.Timeout
|
|
186
|
+
types.append(node.attr)
|
|
187
|
+
|
|
188
|
+
return types
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Git analysis tools for implementation agent.
|
|
2
|
+
|
|
3
|
+
Provides git blame and git log information for code context.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def git_blame(file_path: str, line: int) -> dict:
|
|
11
|
+
"""Get blame info for a specific line.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
file_path: Path to the file (relative or absolute)
|
|
15
|
+
line: Line number (1-indexed)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
dict with author, date, commit, summary, line_content
|
|
19
|
+
or dict with error key if failed
|
|
20
|
+
"""
|
|
21
|
+
path = Path(file_path)
|
|
22
|
+
if not path.exists():
|
|
23
|
+
return {"error": f"File not found: {file_path}"}
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# Use porcelain format for machine-readable output
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["git", "blame", "-L", f"{line},{line}", "--porcelain", str(path)],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=10,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if result.returncode != 0:
|
|
35
|
+
return {
|
|
36
|
+
"error": result.stderr.strip()
|
|
37
|
+
or f"git blame failed for {file_path}:{line}"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Parse porcelain output
|
|
41
|
+
lines = result.stdout.strip().split("\n")
|
|
42
|
+
if not lines or len(lines) < 2:
|
|
43
|
+
return {"error": f"Invalid line number: {line}"}
|
|
44
|
+
|
|
45
|
+
# First line is commit hash and original line number
|
|
46
|
+
first_line = lines[0].split()
|
|
47
|
+
if not first_line:
|
|
48
|
+
return {"error": "Failed to parse git blame output"}
|
|
49
|
+
|
|
50
|
+
commit = first_line[0]
|
|
51
|
+
|
|
52
|
+
# Parse the rest of the porcelain output
|
|
53
|
+
author = ""
|
|
54
|
+
date = ""
|
|
55
|
+
summary = ""
|
|
56
|
+
line_content = ""
|
|
57
|
+
|
|
58
|
+
for output_line in lines[1:]:
|
|
59
|
+
if output_line.startswith("author "):
|
|
60
|
+
author = output_line[7:]
|
|
61
|
+
elif output_line.startswith("author-time "):
|
|
62
|
+
# Convert Unix timestamp to readable format
|
|
63
|
+
import datetime
|
|
64
|
+
|
|
65
|
+
timestamp = int(output_line[12:])
|
|
66
|
+
date = datetime.datetime.fromtimestamp(timestamp).strftime(
|
|
67
|
+
"%Y-%m-%d %H:%M"
|
|
68
|
+
)
|
|
69
|
+
elif output_line.startswith("summary "):
|
|
70
|
+
summary = output_line[8:]
|
|
71
|
+
elif output_line.startswith("\t"):
|
|
72
|
+
# Actual line content starts with tab
|
|
73
|
+
line_content = output_line[1:]
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"commit": commit[:8], # Short hash
|
|
77
|
+
"author": author,
|
|
78
|
+
"date": date,
|
|
79
|
+
"summary": summary,
|
|
80
|
+
"line_content": line_content,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
except subprocess.TimeoutExpired:
|
|
84
|
+
return {"error": "git blame timed out"}
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return {"error": str(e)}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def git_log(file_path: str, n: int = 5) -> dict:
|
|
90
|
+
"""Get recent commits for a file.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
file_path: Path to the file (relative or absolute)
|
|
94
|
+
n: Maximum number of commits to return (default 5)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
dict with commits list, each containing hash, author, date, message
|
|
98
|
+
or dict with error key if failed
|
|
99
|
+
"""
|
|
100
|
+
path = Path(file_path)
|
|
101
|
+
if not path.exists():
|
|
102
|
+
return {"error": f"File not found: {file_path}"}
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Use format for clean parsing
|
|
106
|
+
result = subprocess.run(
|
|
107
|
+
[
|
|
108
|
+
"git",
|
|
109
|
+
"log",
|
|
110
|
+
f"-{n}",
|
|
111
|
+
"--format=%H|%an|%ai|%s",
|
|
112
|
+
"--follow",
|
|
113
|
+
"--",
|
|
114
|
+
str(path),
|
|
115
|
+
],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
timeout=10,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if result.returncode != 0:
|
|
122
|
+
error = result.stderr.strip()
|
|
123
|
+
if "does not have any commits" in error or not result.stdout.strip():
|
|
124
|
+
return {"error": f"No git history for {file_path}"}
|
|
125
|
+
return {"error": error or f"git log failed for {file_path}"}
|
|
126
|
+
|
|
127
|
+
output = result.stdout.strip()
|
|
128
|
+
if not output:
|
|
129
|
+
return {"error": f"No git history for {file_path}"}
|
|
130
|
+
|
|
131
|
+
commits = []
|
|
132
|
+
for line in output.split("\n"):
|
|
133
|
+
if not line.strip():
|
|
134
|
+
continue
|
|
135
|
+
parts = line.split("|", 3)
|
|
136
|
+
if len(parts) >= 4:
|
|
137
|
+
commits.append(
|
|
138
|
+
{
|
|
139
|
+
"hash": parts[0][:8], # Short hash
|
|
140
|
+
"author": parts[1],
|
|
141
|
+
"date": parts[2][:10], # Just the date part
|
|
142
|
+
"message": parts[3],
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return {"commits": commits}
|
|
147
|
+
|
|
148
|
+
except subprocess.TimeoutExpired:
|
|
149
|
+
return {"error": "git log timed out"}
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return {"error": str(e)}
|