Skip to content

Commit 9af0e3f

Browse files
committed
feat: Introduce KnowCode server with a new service layer and API endpoints, add server tests, and update project roadmap documentation.
1 parent c4b5e02 commit 9af0e3f

20 files changed

Lines changed: 771 additions & 160 deletions

KnowCode.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -673,22 +673,25 @@ You've essentially defined a **code intelligence system**, not a chatbot with em
673673

674674
---
675675

676-
## **Implementation Priority Roadmap**
677-
678-
### Phase 1: Foundation (Must Have)
679-
1. Schema versioning for semantic graph (Layer 3)
680-
2. Incremental processing pipeline (Layer 1)
681-
3. Token budget specification (Layer 9)
682-
4. Query language definition (Layer 8)
683-
684-
### Phase 2: Robustness (Should Have)
685-
5. Confidence scoring on all inferences (Layers 3, 4, 6)
686-
6. Parse error recovery strategy (Layer 2)
687-
7. Cross-language boundary detection (Layer 2)
688-
8. Intent staleness detection (Layer 6)
689-
690-
### Phase 3: Enterprise (Nice to Have)
691-
9. Security model (Cross-cutting)
692-
10. Scalability architecture (Cross-cutting)
693-
11. Observability infrastructure (Cross-cutting)
694-
12. Multi-tenant knowledge store (Layer 8)
676+
## **Implementation Status & Roadmap**
677+
678+
### **Phase 1: Foundation (COMPLETED)**
679+
1. **[x] Unified Semantic Graph (Layer 3)**: Multi-language support (Python, JS, Java, MD, YAML).
680+
2. **[x] Token-Budgeted Synthesis (Layer 9)**: Priority-ranked context generation.
681+
3. **[x] Local Knowledge Store (Layer 8)**: JSON persistence with graph-like querying.
682+
4. **[x] Service Layer (Architecture Substrate)**: Unified business logic for CLI and API.
683+
684+
### **Phase 2: Intelligence Server (COMPLETED)**
685+
5. **[x] FastAPI Server (Layer 10)**: REST API for local IDE agent integration.
686+
6. **[x] Hot Reload (Layer 8)**: Dynamic refresh of knowledge store without downtime.
687+
7. **[x] Granular API (Layer 8)**: Programmatic access to raw entities and relationships.
688+
689+
### **Phase 3: Deep Analysis (NEXT)**
690+
8. **[ ] Static Behavioral Analysis (Layer 4)**: Data flow and state transition tracking.
691+
9. **[ ] Intent Extraction (Layer 6)**: Linking commit messages and ADRs to semantic nodes.
692+
10. **[ ] Confidence Scoring (Layer 3)**: Weighted edges based on analysis source quality.
693+
694+
### **Phase 4: Enterprise (FUTURE)**
695+
11. **[ ] Security & RBAC**: Who can query what modules.
696+
12. **[ ] Scalability**: Supporting monorepos > 1M LOC.
697+
13. **[ ] Team Sharing**: Remote knowledge store synchronization.

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ knowcode context "MyClass.important_method"
4242
# 4. Export documentation
4343
knowcode export -o docs/
4444

45-
# 5. View statistics
45+
# 5. Start the intelligence server
46+
knowcode server --port 8080
47+
48+
# 6. View statistics
4649
knowcode stats
4750
```
4851

@@ -111,6 +114,23 @@ Show statistics about the knowledge store.
111114
knowcode stats [--store <path>]
112115
```
113116

117+
### `server`
118+
Start the FastAPI intelligence server. This is the preferred way for locally hosted AI agents (IDEs) to interact with KnowCode.
119+
120+
```bash
121+
knowcode server [--host <host>] [--port <port>] [--store <path>]
122+
```
123+
124+
**Example:**
125+
```bash
126+
knowcode server --port 8080
127+
```
128+
129+
Once running, you can access endpoints like:
130+
- `GET /api/v1/context?target=MyClass`
131+
- `GET /api/v1/search?q=parser`
132+
- `POST /api/v1/reload` (to refresh data after a new `analyze` run)
133+
114134
## Supported Languages (MVP)
115135

116136
- **Python** (.py) - Full AST parsing (Supports Python 3.9 - 3.12)
@@ -204,9 +224,11 @@ See [KnowCode.md](KnowCode.md) for the full vision. The MVP focuses on:
204224
- ✅ v1.2: Git history integration, temporal tracking
205225
- ✅ v1.3: Token budget optimization, priority ranking
206226
- ✅ v1.4: Runtime signal integration
227+
- ✅ v2.0: Intelligence Server mode (local API for local IDE agents)
207228

208229
**Future releases:**
209-
- v2.0: Server mode, team sharing, enterprise features
230+
- v2.1: Semantic search with embeddings
231+
- v3.0: Team sharing & Enterprise features (RBAC, SSO, etc.)
210232

211233
## License
212234

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ dependencies = [
1313
"tree-sitter-languages>=1.10.0",
1414
"GitPython>=3.1.0",
1515
"tiktoken>=0.7.0",
16+
"fastapi>=0.100.0",
17+
"uvicorn>=0.22.0",
1618
]
1719

1820
[project.scripts]

src/knowcode/cli.py

Lines changed: 67 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
import json
44
import sys
55
from pathlib import Path
6-
from typing import Optional
6+
from typing import Any, Optional
77

88
import click
99

1010
from knowcode import __version__
11-
from knowcode.context_synthesizer import ContextSynthesizer
12-
from knowcode.graph_builder import GraphBuilder
13-
from knowcode.knowledge_store import KnowledgeStore
1411
from knowcode.models import EntityKind
12+
from knowcode.service import KnowCodeService
13+
from knowcode.knowledge_store import KnowledgeStore
1514

1615

1716
@click.group()
@@ -54,28 +53,22 @@ def analyze(directory: str, output: str, ignore: tuple[str, ...], temporal: bool
5453
if coverage:
5554
click.echo(f"Coverage report: {coverage}")
5655

57-
# Build graph
58-
builder = GraphBuilder()
59-
builder.build_from_directory(
60-
root_dir=directory,
61-
additional_ignores=list(ignore),
62-
analyze_temporal=temporal,
63-
coverage_path=Path(coverage) if coverage else None,
56+
service = KnowCodeService()
57+
stats = service.analyze(
58+
directory=directory,
59+
output=output,
60+
ignore=list(ignore),
61+
temporal=temporal,
62+
coverage=coverage,
6463
)
6564

66-
# Create store and save
67-
store = KnowledgeStore.from_graph_builder(builder)
68-
output_path = Path(output)
69-
store.save(output_path)
70-
71-
# Print summary
72-
stats = builder.stats()
7365
click.echo("\n✓ Analysis complete!")
7466
click.echo(f" Entities: {stats['total_entities']}")
7567
click.echo(f" Relationships: {stats['total_relationships']}")
76-
if stats['total_errors'] > 0:
68+
if stats.get('total_errors', 0) > 0:
7769
click.echo(f" Errors: {stats['total_errors']}")
7870

71+
output_path = Path(output)
7972
save_path = output_path / KnowledgeStore.DEFAULT_FILENAME if output_path.is_dir() else output_path
8073
click.echo(f"\n Saved to: {save_path}")
8174

@@ -101,68 +94,26 @@ def query(query_type: str, target: str, store: str, as_json: bool) -> None:
10194
TARGET: Entity ID or search pattern
10295
"""
10396
try:
104-
knowledge = KnowledgeStore.load(store)
97+
service = KnowCodeService(store_path=store)
10598
except FileNotFoundError:
10699
click.echo("Error: Knowledge store not found. Run 'knowcode analyze' first.", err=True)
107100
sys.exit(1)
108101

109-
results: list[dict[str, str]] = []
102+
results: list[dict[str, Any]] = []
110103

111104
if query_type == "search":
112-
entities = knowledge.search(target)
113-
for e in entities:
114-
results.append({
115-
"id": e.id,
116-
"kind": e.kind.value,
117-
"name": e.qualified_name,
118-
"file": e.location.file_path,
119-
"line": str(e.location.line_start),
120-
})
105+
results = service.search(target)
121106

122107
elif query_type == "callers":
123-
entity = knowledge.get_entity(target)
124-
if not entity:
125-
# Try searching
126-
matches = knowledge.search(target)
127-
if matches:
128-
entity = matches[0]
129-
click.echo(f"Using: {entity.id}")
130-
131-
if entity:
132-
callers = knowledge.get_callers(entity.id)
133-
for c in callers:
134-
results.append({
135-
"id": c.id,
136-
"name": c.qualified_name,
137-
"file": c.location.file_path,
138-
})
108+
results = service.get_callers(target)
139109

140110
elif query_type == "callees":
141-
entity = knowledge.get_entity(target)
142-
if not entity:
143-
matches = knowledge.search(target)
144-
if matches:
145-
entity = matches[0]
146-
click.echo(f"Using: {entity.id}")
147-
148-
if entity:
149-
callees = knowledge.get_callees(entity.id)
150-
for c in callees:
151-
results.append({
152-
"id": c.id,
153-
"name": c.qualified_name,
154-
})
111+
results = service.get_callees(target)
155112

156113
elif query_type == "deps":
157-
entity = knowledge.get_entity(target)
158-
if not entity:
159-
matches = knowledge.search(target)
160-
if matches:
161-
entity = matches[0]
162-
click.echo(f"Using: {entity.id}")
163-
114+
entity = service.store.get_entity(target) or next(iter(service.store.search(target)), None)
164115
if entity:
165-
deps = knowledge.get_dependencies(entity.id)
116+
deps = service.store.get_dependencies(entity.id)
166117
for d in deps:
167118
results.append({
168119
"id": d.id,
@@ -179,6 +130,8 @@ def query(query_type: str, target: str, store: str, as_json: bool) -> None:
179130
else:
180131
for r in results:
181132
name = r.get("name", r.get("id", "unknown"))
133+
if "qualified_name" in r:
134+
name = r["qualified_name"]
182135
extra = ""
183136
if "file" in r:
184137
extra = f" ({r['file']}:{r.get('line', '')})"
@@ -207,32 +160,20 @@ def context(target: str, store: str, max_tokens: int) -> None:
207160
TARGET: Entity ID or search pattern
208161
"""
209162
try:
210-
knowledge = KnowledgeStore.load(store)
163+
service = KnowCodeService(store_path=store)
211164
except FileNotFoundError:
212165
click.echo("Error: Knowledge store not found. Run 'knowcode analyze' first.", err=True)
213166
sys.exit(1)
214167

215-
synthesizer = ContextSynthesizer(knowledge, max_tokens=max_tokens)
216-
217-
# Try exact match first
218-
entity = knowledge.get_entity(target)
219-
if not entity:
220-
# Try search
221-
matches = knowledge.search(target)
222-
if matches:
223-
entity = matches[0]
224-
click.echo(f"Using: {entity.id}\n", err=True)
225-
226-
if not entity:
227-
click.echo(f"Entity not found: {target}", err=True)
228-
sys.exit(1)
229-
230-
bundle = synthesizer.synthesize(entity.id)
231-
if bundle:
232-
click.echo(bundle.context_text)
233-
click.echo(f"\n--- {bundle.total_chars} chars, {bundle.total_tokens} tokens, {len(bundle.included_entities)} entities ---", err=True)
234-
if bundle.truncated:
168+
try:
169+
bundle_dict = service.get_context(target, max_tokens=max_tokens)
170+
click.echo(bundle_dict["context_text"])
171+
click.echo(f"\n--- {len(bundle_dict['context_text'])} chars, {bundle_dict['total_tokens']} tokens, {len(bundle_dict['included_entities'])} entities ---", err=True)
172+
if bundle_dict["truncated"]:
235173
click.echo("(truncated)", err=True)
174+
except ValueError as e:
175+
click.echo(f"Error: {e}", err=True)
176+
sys.exit(1)
236177

237178

238179
@cli.command()
@@ -251,11 +192,12 @@ def context(target: str, store: str, max_tokens: int) -> None:
251192
def export(store: str, output: str) -> None:
252193
"""Export knowledge store as Markdown documentation."""
253194
try:
254-
knowledge = KnowledgeStore.load(store)
195+
service = KnowCodeService(store_path=store)
255196
except FileNotFoundError:
256197
click.echo("Error: Knowledge store not found. Run 'knowcode analyze' first.", err=True)
257198
sys.exit(1)
258199

200+
knowledge = service.store
259201
output_dir = Path(output)
260202
output_dir.mkdir(parents=True, exist_ok=True)
261203

@@ -298,36 +240,50 @@ def export(store: str, output: str) -> None:
298240
def stats(store: str) -> None:
299241
"""Show statistics about the knowledge store."""
300242
try:
301-
knowledge = KnowledgeStore.load(store)
243+
service = KnowCodeService(store_path=store)
302244
except FileNotFoundError:
303245
click.echo("Error: Knowledge store not found. Run 'knowcode analyze' first.", err=True)
304246
sys.exit(1)
305247

248+
s = service.get_stats()
306249
click.echo("Knowledge Store Statistics")
307250
click.echo("-" * 30)
308251

309-
# Count by kind
310-
by_kind: dict[str, int] = {}
311-
for entity in knowledge.entities.values():
312-
kind = entity.kind.value
313-
by_kind[kind] = by_kind.get(kind, 0) + 1
314-
315-
click.echo(f"\nTotal Entities: {len(knowledge.entities)}")
316-
for kind, count in sorted(by_kind.items()):
252+
click.echo(f"\nTotal Entities: {s['total_entities']}")
253+
for kind, count in sorted(s['entities_by_kind'].items()):
317254
click.echo(f" {kind}: {count}")
318255

319-
click.echo(f"\nTotal Relationships: {len(knowledge.relationships)}")
320-
321-
# Relationship types
322-
rel_types: dict[str, int] = {}
323-
for rel in knowledge.relationships:
324-
kind = rel.kind.value
325-
rel_types[kind] = rel_types.get(kind, 0) + 1
326-
327-
for kind, count in sorted(rel_types.items()):
256+
click.echo(f"\nTotal Relationships: {s['total_relationships']}")
257+
for kind, count in sorted(s['relationships_by_type'].items()):
328258
click.echo(f" {kind}: {count}")
329259

330260

261+
@cli.command()
262+
@click.option(
263+
"--store", "-s",
264+
type=click.Path(exists=True),
265+
default=".",
266+
help="Path to knowledge store file or directory",
267+
)
268+
@click.option(
269+
"--host",
270+
default="127.0.0.1",
271+
help="Host to bind the server to (default: 127.0.0.1)",
272+
)
273+
@click.option(
274+
"--port",
275+
default=8000,
276+
help="Port to bind the server to (default: 8000)",
277+
)
278+
def server(store: str, host: str, port: int) -> None:
279+
"""Start the KnowCode intelligence server."""
280+
from knowcode.server.main import start_server
281+
282+
click.echo(f"Starting KnowCode server on {host}:{port}")
283+
click.echo(f"Using knowledge store: {store}")
284+
285+
start_server(host=host, port=port, store_path=store)
286+
331287

332288
@cli.command()
333289
@click.argument("target", required=False)
@@ -349,11 +305,13 @@ def history(target: Optional[str], store: str, limit: int) -> None:
349305
TARGET: Optional entity ID or search pattern. If omitted, shows commit log.
350306
"""
351307
try:
352-
knowledge = KnowledgeStore.load(store)
308+
service = KnowCodeService(store_path=store)
353309
except FileNotFoundError:
354310
click.echo("Error: Knowledge store not found. Run 'knowcode analyze' first.", err=True)
355311
sys.exit(1)
356312

313+
knowledge = service.store
314+
357315
if not target:
358316
# Show recent commits
359317
commits = knowledge.get_entities_by_kind("commit")

0 commit comments

Comments
 (0)