agenticstackfile 0.1.0__tar.gz
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.
- agenticstackfile-0.1.0/PKG-INFO +15 -0
- agenticstackfile-0.1.0/README.md +0 -0
- agenticstackfile-0.1.0/agenticstack/__init__.py +0 -0
- agenticstackfile-0.1.0/agenticstack/analyzer/__init__.py +0 -0
- agenticstackfile-0.1.0/agenticstack/analyzer/base.py +12 -0
- agenticstackfile-0.1.0/agenticstack/analyzer/python.py +136 -0
- agenticstackfile-0.1.0/agenticstack/cli.py +45 -0
- agenticstackfile-0.1.0/agenticstack/config.py +98 -0
- agenticstackfile-0.1.0/agenticstack/core.py +54 -0
- agenticstackfile-0.1.0/agenticstack/errors.py +0 -0
- agenticstackfile-0.1.0/agenticstack/formatter/__init__.py +0 -0
- agenticstackfile-0.1.0/agenticstack/formatter/static.py +68 -0
- agenticstackfile-0.1.0/agenticstack/prompt.py +73 -0
- agenticstackfile-0.1.0/agenticstack/providers/__init__.py +0 -0
- agenticstackfile-0.1.0/agenticstack/providers/anthropic.py +31 -0
- agenticstackfile-0.1.0/agenticstack/providers/base.py +12 -0
- agenticstackfile-0.1.0/agenticstack/providers/factory.py +21 -0
- agenticstackfile-0.1.0/agenticstack/providers/openai.py +31 -0
- agenticstackfile-0.1.0/agenticstack/writer.py +5 -0
- agenticstackfile-0.1.0/agenticstackfile.egg-info/PKG-INFO +15 -0
- agenticstackfile-0.1.0/agenticstackfile.egg-info/SOURCES.txt +25 -0
- agenticstackfile-0.1.0/agenticstackfile.egg-info/dependency_links.txt +1 -0
- agenticstackfile-0.1.0/agenticstackfile.egg-info/entry_points.txt +2 -0
- agenticstackfile-0.1.0/agenticstackfile.egg-info/requires.txt +10 -0
- agenticstackfile-0.1.0/agenticstackfile.egg-info/top_level.txt +1 -0
- agenticstackfile-0.1.0/pyproject.toml +26 -0
- agenticstackfile-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agenticstackfile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Instant codebase map for AI agents — understand any project before making changes
|
|
5
|
+
Author-email: Rajat Handa <handarajat111@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: anthropic
|
|
10
|
+
Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
|
|
11
|
+
Provides-Extra: openai
|
|
12
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
13
|
+
Provides-Extra: all
|
|
14
|
+
Requires-Dist: anthropic>=0.20.0; extra == "all"
|
|
15
|
+
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .base import BaseAnalyzer
|
|
5
|
+
import fnmatch
|
|
6
|
+
|
|
7
|
+
FILE_PATTERNS = {
|
|
8
|
+
"models": ["model", "models"],
|
|
9
|
+
"views": ["view", "views"],
|
|
10
|
+
"serializers": ["serializer", "serializers"],
|
|
11
|
+
"services": ["service", "services"],
|
|
12
|
+
"tests": ["test", "tests"],
|
|
13
|
+
"admin": ["admin"],
|
|
14
|
+
"urls": ["url", "urls"],
|
|
15
|
+
"filters": ["filter", "filters"],
|
|
16
|
+
"tasks": ["task", "tasks"],
|
|
17
|
+
"utils": ["util", "utils", "helper", "helpers"],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PythonAnalyzer(BaseAnalyzer):
|
|
22
|
+
|
|
23
|
+
def __init__(self, root_path: Path):
|
|
24
|
+
super().__init__(root_path)
|
|
25
|
+
self.results = {
|
|
26
|
+
"classes": [],
|
|
27
|
+
"functions": [],
|
|
28
|
+
"imports": [],
|
|
29
|
+
"file_map": {},
|
|
30
|
+
"errors": [],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def _load_ignore_patterns(self) -> list:
|
|
34
|
+
ignore_file = self.root_path / ".agenticstackignore"
|
|
35
|
+
patterns = []
|
|
36
|
+
|
|
37
|
+
if not ignore_file.exists():
|
|
38
|
+
return patterns
|
|
39
|
+
|
|
40
|
+
for line in ignore_file.read_text(encoding="utf-8").splitlines():
|
|
41
|
+
line = line.strip()
|
|
42
|
+
if line and not line.startswith("#"):
|
|
43
|
+
patterns.append(line)
|
|
44
|
+
|
|
45
|
+
return patterns
|
|
46
|
+
|
|
47
|
+
def _is_ignored(self, path: Path, patterns: list) -> bool:
|
|
48
|
+
relative = str(path.relative_to(self.root_path))
|
|
49
|
+
|
|
50
|
+
for pattern in patterns:
|
|
51
|
+
if fnmatch.fnmatch(relative, pattern):
|
|
52
|
+
return True
|
|
53
|
+
if fnmatch.fnmatch(path.name, pattern):
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def _classify_file(self, filepath: str) -> str:
|
|
59
|
+
path = Path(filepath)
|
|
60
|
+
filename = path.stem.lower()
|
|
61
|
+
parent = path.parent.name.lower()
|
|
62
|
+
|
|
63
|
+
for file_type, patterns in FILE_PATTERNS.items():
|
|
64
|
+
for pattern in patterns:
|
|
65
|
+
if pattern in filename or pattern in parent:
|
|
66
|
+
return file_type
|
|
67
|
+
|
|
68
|
+
return "other"
|
|
69
|
+
|
|
70
|
+
def _parse_file(self, filepath: Path) -> None:
|
|
71
|
+
relative_path = str(filepath.relative_to(self.root_path))
|
|
72
|
+
file_type = self._classify_file(str(filepath))
|
|
73
|
+
self.results["file_map"][relative_path] = file_type
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
source = filepath.read_text(encoding="utf-8")
|
|
77
|
+
tree = ast.parse(source)
|
|
78
|
+
except (SyntaxError, UnicodeDecodeError) as e:
|
|
79
|
+
self.results["errors"].append({
|
|
80
|
+
"file": relative_path,
|
|
81
|
+
"error": str(e)
|
|
82
|
+
})
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
for node in ast.walk(tree):
|
|
86
|
+
if isinstance(node, ast.ClassDef):
|
|
87
|
+
self.results["classes"].append({
|
|
88
|
+
"name": node.name,
|
|
89
|
+
"file": relative_path,
|
|
90
|
+
"type": file_type,
|
|
91
|
+
"line": node.lineno,
|
|
92
|
+
})
|
|
93
|
+
elif isinstance(node, ast.FunctionDef):
|
|
94
|
+
self.results["functions"].append({
|
|
95
|
+
"name": node.name,
|
|
96
|
+
"file": relative_path,
|
|
97
|
+
"type": file_type,
|
|
98
|
+
"line": node.lineno,
|
|
99
|
+
})
|
|
100
|
+
elif isinstance(node, ast.Import):
|
|
101
|
+
for alias in node.names:
|
|
102
|
+
self.results["imports"].append({
|
|
103
|
+
"name": alias.name,
|
|
104
|
+
"file": relative_path,
|
|
105
|
+
})
|
|
106
|
+
elif isinstance(node, ast.ImportFrom):
|
|
107
|
+
self.results["imports"].append({
|
|
108
|
+
"name": f"from {node.module} import ...",
|
|
109
|
+
"file": relative_path,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
def _get_python_files(self) -> list:
|
|
113
|
+
python_files = []
|
|
114
|
+
skip_dirs = {".venv", "__pycache__", ".git", "migrations", ".tox", "build", "dist"}
|
|
115
|
+
patterns = self._load_ignore_patterns()
|
|
116
|
+
|
|
117
|
+
for root, dirs, files in os.walk(self.root_path):
|
|
118
|
+
dirs[:] = [
|
|
119
|
+
d for d in dirs
|
|
120
|
+
if d not in skip_dirs
|
|
121
|
+
and not self._is_ignored(Path(root) / d, patterns)
|
|
122
|
+
]
|
|
123
|
+
for file in files:
|
|
124
|
+
filepath = Path(root) / file
|
|
125
|
+
if file.endswith(".py") and not self._is_ignored(filepath, patterns):
|
|
126
|
+
python_files.append(filepath)
|
|
127
|
+
|
|
128
|
+
return python_files
|
|
129
|
+
|
|
130
|
+
def analyze(self) -> dict:
|
|
131
|
+
python_files = self._get_python_files()
|
|
132
|
+
|
|
133
|
+
for filepath in python_files:
|
|
134
|
+
self._parse_file(filepath)
|
|
135
|
+
|
|
136
|
+
return self.results
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from .config import create_default_config, config_exists, write_config, read_config, create_default_ignore_file
|
|
4
|
+
from .core import run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def main():
|
|
9
|
+
"""AgenticStack — instant codebase map for AI agents."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@main.command()
|
|
14
|
+
@click.option("--api-key", default=None, help="Your LLM provider API key")
|
|
15
|
+
@click.option("--provider", default=None, help="Provider: anthropic or openai")
|
|
16
|
+
@click.option("--model", default=None, help="Model: default, fast, or smart")
|
|
17
|
+
def init(api_key, provider, model):
|
|
18
|
+
"""Initialize AgenticStack in this project."""
|
|
19
|
+
if config_exists():
|
|
20
|
+
click.echo("AgenticStack: Config already exists. Use 'agenticstack update' to refresh.")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
create_default_config()
|
|
24
|
+
click.echo("AgenticStack: Created .agenticstack.ini")
|
|
25
|
+
create_default_ignore_file()
|
|
26
|
+
click.echo("AgenticStack: Created .agenticstackignore")
|
|
27
|
+
|
|
28
|
+
if api_key:
|
|
29
|
+
write_config("api_key", api_key)
|
|
30
|
+
if provider:
|
|
31
|
+
write_config("provider", provider)
|
|
32
|
+
if model:
|
|
33
|
+
write_config("model", model)
|
|
34
|
+
|
|
35
|
+
run(Path.cwd())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@main.command()
|
|
39
|
+
def update():
|
|
40
|
+
"""Refresh AgenticStack.txt for this project."""
|
|
41
|
+
if not config_exists():
|
|
42
|
+
click.echo("AgenticStack: No config found. Run 'agenticstack init' first.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
run(Path.cwd())
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
CONFIG_FILE = ".agenticstack.ini"
|
|
5
|
+
SECTION = "agenticstack"
|
|
6
|
+
|
|
7
|
+
DEFAULTS = {
|
|
8
|
+
"model": "default",
|
|
9
|
+
"api_key": "None",
|
|
10
|
+
"provider": "anthropic",
|
|
11
|
+
"language": "auto",
|
|
12
|
+
"output_file": "AgenticStack.txt",
|
|
13
|
+
"depth": "standard",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_config_path() -> Path:
|
|
18
|
+
return Path.cwd() / CONFIG_FILE
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def config_exists() -> bool:
|
|
22
|
+
return get_config_path().exists()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_default_config() -> None:
|
|
26
|
+
config = configparser.ConfigParser()
|
|
27
|
+
config[SECTION] = DEFAULTS
|
|
28
|
+
with open(get_config_path(), "w") as f:
|
|
29
|
+
config.write(f)
|
|
30
|
+
_add_to_gitignore()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def read_config() -> dict:
|
|
34
|
+
if not config_exists():
|
|
35
|
+
create_default_config()
|
|
36
|
+
config = configparser.ConfigParser()
|
|
37
|
+
config.read(get_config_path())
|
|
38
|
+
return dict(config[SECTION])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def write_config(key: str, value: str) -> None:
|
|
42
|
+
config = configparser.ConfigParser()
|
|
43
|
+
config.read(get_config_path())
|
|
44
|
+
if SECTION not in config:
|
|
45
|
+
config[SECTION] = {}
|
|
46
|
+
config[SECTION][key] = value
|
|
47
|
+
with open(get_config_path(), "w") as f:
|
|
48
|
+
config.write(f)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _add_to_gitignore() -> None:
|
|
52
|
+
gitignore_path = Path.cwd() / ".gitignore"
|
|
53
|
+
entry = CONFIG_FILE
|
|
54
|
+
|
|
55
|
+
if gitignore_path.exists():
|
|
56
|
+
content = gitignore_path.read_text()
|
|
57
|
+
if entry not in content:
|
|
58
|
+
with open(gitignore_path, "a") as f:
|
|
59
|
+
f.write(f"\n{entry}\n")
|
|
60
|
+
else:
|
|
61
|
+
with open(gitignore_path, "w") as f:
|
|
62
|
+
f.write(f"{entry}\n")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_default_ignore_file() -> None:
|
|
66
|
+
ignore_path = Path.cwd() / ".agenticstackignore"
|
|
67
|
+
|
|
68
|
+
if ignore_path.exists():
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
content = """# AgenticStack Ignore File
|
|
72
|
+
# Patterns follow Unix glob syntax — same as .gitignore
|
|
73
|
+
|
|
74
|
+
# Virtual environments
|
|
75
|
+
.venv
|
|
76
|
+
venv
|
|
77
|
+
env
|
|
78
|
+
|
|
79
|
+
# Test directories
|
|
80
|
+
test_project
|
|
81
|
+
tests
|
|
82
|
+
|
|
83
|
+
# Build artifacts
|
|
84
|
+
build
|
|
85
|
+
dist
|
|
86
|
+
*.egg-info
|
|
87
|
+
|
|
88
|
+
# Cache
|
|
89
|
+
__pycache__
|
|
90
|
+
*.pyc
|
|
91
|
+
|
|
92
|
+
# Migrations
|
|
93
|
+
**/migrations/**
|
|
94
|
+
|
|
95
|
+
# Node modules (for projects with frontend)
|
|
96
|
+
node_modules
|
|
97
|
+
"""
|
|
98
|
+
ignore_path.write_text(content, encoding="utf-8")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from .config import read_config
|
|
3
|
+
from .analyzer.python import PythonAnalyzer
|
|
4
|
+
from .formatter.static import StaticFormatter
|
|
5
|
+
from .prompt import build_prompt
|
|
6
|
+
from .writer import write_output
|
|
7
|
+
|
|
8
|
+
def _run_ai_mode(
|
|
9
|
+
static_output: str,
|
|
10
|
+
api_key: str,
|
|
11
|
+
provider_name: str,
|
|
12
|
+
model: str
|
|
13
|
+
) -> str:
|
|
14
|
+
try:
|
|
15
|
+
from .providers.factory import get_provider
|
|
16
|
+
provider = get_provider(provider_name, api_key, model)
|
|
17
|
+
prompt = build_prompt(static_output)
|
|
18
|
+
print("AgenticStack: Sending to LLM...")
|
|
19
|
+
response = provider.generate(prompt)
|
|
20
|
+
return response
|
|
21
|
+
except Exception as e:
|
|
22
|
+
print(f"AgenticStack: AI mode failed — {e}")
|
|
23
|
+
print("AgenticStack: Falling back to static output")
|
|
24
|
+
return static_output
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(root_path: Path = None) -> None:
|
|
28
|
+
if root_path is None:
|
|
29
|
+
root_path = Path.cwd()
|
|
30
|
+
|
|
31
|
+
print("AgenticStack: Analyzing codebase...")
|
|
32
|
+
|
|
33
|
+
config = read_config()
|
|
34
|
+
|
|
35
|
+
analyzer = PythonAnalyzer(root_path)
|
|
36
|
+
results = analyzer.analyze()
|
|
37
|
+
|
|
38
|
+
formatter = StaticFormatter(results, root_path)
|
|
39
|
+
static_output = formatter.format()
|
|
40
|
+
|
|
41
|
+
api_key = config.get("api_key", "None")
|
|
42
|
+
provider_name = config.get("provider", "anthropic")
|
|
43
|
+
model = config.get("model", "default")
|
|
44
|
+
|
|
45
|
+
if api_key and api_key != "None":
|
|
46
|
+
print(f"AgenticStack: AI mode — using {provider_name} ({model})")
|
|
47
|
+
final_output = _run_ai_mode(static_output, api_key, provider_name, model)
|
|
48
|
+
else:
|
|
49
|
+
print("AgenticStack: Static mode — no API key found")
|
|
50
|
+
final_output = static_output
|
|
51
|
+
|
|
52
|
+
output_file = config.get("output_file", "AgenticStack.txt")
|
|
53
|
+
write_output(final_output, Path(output_file))
|
|
54
|
+
print(f"AgenticStack: Done. Output written to {output_file}")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
|
|
4
|
+
class StaticFormatter:
|
|
5
|
+
|
|
6
|
+
def __init__(self, results: dict, root_path: Path):
|
|
7
|
+
self.results = results
|
|
8
|
+
self.root_path = root_path
|
|
9
|
+
self.output = []
|
|
10
|
+
|
|
11
|
+
def _format_file_map(self) -> None:
|
|
12
|
+
self.output.append("=" * 60)
|
|
13
|
+
self.output.append("SECTION 1: WHERE THINGS LIVE")
|
|
14
|
+
self.output.append("=" * 60)
|
|
15
|
+
|
|
16
|
+
grouped = defaultdict(list)
|
|
17
|
+
for filepath, file_type in self.results["file_map"].items():
|
|
18
|
+
grouped[file_type].append(filepath)
|
|
19
|
+
|
|
20
|
+
for file_type, files in sorted(grouped.items()):
|
|
21
|
+
self.output.append(f"\n[{file_type.upper()}]")
|
|
22
|
+
for f in sorted(files):
|
|
23
|
+
self.output.append(f" {f}")
|
|
24
|
+
|
|
25
|
+
def _format_classes(self) -> None:
|
|
26
|
+
self.output.append("\n" + "=" * 60)
|
|
27
|
+
self.output.append("SECTION 2: CLASSES")
|
|
28
|
+
self.output.append("=" * 60)
|
|
29
|
+
|
|
30
|
+
grouped = defaultdict(list)
|
|
31
|
+
for cls in self.results["classes"]:
|
|
32
|
+
grouped[cls["type"]].append(cls)
|
|
33
|
+
|
|
34
|
+
for file_type, classes in sorted(grouped.items()):
|
|
35
|
+
self.output.append(f"\n[{file_type.upper()}]")
|
|
36
|
+
for cls in sorted(classes, key=lambda x: x["name"]):
|
|
37
|
+
self.output.append(f" {cls['name']} -> {cls['file']} (line {cls['line']})")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _format_functions(self) -> None:
|
|
41
|
+
self.output.append("\n" + "=" * 60)
|
|
42
|
+
self.output.append("SECTION 3: FUNCTIONS")
|
|
43
|
+
self.output.append("=" * 60)
|
|
44
|
+
|
|
45
|
+
grouped = defaultdict(list)
|
|
46
|
+
for func in self.results["functions"]:
|
|
47
|
+
grouped[func["type"]].append(func)
|
|
48
|
+
|
|
49
|
+
for file_type, functions in sorted(grouped.items()):
|
|
50
|
+
self.output.append(f"\n[{file_type.upper()}]")
|
|
51
|
+
for func in sorted(functions, key=lambda x: x["name"]):
|
|
52
|
+
self.output.append(f" {func['name']} -> {func['file']} (line {func['line']})")
|
|
53
|
+
|
|
54
|
+
def _format_errors(self) -> None:
|
|
55
|
+
if not self.results["errors"]:
|
|
56
|
+
return
|
|
57
|
+
self.output.append("\n" + "=" * 60)
|
|
58
|
+
self.output.append("SECTION 4: PARSE ERRORS")
|
|
59
|
+
self.output.append("=" * 60)
|
|
60
|
+
for error in self.results["errors"]:
|
|
61
|
+
self.output.append(f" {error['file']}: {error['error']}")
|
|
62
|
+
|
|
63
|
+
def format(self) -> str:
|
|
64
|
+
self._format_file_map()
|
|
65
|
+
self._format_classes()
|
|
66
|
+
self._format_functions()
|
|
67
|
+
self._format_errors()
|
|
68
|
+
return "\n".join(self.output)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
PROMPT_TEMPLATE = """You are an expert software architect analyzing a codebase.
|
|
2
|
+
|
|
3
|
+
You have been given a static analysis of a Python project below.
|
|
4
|
+
Your job is to produce a structured AgenticStack.txt file that helps AI agents
|
|
5
|
+
understand this codebase before making any changes.
|
|
6
|
+
|
|
7
|
+
STATIC ANALYSIS INPUT:
|
|
8
|
+
{static_analysis}
|
|
9
|
+
|
|
10
|
+
Produce output with EXACTLY these five sections. Do not add extra sections.
|
|
11
|
+
Do not use markdown. Use plain text only.
|
|
12
|
+
|
|
13
|
+
============================================================
|
|
14
|
+
SECTION 1: WHERE THINGS LIVE
|
|
15
|
+
============================================================
|
|
16
|
+
List every important file with its exact path and one line description.
|
|
17
|
+
Group by type: MODELS, VIEWS, SERIALIZERS, SERVICES, TESTS, OTHER.
|
|
18
|
+
Format:
|
|
19
|
+
filename.py -> what this file does in one line
|
|
20
|
+
|
|
21
|
+
============================================================
|
|
22
|
+
SECTION 2: CHANGE RULES
|
|
23
|
+
============================================================
|
|
24
|
+
Based on the files found, write rules that tell an agent WHEN to touch which file.
|
|
25
|
+
Use this decision tree format for each change type:
|
|
26
|
+
|
|
27
|
+
When adding a new [thing]:
|
|
28
|
+
Step 1: Always edit [file]
|
|
29
|
+
Step 2: If [condition] -> also edit [file]
|
|
30
|
+
Step 3: If [condition] -> also edit [file]
|
|
31
|
+
|
|
32
|
+
Cover these change types if the relevant files exist in the project:
|
|
33
|
+
- Adding a model field (always include migration step for Django projects)
|
|
34
|
+
- Adding a new endpoint
|
|
35
|
+
- Adding a new model
|
|
36
|
+
- Adding business logic
|
|
37
|
+
|
|
38
|
+
Every step must reference an actual file path from this project.
|
|
39
|
+
Never write generic steps like "update the model file" — always write
|
|
40
|
+
"edit test_project/app/models/user.py" using the real path.
|
|
41
|
+
|
|
42
|
+
============================================================
|
|
43
|
+
SECTION 3: FIELD EXPOSURE MAP
|
|
44
|
+
============================================================
|
|
45
|
+
List which models are exposed via API (have a serializer) vs internal only.
|
|
46
|
+
Format:
|
|
47
|
+
ModelName -> EXPOSED (serializer: path/to/serializer.py)
|
|
48
|
+
ModelName -> INTERNAL (no serializer found)
|
|
49
|
+
|
|
50
|
+
============================================================
|
|
51
|
+
SECTION 4: HOW-TO GUIDES
|
|
52
|
+
============================================================
|
|
53
|
+
Write step by step guides using the ACTUAL file paths found in this project.
|
|
54
|
+
Cover:
|
|
55
|
+
- How to add a model field
|
|
56
|
+
- How to add a new API endpoint
|
|
57
|
+
|
|
58
|
+
Each step must reference a real file path from this codebase.
|
|
59
|
+
Do not use generic placeholders like myapp/ or yourproject/.
|
|
60
|
+
|
|
61
|
+
============================================================
|
|
62
|
+
SECTION 5: AGENT NOTES
|
|
63
|
+
============================================================
|
|
64
|
+
Write 5 to 10 critical things an agent must know before editing this codebase.
|
|
65
|
+
Focus on patterns, constraints, and things that break easily.
|
|
66
|
+
Base these notes on what you can infer from the file structure and class names found.
|
|
67
|
+
|
|
68
|
+
Be specific. Be direct. No filler text.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_prompt(static_analysis: str) -> str:
|
|
73
|
+
return PROMPT_TEMPLATE.format(static_analysis=static_analysis)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .base import BaseProvider
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
ANTHROPIC_MODELS = {
|
|
5
|
+
"default": "claude-sonnet-4-20250514",
|
|
6
|
+
"fast": "claude-haiku-4-5-20251001",
|
|
7
|
+
"smart": "claude-opus-4-20250514",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnthropicProvider(BaseProvider):
|
|
12
|
+
|
|
13
|
+
def __init__(self, api_key: str, model: str = "default"):
|
|
14
|
+
resolved_model = ANTHROPIC_MODELS.get(model, ANTHROPIC_MODELS["default"])
|
|
15
|
+
super().__init__(api_key, resolved_model)
|
|
16
|
+
|
|
17
|
+
def generate(self, prompt: str) -> str:
|
|
18
|
+
try:
|
|
19
|
+
import anthropic
|
|
20
|
+
client = anthropic.Anthropic(api_key=self.api_key)
|
|
21
|
+
message = client.messages.create(
|
|
22
|
+
model=self.model,
|
|
23
|
+
max_tokens=8096,
|
|
24
|
+
messages=[{"role": "user", "content": prompt}],
|
|
25
|
+
)
|
|
26
|
+
return message.content[0].text
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"Anthropic SDK not installed. "
|
|
30
|
+
"Run: pip install agenticstack[anthropic]"
|
|
31
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .anthropic import AnthropicProvider
|
|
2
|
+
from .openai import OpenAIProvider
|
|
3
|
+
from .base import BaseProvider
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PROVIDERS = {
|
|
7
|
+
"anthropic": AnthropicProvider,
|
|
8
|
+
"openai": OpenAIProvider,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_provider(provider: str, api_key: str, model: str) -> BaseProvider:
|
|
13
|
+
provider_class = PROVIDERS.get(provider.lower())
|
|
14
|
+
|
|
15
|
+
if provider_class is None:
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Unknown provider '{provider}'. "
|
|
18
|
+
f"Valid options are: {', '.join(PROVIDERS.keys())}"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return provider_class(api_key=api_key, model=model)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .base import BaseProvider
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
OPENAI_MODELS = {
|
|
5
|
+
"default": "gpt-4o",
|
|
6
|
+
"fast": "gpt-4o-mini",
|
|
7
|
+
"smart": "gpt-4o",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpenAIProvider(BaseProvider):
|
|
12
|
+
|
|
13
|
+
def __init__(self, api_key: str, model: str = "default"):
|
|
14
|
+
resolved_model = OPENAI_MODELS.get(model, OPENAI_MODELS["default"])
|
|
15
|
+
super().__init__(api_key, resolved_model)
|
|
16
|
+
|
|
17
|
+
def generate(self, prompt: str) -> str:
|
|
18
|
+
try:
|
|
19
|
+
import openai
|
|
20
|
+
client = openai.OpenAI(api_key=self.api_key)
|
|
21
|
+
response = client.chat.completions.create(
|
|
22
|
+
model=self.model,
|
|
23
|
+
max_tokens=8096,
|
|
24
|
+
messages=[{"role": "user", "content": prompt}],
|
|
25
|
+
)
|
|
26
|
+
return response.choices[0].message.content
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise ImportError(
|
|
29
|
+
"OpenAI SDK not installed. "
|
|
30
|
+
"Run: pip install agenticstack[openai]"
|
|
31
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agenticstackfile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Instant codebase map for AI agents — understand any project before making changes
|
|
5
|
+
Author-email: Rajat Handa <handarajat111@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: anthropic
|
|
10
|
+
Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
|
|
11
|
+
Provides-Extra: openai
|
|
12
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
13
|
+
Provides-Extra: all
|
|
14
|
+
Requires-Dist: anthropic>=0.20.0; extra == "all"
|
|
15
|
+
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
agenticstack/__init__.py
|
|
4
|
+
agenticstack/cli.py
|
|
5
|
+
agenticstack/config.py
|
|
6
|
+
agenticstack/core.py
|
|
7
|
+
agenticstack/errors.py
|
|
8
|
+
agenticstack/prompt.py
|
|
9
|
+
agenticstack/writer.py
|
|
10
|
+
agenticstack/analyzer/__init__.py
|
|
11
|
+
agenticstack/analyzer/base.py
|
|
12
|
+
agenticstack/analyzer/python.py
|
|
13
|
+
agenticstack/formatter/__init__.py
|
|
14
|
+
agenticstack/formatter/static.py
|
|
15
|
+
agenticstack/providers/__init__.py
|
|
16
|
+
agenticstack/providers/anthropic.py
|
|
17
|
+
agenticstack/providers/base.py
|
|
18
|
+
agenticstack/providers/factory.py
|
|
19
|
+
agenticstack/providers/openai.py
|
|
20
|
+
agenticstackfile.egg-info/PKG-INFO
|
|
21
|
+
agenticstackfile.egg-info/SOURCES.txt
|
|
22
|
+
agenticstackfile.egg-info/dependency_links.txt
|
|
23
|
+
agenticstackfile.egg-info/entry_points.txt
|
|
24
|
+
agenticstackfile.egg-info/requires.txt
|
|
25
|
+
agenticstackfile.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agenticstack
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agenticstackfile"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Instant codebase map for AI agents — understand any project before making changes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Rajat Handa", email = "handarajat111@gmail.com"}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
anthropic = ["anthropic>=0.20.0"]
|
|
18
|
+
openai = ["openai>=1.0.0"]
|
|
19
|
+
all = ["anthropic>=0.20.0", "openai>=1.0.0"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
agenticstack = "agenticstack.cli:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["."]
|
|
26
|
+
include = ["agenticstack*"]
|