contextforllm 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.
- contextforllm/__init__.py +0 -0
- contextforllm/__main__.py +34 -0
- contextforllm/app.py +225 -0
- contextforllm/context_builder.py +178 -0
- contextforllm/project_summary.py +87 -0
- contextforllm/ui/index.html +974 -0
- contextforllm-0.1.0.dist-info/METADATA +145 -0
- contextforllm-0.1.0.dist-info/RECORD +12 -0
- contextforllm-0.1.0.dist-info/WHEEL +5 -0
- contextforllm-0.1.0.dist-info/entry_points.txt +2 -0
- contextforllm-0.1.0.dist-info/licenses/LICENSE +20 -0
- contextforllm-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import webbrowser
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
app_dir = os.path.dirname(os.path.abspath(__file__))
|
|
9
|
+
app_path = os.path.join(app_dir, "app.py")
|
|
10
|
+
|
|
11
|
+
print("")
|
|
12
|
+
print("Starting ContextForLLM...")
|
|
13
|
+
print("")
|
|
14
|
+
|
|
15
|
+
process = subprocess.Popen(
|
|
16
|
+
[sys.executable, app_path],
|
|
17
|
+
cwd=app_dir
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
time.sleep(2)
|
|
21
|
+
webbrowser.open("http://127.0.0.1:5000")
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
process.wait()
|
|
25
|
+
except KeyboardInterrupt:
|
|
26
|
+
process.terminate()
|
|
27
|
+
print("")
|
|
28
|
+
print("ContextForLLM stopped.")
|
|
29
|
+
print("")
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
33
|
+
|
|
34
|
+
|
contextforllm/app.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from flask import Flask, request, jsonify, send_from_directory
|
|
4
|
+
from context_builder import (
|
|
5
|
+
build_folder_tree,
|
|
6
|
+
collect_files,
|
|
7
|
+
build_file_block,
|
|
8
|
+
build_header,
|
|
9
|
+
split_into_prompts,
|
|
10
|
+
save_prompts,
|
|
11
|
+
count_tokens,
|
|
12
|
+
load_contextignore,
|
|
13
|
+
)
|
|
14
|
+
from project_summary import (
|
|
15
|
+
generate_project_summary,
|
|
16
|
+
save_summary,
|
|
17
|
+
load_summary,
|
|
18
|
+
delete_summary,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
app = Flask(__name__, static_folder=os.path.join(os.path.dirname(__file__), "ui"))
|
|
22
|
+
|
|
23
|
+
TOOL_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
RECENT_FILE = os.path.join(TOOL_DIR, "recent_projects.json")
|
|
25
|
+
MAX_RECENT = 8
|
|
26
|
+
|
|
27
|
+
# Session-only Groq key (not saved to disk)
|
|
28
|
+
session_groq_key = ""
|
|
29
|
+
|
|
30
|
+
# ── Recent projects ────────────────────────────────────────────
|
|
31
|
+
def load_recent():
|
|
32
|
+
if os.path.isfile(RECENT_FILE):
|
|
33
|
+
try:
|
|
34
|
+
with open(RECENT_FILE, "r") as f:
|
|
35
|
+
return json.load(f)
|
|
36
|
+
except Exception:
|
|
37
|
+
return []
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
def save_recent(project_path, project_name):
|
|
41
|
+
recent = load_recent()
|
|
42
|
+
recent = [r for r in recent if r["path"] != project_path]
|
|
43
|
+
recent.insert(0, {"path": project_path, "name": project_name})
|
|
44
|
+
recent = recent[:MAX_RECENT]
|
|
45
|
+
with open(RECENT_FILE, "w") as f:
|
|
46
|
+
json.dump(recent, f, indent=2)
|
|
47
|
+
|
|
48
|
+
# ── Routes ─────────────────────────────────────────────────────
|
|
49
|
+
@app.route("/")
|
|
50
|
+
def index():
|
|
51
|
+
return send_from_directory("ui", "index.html")
|
|
52
|
+
|
|
53
|
+
@app.route("/api/recent", methods=["GET"])
|
|
54
|
+
def get_recent():
|
|
55
|
+
recent = load_recent()
|
|
56
|
+
recent = [r for r in recent if os.path.isdir(r["path"])]
|
|
57
|
+
return jsonify({"recent": recent})
|
|
58
|
+
|
|
59
|
+
@app.route("/api/recent/remove", methods=["POST"])
|
|
60
|
+
def remove_recent():
|
|
61
|
+
data = request.json
|
|
62
|
+
path = data.get("path", "")
|
|
63
|
+
recent = load_recent()
|
|
64
|
+
recent = [r for r in recent if r["path"] != path]
|
|
65
|
+
with open(RECENT_FILE, "w") as f:
|
|
66
|
+
json.dump(recent, f, indent=2)
|
|
67
|
+
delete_summary(TOOL_DIR, path)
|
|
68
|
+
return jsonify({"ok": True})
|
|
69
|
+
|
|
70
|
+
@app.route("/api/groq-key", methods=["POST"])
|
|
71
|
+
def set_groq_key():
|
|
72
|
+
global session_groq_key
|
|
73
|
+
data = request.json
|
|
74
|
+
key = data.get("key", "").strip()
|
|
75
|
+
if not key:
|
|
76
|
+
return jsonify({"error": "Key is empty"}), 400
|
|
77
|
+
if not key.startswith("gsk_"):
|
|
78
|
+
return jsonify({"error": "Invalid key — Groq keys start with gsk_"}), 400
|
|
79
|
+
session_groq_key = key
|
|
80
|
+
return jsonify({"ok": True})
|
|
81
|
+
|
|
82
|
+
@app.route("/api/groq-key/status", methods=["GET"])
|
|
83
|
+
def groq_key_status():
|
|
84
|
+
return jsonify({"has_key": bool(session_groq_key)})
|
|
85
|
+
|
|
86
|
+
@app.route("/api/scan", methods=["POST"])
|
|
87
|
+
def scan():
|
|
88
|
+
data = request.json
|
|
89
|
+
project_path = data.get("project_path", "").strip()
|
|
90
|
+
project_path = os.path.expanduser(project_path)
|
|
91
|
+
project_path = os.path.abspath(project_path)
|
|
92
|
+
|
|
93
|
+
if not os.path.isdir(project_path):
|
|
94
|
+
return jsonify({"error": f"Folder not found: {project_path}"}), 400
|
|
95
|
+
|
|
96
|
+
project_name = os.path.basename(project_path)
|
|
97
|
+
tree_lines = build_folder_tree(project_path)
|
|
98
|
+
files = collect_files(project_path)
|
|
99
|
+
patterns = load_contextignore(project_path)
|
|
100
|
+
save_recent(project_path, project_name)
|
|
101
|
+
|
|
102
|
+
file_blocks = [build_file_block(path, content) for path, content in files]
|
|
103
|
+
header = build_header(project_name, project_path, "\n".join(tree_lines), 1)
|
|
104
|
+
full_text = header + "\n".join(file_blocks)
|
|
105
|
+
total_tokens = count_tokens(full_text)
|
|
106
|
+
|
|
107
|
+
existing_summary = load_summary(TOOL_DIR, project_path)
|
|
108
|
+
|
|
109
|
+
return jsonify({
|
|
110
|
+
"project_name": project_name,
|
|
111
|
+
"project_path": project_path,
|
|
112
|
+
"tree": tree_lines,
|
|
113
|
+
"files": [{"path": p, "tokens": count_tokens(c)} for p, c in files],
|
|
114
|
+
"total_tokens": total_tokens,
|
|
115
|
+
"file_count": len(files),
|
|
116
|
+
"contextignore_rules": patterns,
|
|
117
|
+
"existing_summary": existing_summary,
|
|
118
|
+
"groq_key_set": bool(session_groq_key)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
@app.route("/api/summary/generate", methods=["POST"])
|
|
122
|
+
def generate_summary():
|
|
123
|
+
global session_groq_key
|
|
124
|
+
data = request.json
|
|
125
|
+
project_path = os.path.abspath(
|
|
126
|
+
os.path.expanduser(data.get("project_path", "")))
|
|
127
|
+
|
|
128
|
+
if not session_groq_key:
|
|
129
|
+
return jsonify({"error": "No Groq API key set for this session"}), 400
|
|
130
|
+
|
|
131
|
+
if not os.path.isdir(project_path):
|
|
132
|
+
return jsonify({"error": "Folder not found"}), 400
|
|
133
|
+
|
|
134
|
+
project_name = os.path.basename(project_path)
|
|
135
|
+
files = collect_files(project_path)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
summary = generate_project_summary(project_name, files, session_groq_key)
|
|
139
|
+
save_summary(TOOL_DIR, project_path, summary)
|
|
140
|
+
return jsonify({"summary": summary})
|
|
141
|
+
except Exception as e:
|
|
142
|
+
return jsonify({"error": str(e)}), 500
|
|
143
|
+
|
|
144
|
+
@app.route("/api/summary/save", methods=["POST"])
|
|
145
|
+
def save_summary_route():
|
|
146
|
+
data = request.json
|
|
147
|
+
project_path = os.path.abspath(
|
|
148
|
+
os.path.expanduser(data.get("project_path", "")))
|
|
149
|
+
summary = data.get("summary", "").strip()
|
|
150
|
+
if not summary:
|
|
151
|
+
return jsonify({"error": "Summary is empty"}), 400
|
|
152
|
+
save_summary(TOOL_DIR, project_path, summary)
|
|
153
|
+
return jsonify({"ok": True})
|
|
154
|
+
|
|
155
|
+
@app.route("/api/summary/delete", methods=["POST"])
|
|
156
|
+
def delete_summary_route():
|
|
157
|
+
data = request.json
|
|
158
|
+
project_path = os.path.abspath(
|
|
159
|
+
os.path.expanduser(data.get("project_path", "")))
|
|
160
|
+
delete_summary(TOOL_DIR, project_path)
|
|
161
|
+
return jsonify({"ok": True})
|
|
162
|
+
|
|
163
|
+
@app.route("/api/generate", methods=["POST"])
|
|
164
|
+
def generate():
|
|
165
|
+
data = request.json
|
|
166
|
+
project_path = os.path.abspath(
|
|
167
|
+
os.path.expanduser(data.get("project_path", "")))
|
|
168
|
+
task = data.get("task", "").strip()
|
|
169
|
+
excluded = set(data.get("excluded", []))
|
|
170
|
+
annotations = data.get("annotations", {})
|
|
171
|
+
include_summary = data.get("include_summary", True)
|
|
172
|
+
|
|
173
|
+
if not os.path.isdir(project_path):
|
|
174
|
+
return jsonify({"error": "Folder not found"}), 400
|
|
175
|
+
|
|
176
|
+
project_name = os.path.basename(project_path)
|
|
177
|
+
tree_lines = build_folder_tree(project_path)
|
|
178
|
+
all_files = collect_files(project_path)
|
|
179
|
+
files = [(p, c) for p, c in all_files if p not in excluded]
|
|
180
|
+
|
|
181
|
+
summary_text = ""
|
|
182
|
+
if include_summary:
|
|
183
|
+
summary_text = load_summary(TOOL_DIR, project_path) or ""
|
|
184
|
+
|
|
185
|
+
output_dir = os.path.join(TOOL_DIR, "output")
|
|
186
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
187
|
+
|
|
188
|
+
file_blocks = [
|
|
189
|
+
build_file_block(path, content, annotations.get(path, ""))
|
|
190
|
+
for path, content in files
|
|
191
|
+
]
|
|
192
|
+
header = build_header(project_name, project_path, "\n".join(tree_lines), 1)
|
|
193
|
+
|
|
194
|
+
if summary_text:
|
|
195
|
+
header = f"## PROJECT SUMMARY\n\n{summary_text}\n\n{header}"
|
|
196
|
+
|
|
197
|
+
prompts = split_into_prompts(header, file_blocks, task, project_name)
|
|
198
|
+
saved_files = save_prompts(prompts, output_dir)
|
|
199
|
+
|
|
200
|
+
parts = []
|
|
201
|
+
for i, filepath in enumerate(saved_files):
|
|
202
|
+
with open(filepath, "r") as f:
|
|
203
|
+
content = f.read()
|
|
204
|
+
parts.append({
|
|
205
|
+
"part": i + 1,
|
|
206
|
+
"filename": os.path.basename(filepath),
|
|
207
|
+
"tokens": count_tokens(content),
|
|
208
|
+
"content": content
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return jsonify({
|
|
212
|
+
"total_parts": len(parts),
|
|
213
|
+
"output_dir": output_dir,
|
|
214
|
+
"parts": parts
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
print("\nContextForLLM is running.")
|
|
219
|
+
print("Open this in your browser: http://localhost:5000\n")
|
|
220
|
+
app.run(debug=False, port=5000)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import fnmatch
|
|
3
|
+
import tiktoken
|
|
4
|
+
|
|
5
|
+
INCLUDE_EXTENSIONS = [
|
|
6
|
+
".py", ".js", ".ts", ".jsx", ".tsx",
|
|
7
|
+
".html", ".css", ".json", ".md",
|
|
8
|
+
".txt", ".env.example", ".yaml", ".yml",
|
|
9
|
+
".sh", ".sql"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
SKIP_FOLDERS = [
|
|
13
|
+
"venv", ".venv", "env",
|
|
14
|
+
"node_modules", ".git",
|
|
15
|
+
"__pycache__", ".next",
|
|
16
|
+
"dist", "build", ".idea",
|
|
17
|
+
".vscode", "coverage",
|
|
18
|
+
"output", "summaries"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
MAX_TOKENS_PER_PART = 80000
|
|
22
|
+
|
|
23
|
+
# ── Token counting ─────────────────────────────────────────────
|
|
24
|
+
def count_tokens(text):
|
|
25
|
+
try:
|
|
26
|
+
enc = tiktoken.get_encoding("cl100k_base")
|
|
27
|
+
return len(enc.encode(text))
|
|
28
|
+
except Exception:
|
|
29
|
+
return len(text) // 4
|
|
30
|
+
|
|
31
|
+
# ── .contextignore ─────────────────────────────────────────────
|
|
32
|
+
def load_contextignore(project_path):
|
|
33
|
+
ignore_file = os.path.join(project_path, ".contextignore")
|
|
34
|
+
if not os.path.isfile(ignore_file):
|
|
35
|
+
return []
|
|
36
|
+
patterns = []
|
|
37
|
+
with open(ignore_file, "r") as f:
|
|
38
|
+
for line in f:
|
|
39
|
+
line = line.strip()
|
|
40
|
+
if line and not line.startswith("#"):
|
|
41
|
+
patterns.append(line)
|
|
42
|
+
return patterns
|
|
43
|
+
|
|
44
|
+
def is_ignored(rel_path, patterns):
|
|
45
|
+
for pattern in patterns:
|
|
46
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
|
47
|
+
return True
|
|
48
|
+
if fnmatch.fnmatch(os.path.basename(rel_path), pattern):
|
|
49
|
+
return True
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
# ── File collection ────────────────────────────────────────────
|
|
53
|
+
def collect_files(project_path):
|
|
54
|
+
patterns = load_contextignore(project_path)
|
|
55
|
+
result = []
|
|
56
|
+
for root, dirs, files in os.walk(project_path):
|
|
57
|
+
dirs[:] = [
|
|
58
|
+
d for d in dirs
|
|
59
|
+
if d not in SKIP_FOLDERS and not d.startswith(".")
|
|
60
|
+
]
|
|
61
|
+
for fname in sorted(files):
|
|
62
|
+
_, ext = os.path.splitext(fname)
|
|
63
|
+
if ext.lower() not in INCLUDE_EXTENSIONS:
|
|
64
|
+
continue
|
|
65
|
+
full_path = os.path.join(root, fname)
|
|
66
|
+
rel_path = os.path.relpath(full_path, project_path)
|
|
67
|
+
if is_ignored(rel_path, patterns):
|
|
68
|
+
continue
|
|
69
|
+
try:
|
|
70
|
+
with open(full_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
71
|
+
content = f.read()
|
|
72
|
+
result.append((rel_path, content))
|
|
73
|
+
except Exception:
|
|
74
|
+
continue
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
# ── Folder tree ────────────────────────────────────────────────
|
|
78
|
+
def build_folder_tree(project_path):
|
|
79
|
+
lines = []
|
|
80
|
+
for root, dirs, files in os.walk(project_path):
|
|
81
|
+
dirs[:] = sorted([
|
|
82
|
+
d for d in dirs
|
|
83
|
+
if d not in SKIP_FOLDERS and not d.startswith(".")
|
|
84
|
+
])
|
|
85
|
+
level = os.path.relpath(root, project_path).count(os.sep)
|
|
86
|
+
if os.path.relpath(root, project_path) == ".":
|
|
87
|
+
level = 0
|
|
88
|
+
indent = " " * level
|
|
89
|
+
if level > 0:
|
|
90
|
+
lines.append(f"{indent}— {os.path.basename(root)}/")
|
|
91
|
+
for fname in sorted(files):
|
|
92
|
+
_, ext = os.path.splitext(fname)
|
|
93
|
+
if ext.lower() in INCLUDE_EXTENSIONS:
|
|
94
|
+
lines.append(f"{' ' * (level+1 if level > 0 else 1)}— {fname}")
|
|
95
|
+
return lines
|
|
96
|
+
|
|
97
|
+
# ── File block ─────────────────────────────────────────────────
|
|
98
|
+
def build_file_block(rel_path, content, annotation=""):
|
|
99
|
+
lines = []
|
|
100
|
+
lines.append(f"\n{'='*60}")
|
|
101
|
+
lines.append(f"FILE: {rel_path}")
|
|
102
|
+
if annotation:
|
|
103
|
+
lines.append(f"# NOTE: {annotation}")
|
|
104
|
+
lines.append("="*60)
|
|
105
|
+
lines.append(content)
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
# ── Header ─────────────────────────────────────────────────────
|
|
109
|
+
def build_header(project_name, project_path, tree_text, part_num):
|
|
110
|
+
return f"""{'='*60}
|
|
111
|
+
CONTEXT DOCUMENT — {project_name}
|
|
112
|
+
Part {part_num}
|
|
113
|
+
{'='*60}
|
|
114
|
+
|
|
115
|
+
This document contains the full source code of the project '{project_name}'.
|
|
116
|
+
Read all files carefully before responding.
|
|
117
|
+
|
|
118
|
+
PROJECT PATH: {project_path}
|
|
119
|
+
|
|
120
|
+
FOLDER STRUCTURE:
|
|
121
|
+
{tree_text}
|
|
122
|
+
|
|
123
|
+
{'='*60}
|
|
124
|
+
SOURCE FILES
|
|
125
|
+
{'='*60}
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# ── Prompt splitter ────────────────────────────────────────────
|
|
129
|
+
def split_into_prompts(header, file_blocks, task, project_name):
|
|
130
|
+
parts = []
|
|
131
|
+
current_blocks = []
|
|
132
|
+
current_tokens = count_tokens(header)
|
|
133
|
+
|
|
134
|
+
for block in file_blocks:
|
|
135
|
+
block_tokens = count_tokens(block)
|
|
136
|
+
if current_tokens + block_tokens > MAX_TOKENS_PER_PART and current_blocks:
|
|
137
|
+
parts.append(current_blocks)
|
|
138
|
+
current_blocks = [block]
|
|
139
|
+
current_tokens = count_tokens(header) + block_tokens
|
|
140
|
+
else:
|
|
141
|
+
current_blocks.append(block)
|
|
142
|
+
current_tokens += block_tokens
|
|
143
|
+
|
|
144
|
+
if current_blocks:
|
|
145
|
+
parts.append(current_blocks)
|
|
146
|
+
|
|
147
|
+
total = len(parts)
|
|
148
|
+
prompts = []
|
|
149
|
+
for i, blocks in enumerate(parts):
|
|
150
|
+
part_num = i + 1
|
|
151
|
+
h = build_header(project_name, "", "", part_num)
|
|
152
|
+
body = h + "\n".join(blocks)
|
|
153
|
+
if part_num < total:
|
|
154
|
+
body += f"\n\n{'='*60}\nEND OF PART {part_num} OF {total}\n"
|
|
155
|
+
body += "This is not all the code. Wait for the next part before responding.\n"
|
|
156
|
+
body += f"Reply only with: 'Part {part_num} received. Send part {part_num+1}.'\n{'='*60}"
|
|
157
|
+
else:
|
|
158
|
+
if task:
|
|
159
|
+
body += f"\n\n{'='*60}\nYOUR TASK\n{'='*60}\n{task}"
|
|
160
|
+
body += f"\n\n{'='*60}\nEND OF CONTEXT"
|
|
161
|
+
if total > 1:
|
|
162
|
+
body += f" (Final part {part_num} of {total})"
|
|
163
|
+
body += f"\n{'='*60}"
|
|
164
|
+
if total > 1:
|
|
165
|
+
body += "\nAll parts have been sent. Please complete the task above."
|
|
166
|
+
prompts.append(body)
|
|
167
|
+
|
|
168
|
+
return prompts
|
|
169
|
+
|
|
170
|
+
def save_prompts(prompts, output_dir):
|
|
171
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
172
|
+
saved = []
|
|
173
|
+
for i, prompt in enumerate(prompts):
|
|
174
|
+
path = os.path.join(output_dir, f"prompt_part_{i+1}.txt")
|
|
175
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
176
|
+
f.write(prompt)
|
|
177
|
+
saved.append(path)
|
|
178
|
+
return saved
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from groq import Groq
|
|
3
|
+
|
|
4
|
+
SUMMARY_MODEL = "llama-3.3-70b-versatile"
|
|
5
|
+
|
|
6
|
+
def get_client(api_key):
|
|
7
|
+
if not api_key:
|
|
8
|
+
raise ValueError("No Groq API key provided")
|
|
9
|
+
return Groq(api_key=api_key)
|
|
10
|
+
|
|
11
|
+
def build_condensed_context(files, max_chars_per_file=800):
|
|
12
|
+
lines = []
|
|
13
|
+
for path, content in files:
|
|
14
|
+
lines.append(f"\n--- FILE: {path} ---")
|
|
15
|
+
preview = content[:max_chars_per_file]
|
|
16
|
+
if len(content) > max_chars_per_file:
|
|
17
|
+
preview += "\n... (truncated)"
|
|
18
|
+
lines.append(preview)
|
|
19
|
+
return "\n".join(lines)
|
|
20
|
+
|
|
21
|
+
def generate_project_summary(project_name, files, api_key):
|
|
22
|
+
client = get_client(api_key)
|
|
23
|
+
condensed = build_condensed_context(files)
|
|
24
|
+
prompt = f"""You are analyzing a software project called '{project_name}'.
|
|
25
|
+
|
|
26
|
+
Here is a condensed view of the project files:
|
|
27
|
+
|
|
28
|
+
{condensed}
|
|
29
|
+
|
|
30
|
+
Write a concise project summary with exactly these sections:
|
|
31
|
+
|
|
32
|
+
**What this project does:**
|
|
33
|
+
One or two sentences describing what the project is and what problem it solves.
|
|
34
|
+
|
|
35
|
+
**Tech stack:**
|
|
36
|
+
List the main languages, frameworks, and libraries used.
|
|
37
|
+
|
|
38
|
+
**Main files:**
|
|
39
|
+
A short description of what each key file does. One line per file.
|
|
40
|
+
|
|
41
|
+
**Notes for the LLM:**
|
|
42
|
+
Any important things an LLM should know before working on this codebase — conventions, patterns, or things to be careful about.
|
|
43
|
+
|
|
44
|
+
Keep the entire summary under 300 words. Be specific and factual. Do not make things up."""
|
|
45
|
+
|
|
46
|
+
response = client.chat.completions.create(
|
|
47
|
+
model=SUMMARY_MODEL,
|
|
48
|
+
messages=[
|
|
49
|
+
{
|
|
50
|
+
"role": "system",
|
|
51
|
+
"content": "You are a technical assistant that writes clear, accurate project summaries for developers."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"role": "user",
|
|
55
|
+
"content": prompt
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
temperature=0.3,
|
|
59
|
+
max_tokens=600
|
|
60
|
+
)
|
|
61
|
+
return response.choices[0].message.content.strip()
|
|
62
|
+
|
|
63
|
+
# ── Summary storage ────────────────────────────────────────────
|
|
64
|
+
def get_summary_path(tool_dir, project_path):
|
|
65
|
+
import hashlib
|
|
66
|
+
summaries_dir = os.path.join(tool_dir, "summaries")
|
|
67
|
+
os.makedirs(summaries_dir, exist_ok=True)
|
|
68
|
+
path_hash = hashlib.md5(project_path.encode()).hexdigest()
|
|
69
|
+
return os.path.join(summaries_dir, f"{path_hash}.txt")
|
|
70
|
+
|
|
71
|
+
def save_summary(tool_dir, project_path, summary):
|
|
72
|
+
summary_path = get_summary_path(tool_dir, project_path)
|
|
73
|
+
with open(summary_path, "w", encoding="utf-8") as f:
|
|
74
|
+
f.write(summary)
|
|
75
|
+
|
|
76
|
+
def load_summary(tool_dir, project_path):
|
|
77
|
+
summary_path = get_summary_path(tool_dir, project_path)
|
|
78
|
+
if os.path.isfile(summary_path):
|
|
79
|
+
with open(summary_path, "r", encoding="utf-8") as f:
|
|
80
|
+
return f.read()
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def delete_summary(tool_dir, project_path):
|
|
84
|
+
summary_path = get_summary_path(tool_dir, project_path)
|
|
85
|
+
if os.path.isfile(summary_path):
|
|
86
|
+
os.remove(summary_path)
|
|
87
|
+
|