code-explore 0.2.0__tar.gz → 0.4.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.
- code_explore-0.4.0/.github/workflows/pages.yml +44 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/PKG-INFO +1 -1
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/api/main.py +93 -17
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/cli/main.py +180 -17
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/database.py +4 -1
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/indexer/embeddings.py +5 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/models.py +25 -0
- code_explore-0.4.0/code_explore/search/filters.py +113 -0
- code_explore-0.4.0/code_explore/static/app.js +482 -0
- code_explore-0.4.0/code_explore/static/index.html +78 -0
- code_explore-0.4.0/code_explore/static/style.css +349 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/summarizer/ollama.py +21 -10
- code_explore-0.4.0/code_explore/tagger/__init__.py +56 -0
- code_explore-0.4.0/docs/CNAME +1 -0
- code_explore-0.4.0/docs/index.html +93 -0
- code_explore-0.4.0/install.cmd +58 -0
- code_explore-0.4.0/install.ps1 +84 -0
- code_explore-0.4.0/install.sh +110 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/pyproject.toml +7 -1
- {code_explore-0.2.0 → code_explore-0.4.0}/.editorconfig +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/.github/workflows/publish.yml +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/.gitignore +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/README.md +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/analyzer/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/analyzer/dependencies.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/analyzer/language.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/analyzer/metrics.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/analyzer/patterns.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/api/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/cli/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/cli/config_cmd.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/config.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/indexer/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/scanner/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/scanner/git_info.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/scanner/local.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/scanner/readme.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/search/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/search/fulltext.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/search/hybrid.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/search/semantic.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/code_explore/summarizer/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/conftest.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/test_cli.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/test_config.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/test_config_cli.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/test_database.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/test_models.py +0 -0
- {code_explore-0.2.0 → code_explore-0.4.0}/tests/test_search_hybrid.py +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Deploy to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- 'docs/**'
|
|
8
|
+
- 'install.*'
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pages: write
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: pages
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Prepare site
|
|
26
|
+
run: |
|
|
27
|
+
mkdir -p _site
|
|
28
|
+
cp docs/index.html _site/index.html
|
|
29
|
+
cp docs/CNAME _site/CNAME
|
|
30
|
+
cp install.sh _site/install.sh
|
|
31
|
+
cp install.ps1 _site/install.ps1
|
|
32
|
+
cp install.cmd _site/install.cmd
|
|
33
|
+
|
|
34
|
+
- uses: actions/upload-pages-artifact@v3
|
|
35
|
+
|
|
36
|
+
deploy:
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment:
|
|
40
|
+
name: github-pages
|
|
41
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
42
|
+
steps:
|
|
43
|
+
- id: deployment
|
|
44
|
+
uses: actions/deploy-pages@v4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-explore
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Developer knowledge base CLI — scan, index, and search your programming projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/aipioneers/code-explore
|
|
6
6
|
Project-URL: Repository, https://github.com/aipioneers/code-explore
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
"""FastAPI REST API for Code Explore."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import hashlib
|
|
4
|
+
from collections import Counter
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from fastapi import FastAPI, HTTPException, Query
|
|
9
9
|
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
12
13
|
from code_explore.database import init_db, save_project, get_project, get_all_projects, get_project_count
|
|
13
|
-
from code_explore.models import Project, ProjectSource, ProjectStatus
|
|
14
|
+
from code_explore.models import Project, ProjectSource, ProjectStatus, SearchFacets
|
|
15
|
+
from code_explore.search.filters import apply_filters, filter_projects, compute_facets
|
|
14
16
|
|
|
15
|
-
app = FastAPI(title="Code Explore", version="0.
|
|
17
|
+
app = FastAPI(title="Code Explore", version="0.4.0", description="Developer knowledge base API")
|
|
16
18
|
|
|
17
19
|
app.add_middleware(
|
|
18
20
|
CORSMiddleware,
|
|
@@ -43,36 +45,38 @@ class StatsResponse(BaseModel):
|
|
|
43
45
|
languages: dict[str, int]
|
|
44
46
|
frameworks: dict[str, int]
|
|
45
47
|
patterns: dict[str, int]
|
|
48
|
+
ai_tags: dict[str, int]
|
|
46
49
|
total_files: int
|
|
47
50
|
total_lines: int
|
|
48
51
|
statuses: dict[str, int]
|
|
49
52
|
|
|
50
53
|
|
|
54
|
+
class TagInfo(BaseModel):
|
|
55
|
+
value: str
|
|
56
|
+
category: str
|
|
57
|
+
count: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TagsResponse(BaseModel):
|
|
61
|
+
tags: list[TagInfo]
|
|
62
|
+
total_tags: int
|
|
63
|
+
categories: dict[str, int]
|
|
64
|
+
|
|
65
|
+
|
|
51
66
|
@app.get("/api/projects", response_model=list[Project])
|
|
52
67
|
async def list_projects(
|
|
53
68
|
language: str | None = Query(None, description="Filter by primary language"),
|
|
54
69
|
framework: str | None = Query(None, description="Filter by framework"),
|
|
55
70
|
source: str | None = Query(None, description="Filter by source (local, github, gitlab)"),
|
|
71
|
+
tag: str | None = Query(None, description="Filter by AI tag"),
|
|
56
72
|
):
|
|
57
73
|
projects = get_all_projects()
|
|
58
74
|
|
|
59
|
-
if language:
|
|
60
|
-
lang_lower = language.lower()
|
|
61
|
-
projects = [
|
|
62
|
-
p for p in projects
|
|
63
|
-
if p.primary_language and p.primary_language.lower() == lang_lower
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
if framework:
|
|
67
|
-
fw_lower = framework.lower()
|
|
68
|
-
projects = [
|
|
69
|
-
p for p in projects
|
|
70
|
-
if any(f.lower() == fw_lower for f in p.frameworks)
|
|
71
|
-
]
|
|
72
|
-
|
|
73
75
|
if source:
|
|
74
76
|
projects = [p for p in projects if p.source.value == source]
|
|
75
77
|
|
|
78
|
+
projects = filter_projects(projects, language=language, framework=framework, tag=tag)
|
|
79
|
+
|
|
76
80
|
return projects
|
|
77
81
|
|
|
78
82
|
|
|
@@ -89,6 +93,10 @@ async def search_projects(
|
|
|
89
93
|
q: str = Query(..., description="Search query"),
|
|
90
94
|
mode: str = Query("hybrid", description="Search mode: fulltext, semantic, or hybrid"),
|
|
91
95
|
limit: int = Query(20, ge=1, le=100, description="Maximum results"),
|
|
96
|
+
language: str | None = Query(None, description="Filter by primary language"),
|
|
97
|
+
framework: str | None = Query(None, description="Filter by framework"),
|
|
98
|
+
pattern: str | None = Query(None, description="Filter by pattern"),
|
|
99
|
+
tag: str | None = Query(None, description="Filter by AI tag"),
|
|
92
100
|
):
|
|
93
101
|
if mode == "fulltext":
|
|
94
102
|
from code_explore.search.fulltext import search as fulltext_search
|
|
@@ -100,6 +108,9 @@ async def search_projects(
|
|
|
100
108
|
from code_explore.search.hybrid import search as hybrid_search
|
|
101
109
|
results = hybrid_search(q, limit=limit)
|
|
102
110
|
|
|
111
|
+
# Apply post-filters
|
|
112
|
+
results = apply_filters(results, language=language, framework=framework, pattern=pattern, tag=tag)
|
|
113
|
+
|
|
103
114
|
return [
|
|
104
115
|
{
|
|
105
116
|
"project": r.project.model_dump(),
|
|
@@ -111,6 +122,61 @@ async def search_projects(
|
|
|
111
122
|
]
|
|
112
123
|
|
|
113
124
|
|
|
125
|
+
@app.get("/api/facets", response_model=SearchFacets)
|
|
126
|
+
async def get_facets(
|
|
127
|
+
q: str | None = Query(None, description="Search query to scope facets"),
|
|
128
|
+
language: str | None = Query(None, description="Active language filter"),
|
|
129
|
+
framework: str | None = Query(None, description="Active framework filter"),
|
|
130
|
+
pattern: str | None = Query(None, description="Active pattern filter"),
|
|
131
|
+
tag: str | None = Query(None, description="Active tag filter"),
|
|
132
|
+
):
|
|
133
|
+
if q:
|
|
134
|
+
# Get search results first, then compute facets from those
|
|
135
|
+
from code_explore.search.hybrid import search as hybrid_search
|
|
136
|
+
results = hybrid_search(q, limit=500)
|
|
137
|
+
projects = [r.project for r in results]
|
|
138
|
+
else:
|
|
139
|
+
projects = get_all_projects()
|
|
140
|
+
|
|
141
|
+
# Apply active filters to scope facets
|
|
142
|
+
projects = filter_projects(projects, language=language, framework=framework, pattern=pattern, tag=tag)
|
|
143
|
+
|
|
144
|
+
return compute_facets(projects)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.get("/api/tags", response_model=TagsResponse)
|
|
148
|
+
async def get_tags(
|
|
149
|
+
category: str | None = Query(None, description="Filter by category: domain, technology-role, maturity"),
|
|
150
|
+
):
|
|
151
|
+
projects = get_all_projects()
|
|
152
|
+
|
|
153
|
+
tag_counts: Counter[str] = Counter()
|
|
154
|
+
tag_categories: dict[str, str] = {}
|
|
155
|
+
|
|
156
|
+
for p in projects:
|
|
157
|
+
for t in p.ai_tags:
|
|
158
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
159
|
+
if category and cat != category:
|
|
160
|
+
continue
|
|
161
|
+
tag_counts[t.value] += 1
|
|
162
|
+
tag_categories[t.value] = cat
|
|
163
|
+
|
|
164
|
+
tags_list = [
|
|
165
|
+
TagInfo(value=value, category=tag_categories[value], count=count)
|
|
166
|
+
for value, count in tag_counts.most_common()
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
category_counts: Counter[str] = Counter()
|
|
170
|
+
for t in tags_list:
|
|
171
|
+
category_counts[t.category] += 1
|
|
172
|
+
|
|
173
|
+
return TagsResponse(
|
|
174
|
+
tags=tags_list,
|
|
175
|
+
total_tags=len(tags_list),
|
|
176
|
+
categories=dict(category_counts),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
114
180
|
@app.get("/api/stats", response_model=StatsResponse)
|
|
115
181
|
async def get_stats():
|
|
116
182
|
projects = get_all_projects()
|
|
@@ -118,6 +184,7 @@ async def get_stats():
|
|
|
118
184
|
languages: dict[str, int] = {}
|
|
119
185
|
frameworks: dict[str, int] = {}
|
|
120
186
|
patterns: dict[str, int] = {}
|
|
187
|
+
ai_tags: dict[str, int] = {}
|
|
121
188
|
statuses: dict[str, int] = {}
|
|
122
189
|
total_files = 0
|
|
123
190
|
total_lines = 0
|
|
@@ -129,6 +196,8 @@ async def get_stats():
|
|
|
129
196
|
frameworks[fw] = frameworks.get(fw, 0) + 1
|
|
130
197
|
for pat in p.patterns:
|
|
131
198
|
patterns[pat.name] = patterns.get(pat.name, 0) + 1
|
|
199
|
+
for t in p.ai_tags:
|
|
200
|
+
ai_tags[t.value] = ai_tags.get(t.value, 0) + 1
|
|
132
201
|
statuses[p.status.value] = statuses.get(p.status.value, 0) + 1
|
|
133
202
|
total_files += p.quality.total_files
|
|
134
203
|
total_lines += p.quality.total_lines
|
|
@@ -138,6 +207,7 @@ async def get_stats():
|
|
|
138
207
|
languages=dict(sorted(languages.items(), key=lambda x: x[1], reverse=True)),
|
|
139
208
|
frameworks=dict(sorted(frameworks.items(), key=lambda x: x[1], reverse=True)),
|
|
140
209
|
patterns=dict(sorted(patterns.items(), key=lambda x: x[1], reverse=True)),
|
|
210
|
+
ai_tags=dict(sorted(ai_tags.items(), key=lambda x: x[1], reverse=True)),
|
|
141
211
|
total_files=total_files,
|
|
142
212
|
total_lines=total_lines,
|
|
143
213
|
statuses=statuses,
|
|
@@ -195,3 +265,9 @@ async def trigger_scan(request: ScanRequest):
|
|
|
195
265
|
results.append(project)
|
|
196
266
|
|
|
197
267
|
return ScanResponse(scanned=len(results), projects=results)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# Mount static files for the dashboard (must be after API routes)
|
|
271
|
+
_static_dir = Path(__file__).parent.parent / "static"
|
|
272
|
+
if _static_dir.is_dir():
|
|
273
|
+
app.mount("/", StaticFiles(directory=str(_static_dir), html=True), name="static")
|
|
@@ -34,8 +34,11 @@ def scan(
|
|
|
34
34
|
path: str = typer.Argument(..., help="Root directory to scan for repositories"),
|
|
35
35
|
depth: int = typer.Option(4, "--depth", "-d", help="Maximum directory depth"),
|
|
36
36
|
force: bool = typer.Option(False, "--force", "-f", help="Re-scan existing projects"),
|
|
37
|
+
no_ai: bool = typer.Option(False, "--no-ai", help="Skip AI summaries and tags (requires Ollama)"),
|
|
38
|
+
no_embed: bool = typer.Option(False, "--no-embed", help="Skip vector embeddings"),
|
|
39
|
+
model: str | None = typer.Option(None, "--model", "-m", help="Ollama model name"),
|
|
37
40
|
) -> None:
|
|
38
|
-
"""Scan
|
|
41
|
+
"""Scan repositories, analyze, summarize, and index — all in one step."""
|
|
39
42
|
from code_explore.scanner.local import scan_local_repos
|
|
40
43
|
from code_explore.scanner.git_info import extract_git_info, get_git_head
|
|
41
44
|
from code_explore.scanner.readme import read_readme, list_key_files
|
|
@@ -51,7 +54,16 @@ def scan(
|
|
|
51
54
|
console.print(f"[red]Error:[/red] Path does not exist: {root}")
|
|
52
55
|
raise typer.Exit(1)
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
steps = ["scan", "analyze"]
|
|
58
|
+
if not no_ai:
|
|
59
|
+
steps.append("summarize + tag")
|
|
60
|
+
if not no_embed:
|
|
61
|
+
steps.append("embed")
|
|
62
|
+
console.print(Panel(
|
|
63
|
+
f"Scanning [bold cyan]{root}[/bold cyan] (depth={depth})\n"
|
|
64
|
+
f"Steps: {' → '.join(steps)}",
|
|
65
|
+
title="Code Explore",
|
|
66
|
+
))
|
|
55
67
|
|
|
56
68
|
repos = asyncio.run(scan_local_repos(root, max_depth=depth))
|
|
57
69
|
|
|
@@ -60,6 +72,7 @@ def scan(
|
|
|
60
72
|
raise typer.Exit(0)
|
|
61
73
|
|
|
62
74
|
results: list[Project] = []
|
|
75
|
+
new_or_changed: list[Project] = []
|
|
63
76
|
|
|
64
77
|
with Progress(
|
|
65
78
|
SpinnerColumn(),
|
|
@@ -68,6 +81,7 @@ def scan(
|
|
|
68
81
|
MofNCompleteColumn(),
|
|
69
82
|
console=console,
|
|
70
83
|
) as progress:
|
|
84
|
+
# Phase 1: Scan & analyze
|
|
71
85
|
task = progress.add_task("Analyzing repositories...", total=len(repos))
|
|
72
86
|
|
|
73
87
|
for repo_path in repos:
|
|
@@ -117,8 +131,55 @@ def scan(
|
|
|
117
131
|
|
|
118
132
|
save_project(project)
|
|
119
133
|
results.append(project)
|
|
134
|
+
new_or_changed.append(project)
|
|
120
135
|
progress.update(task, advance=1)
|
|
121
136
|
|
|
137
|
+
# Phase 2: AI summaries & tags (all projects that need them)
|
|
138
|
+
if not no_ai:
|
|
139
|
+
from code_explore.summarizer.ollama import summarize_project
|
|
140
|
+
|
|
141
|
+
ai_candidates = [p for p in results if not p.summary or not p.ai_tags]
|
|
142
|
+
if ai_candidates:
|
|
143
|
+
ai_task = progress.add_task("AI summaries & tags...", total=len(ai_candidates))
|
|
144
|
+
summarized = 0
|
|
145
|
+
tagged = 0
|
|
146
|
+
|
|
147
|
+
for project in ai_candidates:
|
|
148
|
+
progress.update(ai_task, description=f"Summarizing [cyan]{project.name}[/cyan]")
|
|
149
|
+
needs_summary = not project.summary
|
|
150
|
+
needs_tags = not project.ai_tags
|
|
151
|
+
|
|
152
|
+
if model:
|
|
153
|
+
summary, tags, concepts, ai_tags = summarize_project(project, model=model)
|
|
154
|
+
else:
|
|
155
|
+
summary, tags, concepts, ai_tags = summarize_project(project)
|
|
156
|
+
|
|
157
|
+
if summary and needs_summary:
|
|
158
|
+
project.summary = summary
|
|
159
|
+
project.tags = tags
|
|
160
|
+
project.concepts = concepts
|
|
161
|
+
summarized += 1
|
|
162
|
+
if ai_tags and needs_tags:
|
|
163
|
+
project.ai_tags = ai_tags
|
|
164
|
+
tagged += 1
|
|
165
|
+
if summary or ai_tags:
|
|
166
|
+
save_project(project)
|
|
167
|
+
progress.update(ai_task, advance=1)
|
|
168
|
+
|
|
169
|
+
# Phase 3: Embeddings (all projects)
|
|
170
|
+
if not no_embed:
|
|
171
|
+
from code_explore.indexer.embeddings import index_project as embed_project
|
|
172
|
+
from datetime import datetime
|
|
173
|
+
|
|
174
|
+
embed_task = progress.add_task("Generating embeddings...", total=len(results))
|
|
175
|
+
for project in results:
|
|
176
|
+
progress.update(embed_task, description=f"Embedding [cyan]{project.name}[/cyan]")
|
|
177
|
+
embed_project(project)
|
|
178
|
+
project.status = ProjectStatus.INDEXED
|
|
179
|
+
project.indexed_at = datetime.now()
|
|
180
|
+
save_project(project)
|
|
181
|
+
progress.update(embed_task, advance=1)
|
|
182
|
+
|
|
122
183
|
table = Table(title=f"Scanned {len(results)} Projects")
|
|
123
184
|
table.add_column("Name", style="cyan", no_wrap=True)
|
|
124
185
|
table.add_column("Language", style="green")
|
|
@@ -145,9 +206,14 @@ def search(
|
|
|
145
206
|
query: str = typer.Argument(..., help="Search query"),
|
|
146
207
|
mode: str = typer.Option("hybrid", "--mode", "-m", help="Search mode: fulltext, semantic, or hybrid"),
|
|
147
208
|
limit: int = typer.Option(None, "--limit", "-l", help="Maximum results"),
|
|
209
|
+
language: str = typer.Option(None, "--language", "-L", help="Filter by primary language"),
|
|
210
|
+
framework: str = typer.Option(None, "--framework", "-F", help="Filter by framework"),
|
|
211
|
+
pattern: str = typer.Option(None, "--pattern", help="Filter by architectural pattern"),
|
|
212
|
+
tag: str = typer.Option(None, "--tag", "-t", help="Filter by AI-generated tag"),
|
|
148
213
|
) -> None:
|
|
149
214
|
"""Search across all indexed projects."""
|
|
150
215
|
from code_explore.config import get_config
|
|
216
|
+
from code_explore.search.filters import apply_filters
|
|
151
217
|
|
|
152
218
|
if limit is None:
|
|
153
219
|
limit = get_config().result_limit
|
|
@@ -163,6 +229,23 @@ def search(
|
|
|
163
229
|
from code_explore.search.hybrid import search as hybrid_search
|
|
164
230
|
results = hybrid_search(query, limit=limit)
|
|
165
231
|
|
|
232
|
+
# Apply post-filters
|
|
233
|
+
results = apply_filters(results, language=language, framework=framework, pattern=pattern, tag=tag)
|
|
234
|
+
|
|
235
|
+
# Show active filters
|
|
236
|
+
active_filters = []
|
|
237
|
+
if language:
|
|
238
|
+
active_filters.append(f"language={language}")
|
|
239
|
+
if framework:
|
|
240
|
+
active_filters.append(f"framework={framework}")
|
|
241
|
+
if pattern:
|
|
242
|
+
active_filters.append(f"pattern={pattern}")
|
|
243
|
+
if tag:
|
|
244
|
+
active_filters.append(f"tag={tag}")
|
|
245
|
+
|
|
246
|
+
if active_filters:
|
|
247
|
+
console.print(f"[dim]Filters: {', '.join(active_filters)}[/dim]")
|
|
248
|
+
|
|
166
249
|
if not results:
|
|
167
250
|
console.print(f"[yellow]No results found for:[/yellow] {query}")
|
|
168
251
|
raise typer.Exit(0)
|
|
@@ -282,6 +365,16 @@ def show(
|
|
|
282
365
|
if project.concepts:
|
|
283
366
|
tree.add(f"[bold]Concepts:[/bold] {', '.join(project.concepts)}")
|
|
284
367
|
|
|
368
|
+
# AI Tags grouped by category
|
|
369
|
+
if project.ai_tags:
|
|
370
|
+
ai_branch = tree.add("[bold]AI Tags[/bold]")
|
|
371
|
+
by_category: dict[str, list[str]] = {}
|
|
372
|
+
for t in project.ai_tags:
|
|
373
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
374
|
+
by_category.setdefault(cat, []).append(t.value)
|
|
375
|
+
for cat, values in sorted(by_category.items()):
|
|
376
|
+
ai_branch.add(f"[magenta]{cat}:[/magenta] {', '.join(values)}")
|
|
377
|
+
|
|
285
378
|
console.print(Panel(tree, title=f"Project: {project.name}", border_style="cyan"))
|
|
286
379
|
|
|
287
380
|
|
|
@@ -289,7 +382,7 @@ def show(
|
|
|
289
382
|
def index(
|
|
290
383
|
model: str | None = typer.Option(None, "--model", "-m", help="Ollama model name for summarization"),
|
|
291
384
|
) -> None:
|
|
292
|
-
"""
|
|
385
|
+
"""Re-generate AI summaries and embeddings for existing projects."""
|
|
293
386
|
from code_explore.indexer.embeddings import index_project as embed_project, index_all_projects
|
|
294
387
|
from code_explore.summarizer.ollama import summarize_project
|
|
295
388
|
|
|
@@ -309,22 +402,30 @@ def index(
|
|
|
309
402
|
MofNCompleteColumn(),
|
|
310
403
|
console=console,
|
|
311
404
|
) as progress:
|
|
312
|
-
summary_task = progress.add_task("Generating summaries...", total=len(projects))
|
|
405
|
+
summary_task = progress.add_task("Generating summaries & AI tags...", total=len(projects))
|
|
313
406
|
summarized = 0
|
|
407
|
+
tagged = 0
|
|
314
408
|
|
|
315
409
|
for project in projects:
|
|
316
|
-
|
|
410
|
+
needs_summary = not project.summary
|
|
411
|
+
needs_tags = not project.ai_tags
|
|
412
|
+
|
|
413
|
+
if needs_summary or needs_tags:
|
|
317
414
|
progress.update(summary_task, description=f"Summarizing [cyan]{project.name}[/cyan]")
|
|
318
415
|
if model:
|
|
319
|
-
summary, tags, concepts = summarize_project(project, model=model)
|
|
416
|
+
summary, tags, concepts, ai_tags = summarize_project(project, model=model)
|
|
320
417
|
else:
|
|
321
|
-
summary, tags, concepts = summarize_project(project)
|
|
322
|
-
if summary:
|
|
418
|
+
summary, tags, concepts, ai_tags = summarize_project(project)
|
|
419
|
+
if summary and needs_summary:
|
|
323
420
|
project.summary = summary
|
|
324
421
|
project.tags = tags
|
|
325
422
|
project.concepts = concepts
|
|
326
|
-
save_project(project)
|
|
327
423
|
summarized += 1
|
|
424
|
+
if ai_tags:
|
|
425
|
+
project.ai_tags = ai_tags
|
|
426
|
+
tagged += 1
|
|
427
|
+
if summary or ai_tags:
|
|
428
|
+
save_project(project)
|
|
328
429
|
progress.update(summary_task, advance=1)
|
|
329
430
|
|
|
330
431
|
embed_task = progress.add_task("Generating embeddings...", total=len(projects))
|
|
@@ -340,7 +441,62 @@ def index(
|
|
|
340
441
|
indexed += 1
|
|
341
442
|
progress.update(embed_task, advance=1)
|
|
342
443
|
|
|
343
|
-
console.print(f"[green]Summarized {summarized}
|
|
444
|
+
console.print(f"[green]Summarized {summarized}, AI-tagged {tagged}, indexed {indexed} projects.[/green]")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@app.command()
|
|
448
|
+
def tags(
|
|
449
|
+
category: str = typer.Option(None, "--category", "-c", help="Filter by category: domain, technology-role, maturity"),
|
|
450
|
+
) -> None:
|
|
451
|
+
"""List all unique AI tags across projects with counts."""
|
|
452
|
+
init_db()
|
|
453
|
+
projects = get_all_projects()
|
|
454
|
+
|
|
455
|
+
if not projects:
|
|
456
|
+
console.print("[yellow]No projects found. Run 'scan' first.[/yellow]")
|
|
457
|
+
raise typer.Exit(0)
|
|
458
|
+
|
|
459
|
+
# Collect all AI tags with counts
|
|
460
|
+
tag_counts: Counter[str] = Counter()
|
|
461
|
+
tag_categories: dict[str, str] = {}
|
|
462
|
+
|
|
463
|
+
for p in projects:
|
|
464
|
+
for t in p.ai_tags:
|
|
465
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
466
|
+
if category and cat != category:
|
|
467
|
+
continue
|
|
468
|
+
tag_counts[t.value] += 1
|
|
469
|
+
tag_categories[t.value] = cat
|
|
470
|
+
|
|
471
|
+
if not tag_counts:
|
|
472
|
+
if category:
|
|
473
|
+
console.print(f"[yellow]No AI tags found for category '{category}'.[/yellow]")
|
|
474
|
+
else:
|
|
475
|
+
console.print("[yellow]No AI tags found. Run 'cex index' to generate tags.[/yellow]")
|
|
476
|
+
raise typer.Exit(0)
|
|
477
|
+
|
|
478
|
+
# Group by category
|
|
479
|
+
by_category: dict[str, list[tuple[str, int]]] = {}
|
|
480
|
+
for tag_value, count in tag_counts.most_common():
|
|
481
|
+
cat = tag_categories[tag_value]
|
|
482
|
+
by_category.setdefault(cat, []).append((tag_value, count))
|
|
483
|
+
|
|
484
|
+
total_tags = len(tag_counts)
|
|
485
|
+
total_projects = len(projects)
|
|
486
|
+
console.print(Panel(
|
|
487
|
+
f"[bold]{total_tags}[/bold] unique AI tags across [bold]{total_projects}[/bold] projects",
|
|
488
|
+
title="AI Tags",
|
|
489
|
+
border_style="magenta",
|
|
490
|
+
))
|
|
491
|
+
|
|
492
|
+
for cat in sorted(by_category.keys()):
|
|
493
|
+
items = by_category[cat]
|
|
494
|
+
table = Table(title=f"{cat} tags")
|
|
495
|
+
table.add_column("Tag", style="magenta")
|
|
496
|
+
table.add_column("Projects", justify="right", style="yellow")
|
|
497
|
+
for tag_value, count in items:
|
|
498
|
+
table.add_row(tag_value, str(count))
|
|
499
|
+
console.print(table)
|
|
344
500
|
|
|
345
501
|
|
|
346
502
|
@app.command()
|
|
@@ -348,6 +504,7 @@ def update(
|
|
|
348
504
|
force: bool = typer.Option(False, "--force", "-f", help="Re-analyze even if git HEAD unchanged"),
|
|
349
505
|
reindex: bool = typer.Option(False, "--reindex", help="Regenerate embeddings for updated projects"),
|
|
350
506
|
resummarize: bool = typer.Option(False, "--resummarize", help="Regenerate AI summaries for updated projects"),
|
|
507
|
+
retag: bool = typer.Option(False, "--retag", help="Regenerate AI tags for updated projects"),
|
|
351
508
|
) -> None:
|
|
352
509
|
"""Update existing projects by re-analyzing changed repositories."""
|
|
353
510
|
from datetime import datetime
|
|
@@ -433,20 +590,25 @@ def update(
|
|
|
433
590
|
updated += 1
|
|
434
591
|
progress.update(task, advance=1)
|
|
435
592
|
|
|
436
|
-
if resummarize and updated_projects:
|
|
593
|
+
if (resummarize or retag) and updated_projects:
|
|
437
594
|
from code_explore.summarizer.ollama import summarize_project
|
|
438
595
|
|
|
439
596
|
summary_task = progress.add_task("Resummarizing...", total=len(updated_projects))
|
|
440
597
|
for project in updated_projects:
|
|
441
598
|
progress.update(summary_task, description=f"Summarizing [cyan]{project.name}[/cyan]")
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if
|
|
599
|
+
if resummarize:
|
|
600
|
+
project.summary = None
|
|
601
|
+
project.tags = []
|
|
602
|
+
project.concepts = []
|
|
603
|
+
if retag:
|
|
604
|
+
project.ai_tags = []
|
|
605
|
+
summary, tags, concepts, ai_tags = summarize_project(project)
|
|
606
|
+
if summary and resummarize:
|
|
447
607
|
project.summary = summary
|
|
448
608
|
project.tags = tags
|
|
449
609
|
project.concepts = concepts
|
|
610
|
+
if ai_tags and retag:
|
|
611
|
+
project.ai_tags = ai_tags
|
|
450
612
|
save_project(project)
|
|
451
613
|
progress.update(summary_task, advance=1)
|
|
452
614
|
|
|
@@ -547,12 +709,13 @@ def serve(
|
|
|
547
709
|
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Bind host"),
|
|
548
710
|
port: int = typer.Option(8000, "--port", "-p", help="Bind port"),
|
|
549
711
|
) -> None:
|
|
550
|
-
"""Start the FastAPI server."""
|
|
712
|
+
"""Start the FastAPI server with web dashboard."""
|
|
551
713
|
import uvicorn
|
|
552
714
|
|
|
553
715
|
init_db()
|
|
554
716
|
console.print(Panel(
|
|
555
717
|
f"Starting server on [bold cyan]http://{host}:{port}[/bold cyan]\n"
|
|
718
|
+
f"Dashboard at [bold cyan]http://{host}:{port}[/bold cyan]\n"
|
|
556
719
|
f"API docs at [bold cyan]http://{host}:{port}/docs[/bold cyan]",
|
|
557
720
|
title="Code Explore API",
|
|
558
721
|
))
|
|
@@ -110,7 +110,10 @@ def init_db(db_path: Path | None = None) -> None:
|
|
|
110
110
|
def save_project(project: Project, db_path: Path | None = None) -> None:
|
|
111
111
|
conn = get_connection(db_path)
|
|
112
112
|
now = datetime.now().isoformat()
|
|
113
|
-
|
|
113
|
+
tag_parts = list(project.tags) if project.tags else []
|
|
114
|
+
if project.ai_tags:
|
|
115
|
+
tag_parts.extend(t.value for t in project.ai_tags)
|
|
116
|
+
tags_str = ", ".join(tag_parts)
|
|
114
117
|
|
|
115
118
|
readme_str = (project.readme_snippet or "")[:2000]
|
|
116
119
|
|
|
@@ -93,6 +93,11 @@ def _project_to_text(project: Project) -> str:
|
|
|
93
93
|
if project.concepts:
|
|
94
94
|
parts.append(f"Concepts: {', '.join(project.concepts)}")
|
|
95
95
|
|
|
96
|
+
# AI classification tags
|
|
97
|
+
if project.ai_tags:
|
|
98
|
+
ai_tag_values = [t.value for t in project.ai_tags]
|
|
99
|
+
parts.append(f"AI Tags: {', '.join(ai_tag_values)}")
|
|
100
|
+
|
|
96
101
|
# Language names
|
|
97
102
|
languages = [lang.name for lang in project.languages]
|
|
98
103
|
if languages:
|
|
@@ -20,6 +20,18 @@ class ProjectStatus(str, Enum):
|
|
|
20
20
|
ERROR = "error"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class TagCategory(str, Enum):
|
|
24
|
+
DOMAIN = "domain"
|
|
25
|
+
TECHNOLOGY_ROLE = "technology-role"
|
|
26
|
+
MATURITY = "maturity"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AiTag(BaseModel):
|
|
30
|
+
value: str
|
|
31
|
+
category: TagCategory = TagCategory.DOMAIN
|
|
32
|
+
confidence: float = 0.8
|
|
33
|
+
|
|
34
|
+
|
|
23
35
|
class LanguageInfo(BaseModel):
|
|
24
36
|
name: str
|
|
25
37
|
files: int = 0
|
|
@@ -82,6 +94,7 @@ class Project(BaseModel):
|
|
|
82
94
|
summary: str | None = None
|
|
83
95
|
tags: list[str] = Field(default_factory=list)
|
|
84
96
|
concepts: list[str] = Field(default_factory=list)
|
|
97
|
+
ai_tags: list[AiTag] = Field(default_factory=list)
|
|
85
98
|
|
|
86
99
|
readme_snippet: str | None = None
|
|
87
100
|
key_files: list[str] = Field(default_factory=list)
|
|
@@ -104,3 +117,15 @@ class SearchQuery(BaseModel):
|
|
|
104
117
|
mode: str = "hybrid"
|
|
105
118
|
limit: int = 20
|
|
106
119
|
filters: dict = Field(default_factory=dict)
|
|
120
|
+
language: str | None = None
|
|
121
|
+
framework: str | None = None
|
|
122
|
+
pattern: str | None = None
|
|
123
|
+
tag: str | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SearchFacets(BaseModel):
|
|
127
|
+
languages: dict[str, int] = Field(default_factory=dict)
|
|
128
|
+
frameworks: dict[str, int] = Field(default_factory=dict)
|
|
129
|
+
patterns: dict[str, int] = Field(default_factory=dict)
|
|
130
|
+
tags: dict[str, int] = Field(default_factory=dict)
|
|
131
|
+
total: int = 0
|