ctxgraph 0.2.3__tar.gz → 0.2.4__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.
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/PKG-INFO +75 -65
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/README.md +74 -64
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/pyproject.toml +1 -1
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph.egg-info/PKG-INFO +75 -65
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/setup.cfg +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/analyzers/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/analyzers/python/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/analyzers/python/importer.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/analyzers/python/semantic.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/analyzers/python/symbols.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/capsule/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/capsule/renderer.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/cli/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/cli/main.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/clients/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/clients/models.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/config/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/config/providers.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/config/settings.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/exclude/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/exclude/patterns.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/graph/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/graph/builder.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/graph/models.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/graph/query.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/graph/storage.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/mcp/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/mcp/server.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/view/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/view/visualizer.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/wrapper/__init__.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph/wrapper/claude.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph.egg-info/SOURCES.txt +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph.egg-info/dependency_links.txt +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph.egg-info/entry_points.txt +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph.egg-info/requires.txt +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/src/ctxgraph.egg-info/top_level.txt +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_analyzers.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_benchmark.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_capsule.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_config.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_integration.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_model_mode.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_models.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_query.py +0 -0
- {ctxgraph-0.2.3 → ctxgraph-0.2.4}/tests/test_storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxgraph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: AI context engine for Python — cuts LLM tokens 97% via code knowledge graphs. Build, query, and generate compact context capsules for Claude, OpenAI, Ollama.
|
|
5
5
|
Author: ctxgraph contributors
|
|
6
6
|
License: MIT
|
|
@@ -330,9 +330,15 @@ ctx capsule "extract payment processing into separate module" --mode deep
|
|
|
330
330
|
|
|
331
331
|
## Framework Integrations
|
|
332
332
|
|
|
333
|
-
ctxgraph
|
|
333
|
+
ctxgraph is a Python library first — the CLI is just a wrapper. This makes it easy to feed code context into LangChain, LangGraph, OpenAI Agents, or any LLM pipeline.
|
|
334
334
|
|
|
335
|
-
### Python API
|
|
335
|
+
### How the Python API works
|
|
336
|
+
|
|
337
|
+
The flow is always the same:
|
|
338
|
+
|
|
339
|
+
1. **`build_graph(path)`** → scans your code, stores a knowledge graph in `path/.ctxgraph/graph.db`
|
|
340
|
+
2. **`get_storage(path)`** → opens that SQLite database for queries (fast, no re-scanning)
|
|
341
|
+
3. **`render_capsule(storage, query)`** → searches the graph, returns a compact text capsule
|
|
336
342
|
|
|
337
343
|
```python
|
|
338
344
|
from pathlib import Path
|
|
@@ -340,154 +346,158 @@ from ctxgraph.graph.builder import build_graph, get_storage
|
|
|
340
346
|
from ctxgraph.capsule.renderer import render_capsule
|
|
341
347
|
from ctxgraph.graph.query import search_relevant_nodes
|
|
342
348
|
|
|
343
|
-
# 1
|
|
344
|
-
stats = build_graph(Path("/path/to/
|
|
349
|
+
# --- Step 1: Build (one-time, ~0.1-1s per project) ---
|
|
350
|
+
stats = build_graph(Path("/path/to/my_project"))
|
|
345
351
|
print(f"Built: {stats['total_nodes']} nodes, {stats['total_edges']} edges")
|
|
346
352
|
|
|
347
|
-
# 2
|
|
348
|
-
storage = get_storage(Path("/path/to/
|
|
353
|
+
# --- Step 2: Use (instant — reads the .db file) ---
|
|
354
|
+
storage = get_storage(Path("/path/to/my_project"))
|
|
349
355
|
|
|
350
|
-
#
|
|
356
|
+
# Generate a capsule — a token-efficient DSL string
|
|
351
357
|
capsule = render_capsule(storage, "fix JWT token validation", max_nodes=20)
|
|
352
358
|
print(capsule)
|
|
353
|
-
|
|
354
|
-
#
|
|
359
|
+
# → [CTX]fix JWT token validation
|
|
360
|
+
# [F]src/auth/jwt.py
|
|
361
|
+
# D:JWT token creation and validation
|
|
362
|
+
# [F]src/auth/middleware.py
|
|
363
|
+
# D:Auth middleware for request validation
|
|
364
|
+
# ...
|
|
365
|
+
|
|
366
|
+
# Or search for nodes programmatically
|
|
355
367
|
results = search_relevant_nodes(storage, "auth login", max_nodes=10, max_depth=2)
|
|
356
368
|
for node, score in results:
|
|
357
369
|
print(f" {node.type}:{node.name} (score={score})")
|
|
358
370
|
```
|
|
359
371
|
|
|
372
|
+
> **Tip:** `build_graph` is a one-time setup. In production, run `ctx build` during CI/deployment and let your app code only call `get_storage` + `render_capsule`.
|
|
373
|
+
|
|
360
374
|
### LangChain
|
|
361
375
|
|
|
362
|
-
|
|
376
|
+
Pass the capsule as context in your prompt template. The LLM gets exactly the files, classes, and dependencies it needs — no token waste.
|
|
363
377
|
|
|
364
378
|
```python
|
|
365
379
|
from pathlib import Path
|
|
366
380
|
from langchain_openai import ChatOpenAI
|
|
367
381
|
from langchain_core.prompts import ChatPromptTemplate
|
|
368
|
-
from ctxgraph.graph.builder import
|
|
382
|
+
from ctxgraph.graph.builder import get_storage # graph already built
|
|
369
383
|
from ctxgraph.capsule.renderer import render_capsule
|
|
370
384
|
|
|
371
|
-
#
|
|
372
|
-
build_graph(Path("./my_project"))
|
|
385
|
+
# Load existing graph (zero build time)
|
|
373
386
|
storage = get_storage(Path("./my_project"))
|
|
374
387
|
|
|
375
|
-
# Generate
|
|
376
|
-
context = render_capsule(storage, "
|
|
388
|
+
# Generate capsule for the question
|
|
389
|
+
context = render_capsule(storage, "login rate limiter", max_nodes=15)
|
|
377
390
|
|
|
378
391
|
prompt = ChatPromptTemplate.from_messages([
|
|
379
|
-
("system", "You are a senior Python
|
|
392
|
+
("system", "You are a senior Python dev. Answer using the code context below.\n\n{context}"),
|
|
380
393
|
("user", "{question}"),
|
|
381
394
|
])
|
|
382
395
|
|
|
383
396
|
llm = ChatOpenAI(model="gpt-4o")
|
|
384
|
-
|
|
397
|
+
response = prompt | llm | (lambda msg: msg.content)
|
|
385
398
|
|
|
386
|
-
response
|
|
399
|
+
print(response.invoke({
|
|
387
400
|
"context": context,
|
|
388
|
-
"question": "Where is the
|
|
389
|
-
})
|
|
401
|
+
"question": "Where is the rate limiter applied in the login flow?",
|
|
402
|
+
}))
|
|
403
|
+
# → "The rate limiter is in src/auth/middleware.py at line 42.
|
|
404
|
+
# It wraps the login endpoint with a 5req/min limit per IP."
|
|
390
405
|
```
|
|
391
406
|
|
|
392
407
|
### LangGraph
|
|
393
408
|
|
|
394
|
-
|
|
409
|
+
Expose ctxgraph as a tool the agent calls on-demand. The agent fetches context only when it hits a code-related question.
|
|
395
410
|
|
|
396
411
|
```python
|
|
397
412
|
from pathlib import Path
|
|
398
|
-
from typing import Literal
|
|
399
413
|
from langgraph.graph import StateGraph, MessagesState
|
|
400
414
|
from langgraph.prebuilt import ToolNode
|
|
401
415
|
from langchain_openai import ChatOpenAI
|
|
402
416
|
from langchain_core.tools import tool
|
|
403
|
-
from ctxgraph.graph.builder import
|
|
417
|
+
from ctxgraph.graph.builder import get_storage
|
|
404
418
|
from ctxgraph.capsule.renderer import render_capsule
|
|
405
419
|
|
|
406
|
-
# Pre-
|
|
407
|
-
|
|
408
|
-
storage = get_storage(Path("./my_project"))
|
|
420
|
+
# Pre-built graph — loaded instantly
|
|
421
|
+
_storage = get_storage(Path("./my_project"))
|
|
409
422
|
|
|
410
423
|
@tool
|
|
411
424
|
def code_context(task: str) -> str:
|
|
412
|
-
"""
|
|
413
|
-
|
|
425
|
+
"""Fetch relevant source code for a development task.
|
|
426
|
+
Use this whenever the user asks about implementation details,
|
|
427
|
+
bug fixes, or architecture in the codebase."""
|
|
428
|
+
return render_capsule(_storage, task, max_nodes=20)
|
|
414
429
|
|
|
430
|
+
# --- Build LangGraph ---
|
|
415
431
|
tools = [code_context]
|
|
416
|
-
tool_node = ToolNode(tools)
|
|
417
|
-
|
|
418
432
|
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
|
|
419
433
|
|
|
420
|
-
def
|
|
421
|
-
return "tools" if state["messages"][-1].tool_calls else "__end__"
|
|
422
|
-
|
|
423
|
-
def call_model(state: MessagesState):
|
|
434
|
+
def agent_node(state: MessagesState):
|
|
424
435
|
return {"messages": [model.invoke(state["messages"])]}
|
|
425
436
|
|
|
426
437
|
graph = StateGraph(MessagesState)
|
|
427
|
-
graph.add_node("agent",
|
|
428
|
-
graph.add_node("tools",
|
|
438
|
+
graph.add_node("agent", agent_node)
|
|
439
|
+
graph.add_node("tools", ToolNode(tools))
|
|
429
440
|
graph.set_entry_point("agent")
|
|
430
|
-
graph.add_conditional_edges(
|
|
441
|
+
graph.add_conditional_edges(
|
|
442
|
+
"agent",
|
|
443
|
+
lambda s: "tools" if s["messages"][-1].tool_calls else "__end__",
|
|
444
|
+
)
|
|
431
445
|
graph.add_edge("tools", "agent")
|
|
432
446
|
|
|
433
447
|
app = graph.compile()
|
|
434
448
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
449
|
+
# --- Run ---
|
|
450
|
+
for chunk in app.stream({"messages": [("user", "How does payment retry work?")]}):
|
|
451
|
+
for node, vals in chunk.items():
|
|
452
|
+
msg = vals["messages"][0]
|
|
453
|
+
if hasattr(msg, "content") and msg.content:
|
|
454
|
+
print(f"[{node}]: {msg.content[:300]}")
|
|
438
455
|
```
|
|
439
456
|
|
|
457
|
+
When the user asks about code, the agent calls `code_context("payment retry")`, gets back a capsule with `[F]src/payment/retry.py`, `[F]src/payment/processor.py`, and their dependency edges, then answers with those files in context.
|
|
458
|
+
|
|
440
459
|
### OpenAI Agents SDK
|
|
441
460
|
|
|
442
|
-
|
|
461
|
+
Same pattern — ctxgraph is a function tool the agent invokes.
|
|
443
462
|
|
|
444
463
|
```python
|
|
445
464
|
from pathlib import Path
|
|
446
|
-
from openai import AzureOpenAI # or OpenAI for standard API
|
|
447
465
|
from agents import Agent, Runner, function_tool
|
|
448
|
-
from ctxgraph.graph.builder import
|
|
466
|
+
from ctxgraph.graph.builder import get_storage
|
|
449
467
|
from ctxgraph.capsule.renderer import render_capsule
|
|
450
468
|
|
|
451
|
-
|
|
452
|
-
build_graph(Path("./my_project"))
|
|
453
|
-
storage = get_storage(Path("./my_project"))
|
|
469
|
+
_storage = get_storage(Path("./my_project"))
|
|
454
470
|
|
|
455
471
|
@function_tool
|
|
456
472
|
def fetch_code_context(task_description: str) -> str:
|
|
457
|
-
"""Retrieve
|
|
458
|
-
|
|
473
|
+
"""Retrieve code context from the project's knowledge graph.
|
|
474
|
+
Provide a task description like 'JWT auth middleware' or 'payment processor'."""
|
|
475
|
+
return render_capsule(_storage, task_description, max_nodes=20)
|
|
459
476
|
|
|
460
477
|
agent = Agent(
|
|
461
478
|
name="Code Assistant",
|
|
462
|
-
instructions="You
|
|
463
|
-
model="gpt-4o",
|
|
479
|
+
instructions="You help developers understand their codebase. Use fetch_code_context to get relevant files before answering.",
|
|
480
|
+
model="gpt-4o",
|
|
464
481
|
tools=[fetch_code_context],
|
|
465
482
|
)
|
|
466
483
|
|
|
467
|
-
result = Runner.run_sync(
|
|
468
|
-
agent,
|
|
469
|
-
"How does the JWT authentication middleware work?",
|
|
470
|
-
)
|
|
484
|
+
result = Runner.run_sync(agent, "How does the notification system handle email vs SMS?")
|
|
471
485
|
print(result.final_output)
|
|
472
486
|
```
|
|
473
487
|
|
|
474
|
-
### Azure OpenAI
|
|
488
|
+
### Azure OpenAI (direct client)
|
|
475
489
|
|
|
476
|
-
For Azure OpenAI,
|
|
490
|
+
For Azure OpenAI or any OpenAI-compatible endpoint, inject the capsule directly into the system message.
|
|
477
491
|
|
|
478
492
|
```python
|
|
479
493
|
import os
|
|
480
494
|
from openai import AzureOpenAI
|
|
481
495
|
from pathlib import Path
|
|
482
|
-
from ctxgraph.graph.builder import
|
|
496
|
+
from ctxgraph.graph.builder import get_storage
|
|
483
497
|
from ctxgraph.capsule.renderer import render_capsule
|
|
484
498
|
|
|
485
|
-
# Build graph
|
|
486
|
-
build_graph(Path("./my_project"))
|
|
487
499
|
storage = get_storage(Path("./my_project"))
|
|
488
|
-
|
|
489
|
-
# Generate context capsule
|
|
490
|
-
context = render_capsule(storage, "authentication and authorization", max_nodes=25)
|
|
500
|
+
context = render_capsule(storage, "event bus architecture", max_nodes=25)
|
|
491
501
|
|
|
492
502
|
client = AzureOpenAI(
|
|
493
503
|
api_version="2024-08-01-preview",
|
|
@@ -498,8 +508,8 @@ client = AzureOpenAI(
|
|
|
498
508
|
response = client.chat.completions.create(
|
|
499
509
|
model="gpt-4o", # deployment name
|
|
500
510
|
messages=[
|
|
501
|
-
{"role": "system", "content": f"You are a senior
|
|
502
|
-
{"role": "user", "content": "
|
|
511
|
+
{"role": "system", "content": f"You are a senior developer. Code context:\n\n{context}"},
|
|
512
|
+
{"role": "user", "content": "How do I add a new event handler?"},
|
|
503
513
|
],
|
|
504
514
|
)
|
|
505
515
|
print(response.choices[0].message.content)
|
|
@@ -301,9 +301,15 @@ ctx capsule "extract payment processing into separate module" --mode deep
|
|
|
301
301
|
|
|
302
302
|
## Framework Integrations
|
|
303
303
|
|
|
304
|
-
ctxgraph
|
|
304
|
+
ctxgraph is a Python library first — the CLI is just a wrapper. This makes it easy to feed code context into LangChain, LangGraph, OpenAI Agents, or any LLM pipeline.
|
|
305
305
|
|
|
306
|
-
### Python API
|
|
306
|
+
### How the Python API works
|
|
307
|
+
|
|
308
|
+
The flow is always the same:
|
|
309
|
+
|
|
310
|
+
1. **`build_graph(path)`** → scans your code, stores a knowledge graph in `path/.ctxgraph/graph.db`
|
|
311
|
+
2. **`get_storage(path)`** → opens that SQLite database for queries (fast, no re-scanning)
|
|
312
|
+
3. **`render_capsule(storage, query)`** → searches the graph, returns a compact text capsule
|
|
307
313
|
|
|
308
314
|
```python
|
|
309
315
|
from pathlib import Path
|
|
@@ -311,154 +317,158 @@ from ctxgraph.graph.builder import build_graph, get_storage
|
|
|
311
317
|
from ctxgraph.capsule.renderer import render_capsule
|
|
312
318
|
from ctxgraph.graph.query import search_relevant_nodes
|
|
313
319
|
|
|
314
|
-
# 1
|
|
315
|
-
stats = build_graph(Path("/path/to/
|
|
320
|
+
# --- Step 1: Build (one-time, ~0.1-1s per project) ---
|
|
321
|
+
stats = build_graph(Path("/path/to/my_project"))
|
|
316
322
|
print(f"Built: {stats['total_nodes']} nodes, {stats['total_edges']} edges")
|
|
317
323
|
|
|
318
|
-
# 2
|
|
319
|
-
storage = get_storage(Path("/path/to/
|
|
324
|
+
# --- Step 2: Use (instant — reads the .db file) ---
|
|
325
|
+
storage = get_storage(Path("/path/to/my_project"))
|
|
320
326
|
|
|
321
|
-
#
|
|
327
|
+
# Generate a capsule — a token-efficient DSL string
|
|
322
328
|
capsule = render_capsule(storage, "fix JWT token validation", max_nodes=20)
|
|
323
329
|
print(capsule)
|
|
324
|
-
|
|
325
|
-
#
|
|
330
|
+
# → [CTX]fix JWT token validation
|
|
331
|
+
# [F]src/auth/jwt.py
|
|
332
|
+
# D:JWT token creation and validation
|
|
333
|
+
# [F]src/auth/middleware.py
|
|
334
|
+
# D:Auth middleware for request validation
|
|
335
|
+
# ...
|
|
336
|
+
|
|
337
|
+
# Or search for nodes programmatically
|
|
326
338
|
results = search_relevant_nodes(storage, "auth login", max_nodes=10, max_depth=2)
|
|
327
339
|
for node, score in results:
|
|
328
340
|
print(f" {node.type}:{node.name} (score={score})")
|
|
329
341
|
```
|
|
330
342
|
|
|
343
|
+
> **Tip:** `build_graph` is a one-time setup. In production, run `ctx build` during CI/deployment and let your app code only call `get_storage` + `render_capsule`.
|
|
344
|
+
|
|
331
345
|
### LangChain
|
|
332
346
|
|
|
333
|
-
|
|
347
|
+
Pass the capsule as context in your prompt template. The LLM gets exactly the files, classes, and dependencies it needs — no token waste.
|
|
334
348
|
|
|
335
349
|
```python
|
|
336
350
|
from pathlib import Path
|
|
337
351
|
from langchain_openai import ChatOpenAI
|
|
338
352
|
from langchain_core.prompts import ChatPromptTemplate
|
|
339
|
-
from ctxgraph.graph.builder import
|
|
353
|
+
from ctxgraph.graph.builder import get_storage # graph already built
|
|
340
354
|
from ctxgraph.capsule.renderer import render_capsule
|
|
341
355
|
|
|
342
|
-
#
|
|
343
|
-
build_graph(Path("./my_project"))
|
|
356
|
+
# Load existing graph (zero build time)
|
|
344
357
|
storage = get_storage(Path("./my_project"))
|
|
345
358
|
|
|
346
|
-
# Generate
|
|
347
|
-
context = render_capsule(storage, "
|
|
359
|
+
# Generate capsule for the question
|
|
360
|
+
context = render_capsule(storage, "login rate limiter", max_nodes=15)
|
|
348
361
|
|
|
349
362
|
prompt = ChatPromptTemplate.from_messages([
|
|
350
|
-
("system", "You are a senior Python
|
|
363
|
+
("system", "You are a senior Python dev. Answer using the code context below.\n\n{context}"),
|
|
351
364
|
("user", "{question}"),
|
|
352
365
|
])
|
|
353
366
|
|
|
354
367
|
llm = ChatOpenAI(model="gpt-4o")
|
|
355
|
-
|
|
368
|
+
response = prompt | llm | (lambda msg: msg.content)
|
|
356
369
|
|
|
357
|
-
response
|
|
370
|
+
print(response.invoke({
|
|
358
371
|
"context": context,
|
|
359
|
-
"question": "Where is the
|
|
360
|
-
})
|
|
372
|
+
"question": "Where is the rate limiter applied in the login flow?",
|
|
373
|
+
}))
|
|
374
|
+
# → "The rate limiter is in src/auth/middleware.py at line 42.
|
|
375
|
+
# It wraps the login endpoint with a 5req/min limit per IP."
|
|
361
376
|
```
|
|
362
377
|
|
|
363
378
|
### LangGraph
|
|
364
379
|
|
|
365
|
-
|
|
380
|
+
Expose ctxgraph as a tool the agent calls on-demand. The agent fetches context only when it hits a code-related question.
|
|
366
381
|
|
|
367
382
|
```python
|
|
368
383
|
from pathlib import Path
|
|
369
|
-
from typing import Literal
|
|
370
384
|
from langgraph.graph import StateGraph, MessagesState
|
|
371
385
|
from langgraph.prebuilt import ToolNode
|
|
372
386
|
from langchain_openai import ChatOpenAI
|
|
373
387
|
from langchain_core.tools import tool
|
|
374
|
-
from ctxgraph.graph.builder import
|
|
388
|
+
from ctxgraph.graph.builder import get_storage
|
|
375
389
|
from ctxgraph.capsule.renderer import render_capsule
|
|
376
390
|
|
|
377
|
-
# Pre-
|
|
378
|
-
|
|
379
|
-
storage = get_storage(Path("./my_project"))
|
|
391
|
+
# Pre-built graph — loaded instantly
|
|
392
|
+
_storage = get_storage(Path("./my_project"))
|
|
380
393
|
|
|
381
394
|
@tool
|
|
382
395
|
def code_context(task: str) -> str:
|
|
383
|
-
"""
|
|
384
|
-
|
|
396
|
+
"""Fetch relevant source code for a development task.
|
|
397
|
+
Use this whenever the user asks about implementation details,
|
|
398
|
+
bug fixes, or architecture in the codebase."""
|
|
399
|
+
return render_capsule(_storage, task, max_nodes=20)
|
|
385
400
|
|
|
401
|
+
# --- Build LangGraph ---
|
|
386
402
|
tools = [code_context]
|
|
387
|
-
tool_node = ToolNode(tools)
|
|
388
|
-
|
|
389
403
|
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
|
|
390
404
|
|
|
391
|
-
def
|
|
392
|
-
return "tools" if state["messages"][-1].tool_calls else "__end__"
|
|
393
|
-
|
|
394
|
-
def call_model(state: MessagesState):
|
|
405
|
+
def agent_node(state: MessagesState):
|
|
395
406
|
return {"messages": [model.invoke(state["messages"])]}
|
|
396
407
|
|
|
397
408
|
graph = StateGraph(MessagesState)
|
|
398
|
-
graph.add_node("agent",
|
|
399
|
-
graph.add_node("tools",
|
|
409
|
+
graph.add_node("agent", agent_node)
|
|
410
|
+
graph.add_node("tools", ToolNode(tools))
|
|
400
411
|
graph.set_entry_point("agent")
|
|
401
|
-
graph.add_conditional_edges(
|
|
412
|
+
graph.add_conditional_edges(
|
|
413
|
+
"agent",
|
|
414
|
+
lambda s: "tools" if s["messages"][-1].tool_calls else "__end__",
|
|
415
|
+
)
|
|
402
416
|
graph.add_edge("tools", "agent")
|
|
403
417
|
|
|
404
418
|
app = graph.compile()
|
|
405
419
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
420
|
+
# --- Run ---
|
|
421
|
+
for chunk in app.stream({"messages": [("user", "How does payment retry work?")]}):
|
|
422
|
+
for node, vals in chunk.items():
|
|
423
|
+
msg = vals["messages"][0]
|
|
424
|
+
if hasattr(msg, "content") and msg.content:
|
|
425
|
+
print(f"[{node}]: {msg.content[:300]}")
|
|
409
426
|
```
|
|
410
427
|
|
|
428
|
+
When the user asks about code, the agent calls `code_context("payment retry")`, gets back a capsule with `[F]src/payment/retry.py`, `[F]src/payment/processor.py`, and their dependency edges, then answers with those files in context.
|
|
429
|
+
|
|
411
430
|
### OpenAI Agents SDK
|
|
412
431
|
|
|
413
|
-
|
|
432
|
+
Same pattern — ctxgraph is a function tool the agent invokes.
|
|
414
433
|
|
|
415
434
|
```python
|
|
416
435
|
from pathlib import Path
|
|
417
|
-
from openai import AzureOpenAI # or OpenAI for standard API
|
|
418
436
|
from agents import Agent, Runner, function_tool
|
|
419
|
-
from ctxgraph.graph.builder import
|
|
437
|
+
from ctxgraph.graph.builder import get_storage
|
|
420
438
|
from ctxgraph.capsule.renderer import render_capsule
|
|
421
439
|
|
|
422
|
-
|
|
423
|
-
build_graph(Path("./my_project"))
|
|
424
|
-
storage = get_storage(Path("./my_project"))
|
|
440
|
+
_storage = get_storage(Path("./my_project"))
|
|
425
441
|
|
|
426
442
|
@function_tool
|
|
427
443
|
def fetch_code_context(task_description: str) -> str:
|
|
428
|
-
"""Retrieve
|
|
429
|
-
|
|
444
|
+
"""Retrieve code context from the project's knowledge graph.
|
|
445
|
+
Provide a task description like 'JWT auth middleware' or 'payment processor'."""
|
|
446
|
+
return render_capsule(_storage, task_description, max_nodes=20)
|
|
430
447
|
|
|
431
448
|
agent = Agent(
|
|
432
449
|
name="Code Assistant",
|
|
433
|
-
instructions="You
|
|
434
|
-
model="gpt-4o",
|
|
450
|
+
instructions="You help developers understand their codebase. Use fetch_code_context to get relevant files before answering.",
|
|
451
|
+
model="gpt-4o",
|
|
435
452
|
tools=[fetch_code_context],
|
|
436
453
|
)
|
|
437
454
|
|
|
438
|
-
result = Runner.run_sync(
|
|
439
|
-
agent,
|
|
440
|
-
"How does the JWT authentication middleware work?",
|
|
441
|
-
)
|
|
455
|
+
result = Runner.run_sync(agent, "How does the notification system handle email vs SMS?")
|
|
442
456
|
print(result.final_output)
|
|
443
457
|
```
|
|
444
458
|
|
|
445
|
-
### Azure OpenAI
|
|
459
|
+
### Azure OpenAI (direct client)
|
|
446
460
|
|
|
447
|
-
For Azure OpenAI,
|
|
461
|
+
For Azure OpenAI or any OpenAI-compatible endpoint, inject the capsule directly into the system message.
|
|
448
462
|
|
|
449
463
|
```python
|
|
450
464
|
import os
|
|
451
465
|
from openai import AzureOpenAI
|
|
452
466
|
from pathlib import Path
|
|
453
|
-
from ctxgraph.graph.builder import
|
|
467
|
+
from ctxgraph.graph.builder import get_storage
|
|
454
468
|
from ctxgraph.capsule.renderer import render_capsule
|
|
455
469
|
|
|
456
|
-
# Build graph
|
|
457
|
-
build_graph(Path("./my_project"))
|
|
458
470
|
storage = get_storage(Path("./my_project"))
|
|
459
|
-
|
|
460
|
-
# Generate context capsule
|
|
461
|
-
context = render_capsule(storage, "authentication and authorization", max_nodes=25)
|
|
471
|
+
context = render_capsule(storage, "event bus architecture", max_nodes=25)
|
|
462
472
|
|
|
463
473
|
client = AzureOpenAI(
|
|
464
474
|
api_version="2024-08-01-preview",
|
|
@@ -469,8 +479,8 @@ client = AzureOpenAI(
|
|
|
469
479
|
response = client.chat.completions.create(
|
|
470
480
|
model="gpt-4o", # deployment name
|
|
471
481
|
messages=[
|
|
472
|
-
{"role": "system", "content": f"You are a senior
|
|
473
|
-
{"role": "user", "content": "
|
|
482
|
+
{"role": "system", "content": f"You are a senior developer. Code context:\n\n{context}"},
|
|
483
|
+
{"role": "user", "content": "How do I add a new event handler?"},
|
|
474
484
|
],
|
|
475
485
|
)
|
|
476
486
|
print(response.choices[0].message.content)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ctxgraph"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "AI context engine for Python — cuts LLM tokens 97% via code knowledge graphs. Build, query, and generate compact context capsules for Claude, OpenAI, Ollama."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxgraph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: AI context engine for Python — cuts LLM tokens 97% via code knowledge graphs. Build, query, and generate compact context capsules for Claude, OpenAI, Ollama.
|
|
5
5
|
Author: ctxgraph contributors
|
|
6
6
|
License: MIT
|
|
@@ -330,9 +330,15 @@ ctx capsule "extract payment processing into separate module" --mode deep
|
|
|
330
330
|
|
|
331
331
|
## Framework Integrations
|
|
332
332
|
|
|
333
|
-
ctxgraph
|
|
333
|
+
ctxgraph is a Python library first — the CLI is just a wrapper. This makes it easy to feed code context into LangChain, LangGraph, OpenAI Agents, or any LLM pipeline.
|
|
334
334
|
|
|
335
|
-
### Python API
|
|
335
|
+
### How the Python API works
|
|
336
|
+
|
|
337
|
+
The flow is always the same:
|
|
338
|
+
|
|
339
|
+
1. **`build_graph(path)`** → scans your code, stores a knowledge graph in `path/.ctxgraph/graph.db`
|
|
340
|
+
2. **`get_storage(path)`** → opens that SQLite database for queries (fast, no re-scanning)
|
|
341
|
+
3. **`render_capsule(storage, query)`** → searches the graph, returns a compact text capsule
|
|
336
342
|
|
|
337
343
|
```python
|
|
338
344
|
from pathlib import Path
|
|
@@ -340,154 +346,158 @@ from ctxgraph.graph.builder import build_graph, get_storage
|
|
|
340
346
|
from ctxgraph.capsule.renderer import render_capsule
|
|
341
347
|
from ctxgraph.graph.query import search_relevant_nodes
|
|
342
348
|
|
|
343
|
-
# 1
|
|
344
|
-
stats = build_graph(Path("/path/to/
|
|
349
|
+
# --- Step 1: Build (one-time, ~0.1-1s per project) ---
|
|
350
|
+
stats = build_graph(Path("/path/to/my_project"))
|
|
345
351
|
print(f"Built: {stats['total_nodes']} nodes, {stats['total_edges']} edges")
|
|
346
352
|
|
|
347
|
-
# 2
|
|
348
|
-
storage = get_storage(Path("/path/to/
|
|
353
|
+
# --- Step 2: Use (instant — reads the .db file) ---
|
|
354
|
+
storage = get_storage(Path("/path/to/my_project"))
|
|
349
355
|
|
|
350
|
-
#
|
|
356
|
+
# Generate a capsule — a token-efficient DSL string
|
|
351
357
|
capsule = render_capsule(storage, "fix JWT token validation", max_nodes=20)
|
|
352
358
|
print(capsule)
|
|
353
|
-
|
|
354
|
-
#
|
|
359
|
+
# → [CTX]fix JWT token validation
|
|
360
|
+
# [F]src/auth/jwt.py
|
|
361
|
+
# D:JWT token creation and validation
|
|
362
|
+
# [F]src/auth/middleware.py
|
|
363
|
+
# D:Auth middleware for request validation
|
|
364
|
+
# ...
|
|
365
|
+
|
|
366
|
+
# Or search for nodes programmatically
|
|
355
367
|
results = search_relevant_nodes(storage, "auth login", max_nodes=10, max_depth=2)
|
|
356
368
|
for node, score in results:
|
|
357
369
|
print(f" {node.type}:{node.name} (score={score})")
|
|
358
370
|
```
|
|
359
371
|
|
|
372
|
+
> **Tip:** `build_graph` is a one-time setup. In production, run `ctx build` during CI/deployment and let your app code only call `get_storage` + `render_capsule`.
|
|
373
|
+
|
|
360
374
|
### LangChain
|
|
361
375
|
|
|
362
|
-
|
|
376
|
+
Pass the capsule as context in your prompt template. The LLM gets exactly the files, classes, and dependencies it needs — no token waste.
|
|
363
377
|
|
|
364
378
|
```python
|
|
365
379
|
from pathlib import Path
|
|
366
380
|
from langchain_openai import ChatOpenAI
|
|
367
381
|
from langchain_core.prompts import ChatPromptTemplate
|
|
368
|
-
from ctxgraph.graph.builder import
|
|
382
|
+
from ctxgraph.graph.builder import get_storage # graph already built
|
|
369
383
|
from ctxgraph.capsule.renderer import render_capsule
|
|
370
384
|
|
|
371
|
-
#
|
|
372
|
-
build_graph(Path("./my_project"))
|
|
385
|
+
# Load existing graph (zero build time)
|
|
373
386
|
storage = get_storage(Path("./my_project"))
|
|
374
387
|
|
|
375
|
-
# Generate
|
|
376
|
-
context = render_capsule(storage, "
|
|
388
|
+
# Generate capsule for the question
|
|
389
|
+
context = render_capsule(storage, "login rate limiter", max_nodes=15)
|
|
377
390
|
|
|
378
391
|
prompt = ChatPromptTemplate.from_messages([
|
|
379
|
-
("system", "You are a senior Python
|
|
392
|
+
("system", "You are a senior Python dev. Answer using the code context below.\n\n{context}"),
|
|
380
393
|
("user", "{question}"),
|
|
381
394
|
])
|
|
382
395
|
|
|
383
396
|
llm = ChatOpenAI(model="gpt-4o")
|
|
384
|
-
|
|
397
|
+
response = prompt | llm | (lambda msg: msg.content)
|
|
385
398
|
|
|
386
|
-
response
|
|
399
|
+
print(response.invoke({
|
|
387
400
|
"context": context,
|
|
388
|
-
"question": "Where is the
|
|
389
|
-
})
|
|
401
|
+
"question": "Where is the rate limiter applied in the login flow?",
|
|
402
|
+
}))
|
|
403
|
+
# → "The rate limiter is in src/auth/middleware.py at line 42.
|
|
404
|
+
# It wraps the login endpoint with a 5req/min limit per IP."
|
|
390
405
|
```
|
|
391
406
|
|
|
392
407
|
### LangGraph
|
|
393
408
|
|
|
394
|
-
|
|
409
|
+
Expose ctxgraph as a tool the agent calls on-demand. The agent fetches context only when it hits a code-related question.
|
|
395
410
|
|
|
396
411
|
```python
|
|
397
412
|
from pathlib import Path
|
|
398
|
-
from typing import Literal
|
|
399
413
|
from langgraph.graph import StateGraph, MessagesState
|
|
400
414
|
from langgraph.prebuilt import ToolNode
|
|
401
415
|
from langchain_openai import ChatOpenAI
|
|
402
416
|
from langchain_core.tools import tool
|
|
403
|
-
from ctxgraph.graph.builder import
|
|
417
|
+
from ctxgraph.graph.builder import get_storage
|
|
404
418
|
from ctxgraph.capsule.renderer import render_capsule
|
|
405
419
|
|
|
406
|
-
# Pre-
|
|
407
|
-
|
|
408
|
-
storage = get_storage(Path("./my_project"))
|
|
420
|
+
# Pre-built graph — loaded instantly
|
|
421
|
+
_storage = get_storage(Path("./my_project"))
|
|
409
422
|
|
|
410
423
|
@tool
|
|
411
424
|
def code_context(task: str) -> str:
|
|
412
|
-
"""
|
|
413
|
-
|
|
425
|
+
"""Fetch relevant source code for a development task.
|
|
426
|
+
Use this whenever the user asks about implementation details,
|
|
427
|
+
bug fixes, or architecture in the codebase."""
|
|
428
|
+
return render_capsule(_storage, task, max_nodes=20)
|
|
414
429
|
|
|
430
|
+
# --- Build LangGraph ---
|
|
415
431
|
tools = [code_context]
|
|
416
|
-
tool_node = ToolNode(tools)
|
|
417
|
-
|
|
418
432
|
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
|
|
419
433
|
|
|
420
|
-
def
|
|
421
|
-
return "tools" if state["messages"][-1].tool_calls else "__end__"
|
|
422
|
-
|
|
423
|
-
def call_model(state: MessagesState):
|
|
434
|
+
def agent_node(state: MessagesState):
|
|
424
435
|
return {"messages": [model.invoke(state["messages"])]}
|
|
425
436
|
|
|
426
437
|
graph = StateGraph(MessagesState)
|
|
427
|
-
graph.add_node("agent",
|
|
428
|
-
graph.add_node("tools",
|
|
438
|
+
graph.add_node("agent", agent_node)
|
|
439
|
+
graph.add_node("tools", ToolNode(tools))
|
|
429
440
|
graph.set_entry_point("agent")
|
|
430
|
-
graph.add_conditional_edges(
|
|
441
|
+
graph.add_conditional_edges(
|
|
442
|
+
"agent",
|
|
443
|
+
lambda s: "tools" if s["messages"][-1].tool_calls else "__end__",
|
|
444
|
+
)
|
|
431
445
|
graph.add_edge("tools", "agent")
|
|
432
446
|
|
|
433
447
|
app = graph.compile()
|
|
434
448
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
449
|
+
# --- Run ---
|
|
450
|
+
for chunk in app.stream({"messages": [("user", "How does payment retry work?")]}):
|
|
451
|
+
for node, vals in chunk.items():
|
|
452
|
+
msg = vals["messages"][0]
|
|
453
|
+
if hasattr(msg, "content") and msg.content:
|
|
454
|
+
print(f"[{node}]: {msg.content[:300]}")
|
|
438
455
|
```
|
|
439
456
|
|
|
457
|
+
When the user asks about code, the agent calls `code_context("payment retry")`, gets back a capsule with `[F]src/payment/retry.py`, `[F]src/payment/processor.py`, and their dependency edges, then answers with those files in context.
|
|
458
|
+
|
|
440
459
|
### OpenAI Agents SDK
|
|
441
460
|
|
|
442
|
-
|
|
461
|
+
Same pattern — ctxgraph is a function tool the agent invokes.
|
|
443
462
|
|
|
444
463
|
```python
|
|
445
464
|
from pathlib import Path
|
|
446
|
-
from openai import AzureOpenAI # or OpenAI for standard API
|
|
447
465
|
from agents import Agent, Runner, function_tool
|
|
448
|
-
from ctxgraph.graph.builder import
|
|
466
|
+
from ctxgraph.graph.builder import get_storage
|
|
449
467
|
from ctxgraph.capsule.renderer import render_capsule
|
|
450
468
|
|
|
451
|
-
|
|
452
|
-
build_graph(Path("./my_project"))
|
|
453
|
-
storage = get_storage(Path("./my_project"))
|
|
469
|
+
_storage = get_storage(Path("./my_project"))
|
|
454
470
|
|
|
455
471
|
@function_tool
|
|
456
472
|
def fetch_code_context(task_description: str) -> str:
|
|
457
|
-
"""Retrieve
|
|
458
|
-
|
|
473
|
+
"""Retrieve code context from the project's knowledge graph.
|
|
474
|
+
Provide a task description like 'JWT auth middleware' or 'payment processor'."""
|
|
475
|
+
return render_capsule(_storage, task_description, max_nodes=20)
|
|
459
476
|
|
|
460
477
|
agent = Agent(
|
|
461
478
|
name="Code Assistant",
|
|
462
|
-
instructions="You
|
|
463
|
-
model="gpt-4o",
|
|
479
|
+
instructions="You help developers understand their codebase. Use fetch_code_context to get relevant files before answering.",
|
|
480
|
+
model="gpt-4o",
|
|
464
481
|
tools=[fetch_code_context],
|
|
465
482
|
)
|
|
466
483
|
|
|
467
|
-
result = Runner.run_sync(
|
|
468
|
-
agent,
|
|
469
|
-
"How does the JWT authentication middleware work?",
|
|
470
|
-
)
|
|
484
|
+
result = Runner.run_sync(agent, "How does the notification system handle email vs SMS?")
|
|
471
485
|
print(result.final_output)
|
|
472
486
|
```
|
|
473
487
|
|
|
474
|
-
### Azure OpenAI
|
|
488
|
+
### Azure OpenAI (direct client)
|
|
475
489
|
|
|
476
|
-
For Azure OpenAI,
|
|
490
|
+
For Azure OpenAI or any OpenAI-compatible endpoint, inject the capsule directly into the system message.
|
|
477
491
|
|
|
478
492
|
```python
|
|
479
493
|
import os
|
|
480
494
|
from openai import AzureOpenAI
|
|
481
495
|
from pathlib import Path
|
|
482
|
-
from ctxgraph.graph.builder import
|
|
496
|
+
from ctxgraph.graph.builder import get_storage
|
|
483
497
|
from ctxgraph.capsule.renderer import render_capsule
|
|
484
498
|
|
|
485
|
-
# Build graph
|
|
486
|
-
build_graph(Path("./my_project"))
|
|
487
499
|
storage = get_storage(Path("./my_project"))
|
|
488
|
-
|
|
489
|
-
# Generate context capsule
|
|
490
|
-
context = render_capsule(storage, "authentication and authorization", max_nodes=25)
|
|
500
|
+
context = render_capsule(storage, "event bus architecture", max_nodes=25)
|
|
491
501
|
|
|
492
502
|
client = AzureOpenAI(
|
|
493
503
|
api_version="2024-08-01-preview",
|
|
@@ -498,8 +508,8 @@ client = AzureOpenAI(
|
|
|
498
508
|
response = client.chat.completions.create(
|
|
499
509
|
model="gpt-4o", # deployment name
|
|
500
510
|
messages=[
|
|
501
|
-
{"role": "system", "content": f"You are a senior
|
|
502
|
-
{"role": "user", "content": "
|
|
511
|
+
{"role": "system", "content": f"You are a senior developer. Code context:\n\n{context}"},
|
|
512
|
+
{"role": "user", "content": "How do I add a new event handler?"},
|
|
503
513
|
],
|
|
504
514
|
)
|
|
505
515
|
print(response.choices[0].message.content)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|