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.
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
+