code-explore 0.2.0__tar.gz → 0.3.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.3.0/.github/workflows/pages.yml +44 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/PKG-INFO +1 -1
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/api/main.py +93 -17
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/cli/main.py +116 -14
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/database.py +4 -1
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/indexer/embeddings.py +5 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/models.py +25 -0
- code_explore-0.3.0/code_explore/search/filters.py +113 -0
- code_explore-0.3.0/code_explore/static/app.js +482 -0
- code_explore-0.3.0/code_explore/static/index.html +78 -0
- code_explore-0.3.0/code_explore/static/style.css +349 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/summarizer/ollama.py +21 -10
- code_explore-0.3.0/code_explore/tagger/__init__.py +56 -0
- code_explore-0.3.0/docs/CNAME +1 -0
- code_explore-0.3.0/docs/index.html +93 -0
- code_explore-0.3.0/install.cmd +58 -0
- code_explore-0.3.0/install.ps1 +84 -0
- code_explore-0.3.0/install.sh +110 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/pyproject.toml +7 -1
- {code_explore-0.2.0 → code_explore-0.3.0}/.editorconfig +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/.github/workflows/publish.yml +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/.gitignore +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/README.md +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/analyzer/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/analyzer/dependencies.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/analyzer/language.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/analyzer/metrics.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/analyzer/patterns.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/api/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/cli/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/cli/config_cmd.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/config.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/indexer/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/scanner/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/scanner/git_info.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/scanner/local.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/scanner/readme.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/search/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/search/fulltext.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/search/hybrid.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/search/semantic.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/code_explore/summarizer/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/tests/__init__.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/tests/conftest.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/tests/test_cli.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/tests/test_config.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/tests/test_config_cli.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/tests/test_database.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.0}/tests/test_models.py +0 -0
- {code_explore-0.2.0 → code_explore-0.3.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.3.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.3.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")
|
|
@@ -145,9 +145,14 @@ def search(
|
|
|
145
145
|
query: str = typer.Argument(..., help="Search query"),
|
|
146
146
|
mode: str = typer.Option("hybrid", "--mode", "-m", help="Search mode: fulltext, semantic, or hybrid"),
|
|
147
147
|
limit: int = typer.Option(None, "--limit", "-l", help="Maximum results"),
|
|
148
|
+
language: str = typer.Option(None, "--language", "-L", help="Filter by primary language"),
|
|
149
|
+
framework: str = typer.Option(None, "--framework", "-F", help="Filter by framework"),
|
|
150
|
+
pattern: str = typer.Option(None, "--pattern", help="Filter by architectural pattern"),
|
|
151
|
+
tag: str = typer.Option(None, "--tag", "-t", help="Filter by AI-generated tag"),
|
|
148
152
|
) -> None:
|
|
149
153
|
"""Search across all indexed projects."""
|
|
150
154
|
from code_explore.config import get_config
|
|
155
|
+
from code_explore.search.filters import apply_filters
|
|
151
156
|
|
|
152
157
|
if limit is None:
|
|
153
158
|
limit = get_config().result_limit
|
|
@@ -163,6 +168,23 @@ def search(
|
|
|
163
168
|
from code_explore.search.hybrid import search as hybrid_search
|
|
164
169
|
results = hybrid_search(query, limit=limit)
|
|
165
170
|
|
|
171
|
+
# Apply post-filters
|
|
172
|
+
results = apply_filters(results, language=language, framework=framework, pattern=pattern, tag=tag)
|
|
173
|
+
|
|
174
|
+
# Show active filters
|
|
175
|
+
active_filters = []
|
|
176
|
+
if language:
|
|
177
|
+
active_filters.append(f"language={language}")
|
|
178
|
+
if framework:
|
|
179
|
+
active_filters.append(f"framework={framework}")
|
|
180
|
+
if pattern:
|
|
181
|
+
active_filters.append(f"pattern={pattern}")
|
|
182
|
+
if tag:
|
|
183
|
+
active_filters.append(f"tag={tag}")
|
|
184
|
+
|
|
185
|
+
if active_filters:
|
|
186
|
+
console.print(f"[dim]Filters: {', '.join(active_filters)}[/dim]")
|
|
187
|
+
|
|
166
188
|
if not results:
|
|
167
189
|
console.print(f"[yellow]No results found for:[/yellow] {query}")
|
|
168
190
|
raise typer.Exit(0)
|
|
@@ -282,6 +304,16 @@ def show(
|
|
|
282
304
|
if project.concepts:
|
|
283
305
|
tree.add(f"[bold]Concepts:[/bold] {', '.join(project.concepts)}")
|
|
284
306
|
|
|
307
|
+
# AI Tags grouped by category
|
|
308
|
+
if project.ai_tags:
|
|
309
|
+
ai_branch = tree.add("[bold]AI Tags[/bold]")
|
|
310
|
+
by_category: dict[str, list[str]] = {}
|
|
311
|
+
for t in project.ai_tags:
|
|
312
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
313
|
+
by_category.setdefault(cat, []).append(t.value)
|
|
314
|
+
for cat, values in sorted(by_category.items()):
|
|
315
|
+
ai_branch.add(f"[magenta]{cat}:[/magenta] {', '.join(values)}")
|
|
316
|
+
|
|
285
317
|
console.print(Panel(tree, title=f"Project: {project.name}", border_style="cyan"))
|
|
286
318
|
|
|
287
319
|
|
|
@@ -309,22 +341,30 @@ def index(
|
|
|
309
341
|
MofNCompleteColumn(),
|
|
310
342
|
console=console,
|
|
311
343
|
) as progress:
|
|
312
|
-
summary_task = progress.add_task("Generating summaries...", total=len(projects))
|
|
344
|
+
summary_task = progress.add_task("Generating summaries & AI tags...", total=len(projects))
|
|
313
345
|
summarized = 0
|
|
346
|
+
tagged = 0
|
|
314
347
|
|
|
315
348
|
for project in projects:
|
|
316
|
-
|
|
349
|
+
needs_summary = not project.summary
|
|
350
|
+
needs_tags = not project.ai_tags
|
|
351
|
+
|
|
352
|
+
if needs_summary or needs_tags:
|
|
317
353
|
progress.update(summary_task, description=f"Summarizing [cyan]{project.name}[/cyan]")
|
|
318
354
|
if model:
|
|
319
|
-
summary, tags, concepts = summarize_project(project, model=model)
|
|
355
|
+
summary, tags, concepts, ai_tags = summarize_project(project, model=model)
|
|
320
356
|
else:
|
|
321
|
-
summary, tags, concepts = summarize_project(project)
|
|
322
|
-
if summary:
|
|
357
|
+
summary, tags, concepts, ai_tags = summarize_project(project)
|
|
358
|
+
if summary and needs_summary:
|
|
323
359
|
project.summary = summary
|
|
324
360
|
project.tags = tags
|
|
325
361
|
project.concepts = concepts
|
|
326
|
-
save_project(project)
|
|
327
362
|
summarized += 1
|
|
363
|
+
if ai_tags:
|
|
364
|
+
project.ai_tags = ai_tags
|
|
365
|
+
tagged += 1
|
|
366
|
+
if summary or ai_tags:
|
|
367
|
+
save_project(project)
|
|
328
368
|
progress.update(summary_task, advance=1)
|
|
329
369
|
|
|
330
370
|
embed_task = progress.add_task("Generating embeddings...", total=len(projects))
|
|
@@ -340,7 +380,62 @@ def index(
|
|
|
340
380
|
indexed += 1
|
|
341
381
|
progress.update(embed_task, advance=1)
|
|
342
382
|
|
|
343
|
-
console.print(f"[green]Summarized {summarized}
|
|
383
|
+
console.print(f"[green]Summarized {summarized}, AI-tagged {tagged}, indexed {indexed} projects.[/green]")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.command()
|
|
387
|
+
def tags(
|
|
388
|
+
category: str = typer.Option(None, "--category", "-c", help="Filter by category: domain, technology-role, maturity"),
|
|
389
|
+
) -> None:
|
|
390
|
+
"""List all unique AI tags across projects with counts."""
|
|
391
|
+
init_db()
|
|
392
|
+
projects = get_all_projects()
|
|
393
|
+
|
|
394
|
+
if not projects:
|
|
395
|
+
console.print("[yellow]No projects found. Run 'scan' first.[/yellow]")
|
|
396
|
+
raise typer.Exit(0)
|
|
397
|
+
|
|
398
|
+
# Collect all AI tags with counts
|
|
399
|
+
tag_counts: Counter[str] = Counter()
|
|
400
|
+
tag_categories: dict[str, str] = {}
|
|
401
|
+
|
|
402
|
+
for p in projects:
|
|
403
|
+
for t in p.ai_tags:
|
|
404
|
+
cat = t.category.value if hasattr(t.category, "value") else str(t.category)
|
|
405
|
+
if category and cat != category:
|
|
406
|
+
continue
|
|
407
|
+
tag_counts[t.value] += 1
|
|
408
|
+
tag_categories[t.value] = cat
|
|
409
|
+
|
|
410
|
+
if not tag_counts:
|
|
411
|
+
if category:
|
|
412
|
+
console.print(f"[yellow]No AI tags found for category '{category}'.[/yellow]")
|
|
413
|
+
else:
|
|
414
|
+
console.print("[yellow]No AI tags found. Run 'cex index' to generate tags.[/yellow]")
|
|
415
|
+
raise typer.Exit(0)
|
|
416
|
+
|
|
417
|
+
# Group by category
|
|
418
|
+
by_category: dict[str, list[tuple[str, int]]] = {}
|
|
419
|
+
for tag_value, count in tag_counts.most_common():
|
|
420
|
+
cat = tag_categories[tag_value]
|
|
421
|
+
by_category.setdefault(cat, []).append((tag_value, count))
|
|
422
|
+
|
|
423
|
+
total_tags = len(tag_counts)
|
|
424
|
+
total_projects = len(projects)
|
|
425
|
+
console.print(Panel(
|
|
426
|
+
f"[bold]{total_tags}[/bold] unique AI tags across [bold]{total_projects}[/bold] projects",
|
|
427
|
+
title="AI Tags",
|
|
428
|
+
border_style="magenta",
|
|
429
|
+
))
|
|
430
|
+
|
|
431
|
+
for cat in sorted(by_category.keys()):
|
|
432
|
+
items = by_category[cat]
|
|
433
|
+
table = Table(title=f"{cat} tags")
|
|
434
|
+
table.add_column("Tag", style="magenta")
|
|
435
|
+
table.add_column("Projects", justify="right", style="yellow")
|
|
436
|
+
for tag_value, count in items:
|
|
437
|
+
table.add_row(tag_value, str(count))
|
|
438
|
+
console.print(table)
|
|
344
439
|
|
|
345
440
|
|
|
346
441
|
@app.command()
|
|
@@ -348,6 +443,7 @@ def update(
|
|
|
348
443
|
force: bool = typer.Option(False, "--force", "-f", help="Re-analyze even if git HEAD unchanged"),
|
|
349
444
|
reindex: bool = typer.Option(False, "--reindex", help="Regenerate embeddings for updated projects"),
|
|
350
445
|
resummarize: bool = typer.Option(False, "--resummarize", help="Regenerate AI summaries for updated projects"),
|
|
446
|
+
retag: bool = typer.Option(False, "--retag", help="Regenerate AI tags for updated projects"),
|
|
351
447
|
) -> None:
|
|
352
448
|
"""Update existing projects by re-analyzing changed repositories."""
|
|
353
449
|
from datetime import datetime
|
|
@@ -433,20 +529,25 @@ def update(
|
|
|
433
529
|
updated += 1
|
|
434
530
|
progress.update(task, advance=1)
|
|
435
531
|
|
|
436
|
-
if resummarize and updated_projects:
|
|
532
|
+
if (resummarize or retag) and updated_projects:
|
|
437
533
|
from code_explore.summarizer.ollama import summarize_project
|
|
438
534
|
|
|
439
535
|
summary_task = progress.add_task("Resummarizing...", total=len(updated_projects))
|
|
440
536
|
for project in updated_projects:
|
|
441
537
|
progress.update(summary_task, description=f"Summarizing [cyan]{project.name}[/cyan]")
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if
|
|
538
|
+
if resummarize:
|
|
539
|
+
project.summary = None
|
|
540
|
+
project.tags = []
|
|
541
|
+
project.concepts = []
|
|
542
|
+
if retag:
|
|
543
|
+
project.ai_tags = []
|
|
544
|
+
summary, tags, concepts, ai_tags = summarize_project(project)
|
|
545
|
+
if summary and resummarize:
|
|
447
546
|
project.summary = summary
|
|
448
547
|
project.tags = tags
|
|
449
548
|
project.concepts = concepts
|
|
549
|
+
if ai_tags and retag:
|
|
550
|
+
project.ai_tags = ai_tags
|
|
450
551
|
save_project(project)
|
|
451
552
|
progress.update(summary_task, advance=1)
|
|
452
553
|
|
|
@@ -547,12 +648,13 @@ def serve(
|
|
|
547
648
|
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Bind host"),
|
|
548
649
|
port: int = typer.Option(8000, "--port", "-p", help="Bind port"),
|
|
549
650
|
) -> None:
|
|
550
|
-
"""Start the FastAPI server."""
|
|
651
|
+
"""Start the FastAPI server with web dashboard."""
|
|
551
652
|
import uvicorn
|
|
552
653
|
|
|
553
654
|
init_db()
|
|
554
655
|
console.print(Panel(
|
|
555
656
|
f"Starting server on [bold cyan]http://{host}:{port}[/bold cyan]\n"
|
|
657
|
+
f"Dashboard at [bold cyan]http://{host}:{port}[/bold cyan]\n"
|
|
556
658
|
f"API docs at [bold cyan]http://{host}:{port}/docs[/bold cyan]",
|
|
557
659
|
title="Code Explore API",
|
|
558
660
|
))
|
|
@@ -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
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Shared filter logic for faceted search."""
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
|
|
5
|
+
from code_explore.models import Project, SearchFacets, SearchResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def apply_filters(
|
|
9
|
+
results: list[SearchResult],
|
|
10
|
+
language: str | None = None,
|
|
11
|
+
framework: str | None = None,
|
|
12
|
+
pattern: str | None = None,
|
|
13
|
+
tag: str | None = None,
|
|
14
|
+
) -> list[SearchResult]:
|
|
15
|
+
"""Apply post-filters to search results. Multiple filters combine with AND logic."""
|
|
16
|
+
filtered = results
|
|
17
|
+
|
|
18
|
+
if language:
|
|
19
|
+
lang_lower = language.lower()
|
|
20
|
+
filtered = [
|
|
21
|
+
r for r in filtered
|
|
22
|
+
if r.project.primary_language and r.project.primary_language.lower() == lang_lower
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
if framework:
|
|
26
|
+
fw_lower = framework.lower()
|
|
27
|
+
filtered = [
|
|
28
|
+
r for r in filtered
|
|
29
|
+
if any(f.lower() == fw_lower for f in r.project.frameworks)
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
if pattern:
|
|
33
|
+
pat_lower = pattern.lower()
|
|
34
|
+
filtered = [
|
|
35
|
+
r for r in filtered
|
|
36
|
+
if any(p.name.lower() == pat_lower for p in r.project.patterns)
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
if tag:
|
|
40
|
+
tag_lower = tag.lower()
|
|
41
|
+
filtered = [
|
|
42
|
+
r for r in filtered
|
|
43
|
+
if any(t.value.lower() == tag_lower for t in r.project.ai_tags)
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
return filtered
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def filter_projects(
|
|
50
|
+
projects: list[Project],
|
|
51
|
+
language: str | None = None,
|
|
52
|
+
framework: str | None = None,
|
|
53
|
+
pattern: str | None = None,
|
|
54
|
+
tag: str | None = None,
|
|
55
|
+
) -> list[Project]:
|
|
56
|
+
"""Apply filters directly to a list of projects."""
|
|
57
|
+
filtered = projects
|
|
58
|
+
|
|
59
|
+
if language:
|
|
60
|
+
lang_lower = language.lower()
|
|
61
|
+
filtered = [
|
|
62
|
+
p for p in filtered
|
|
63
|
+
if p.primary_language and p.primary_language.lower() == lang_lower
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
if framework:
|
|
67
|
+
fw_lower = framework.lower()
|
|
68
|
+
filtered = [
|
|
69
|
+
p for p in filtered
|
|
70
|
+
if any(f.lower() == fw_lower for f in p.frameworks)
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
if pattern:
|
|
74
|
+
pat_lower = pattern.lower()
|
|
75
|
+
filtered = [
|
|
76
|
+
p for p in filtered
|
|
77
|
+
if any(pt.name.lower() == pat_lower for pt in p.patterns)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
if tag:
|
|
81
|
+
tag_lower = tag.lower()
|
|
82
|
+
filtered = [
|
|
83
|
+
p for p in filtered
|
|
84
|
+
if any(t.value.lower() == tag_lower for t in p.ai_tags)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
return filtered
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def compute_facets(projects: list[Project]) -> SearchFacets:
|
|
91
|
+
"""Compute facet counts from a list of projects."""
|
|
92
|
+
languages: Counter[str] = Counter()
|
|
93
|
+
frameworks: Counter[str] = Counter()
|
|
94
|
+
patterns: Counter[str] = Counter()
|
|
95
|
+
tags: Counter[str] = Counter()
|
|
96
|
+
|
|
97
|
+
for p in projects:
|
|
98
|
+
if p.primary_language:
|
|
99
|
+
languages[p.primary_language] += 1
|
|
100
|
+
for fw in p.frameworks:
|
|
101
|
+
frameworks[fw] += 1
|
|
102
|
+
for pat in p.patterns:
|
|
103
|
+
patterns[pat.name] += 1
|
|
104
|
+
for t in p.ai_tags:
|
|
105
|
+
tags[t.value] += 1
|
|
106
|
+
|
|
107
|
+
return SearchFacets(
|
|
108
|
+
languages=dict(languages.most_common()),
|
|
109
|
+
frameworks=dict(frameworks.most_common()),
|
|
110
|
+
patterns=dict(patterns.most_common()),
|
|
111
|
+
tags=dict(tags.most_common()),
|
|
112
|
+
total=len(projects),
|
|
113
|
+
)
|