ctxgraph 0.2.0__tar.gz → 0.2.1__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.0 → ctxgraph-0.2.1}/PKG-INFO +192 -3
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/README.md +191 -2
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/pyproject.toml +1 -1
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/cli/main.py +16 -7
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/config/providers.py +34 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/config/settings.py +13 -2
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/view/visualizer.py +114 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph.egg-info/PKG-INFO +192 -3
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/setup.cfg +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/analyzers/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/analyzers/python/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/analyzers/python/importer.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/analyzers/python/semantic.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/analyzers/python/symbols.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/capsule/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/capsule/renderer.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/cli/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/clients/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/clients/models.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/config/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/exclude/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/exclude/patterns.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/graph/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/graph/builder.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/graph/models.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/graph/query.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/graph/storage.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/mcp/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/mcp/server.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/view/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/wrapper/__init__.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph/wrapper/claude.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph.egg-info/SOURCES.txt +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph.egg-info/dependency_links.txt +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph.egg-info/entry_points.txt +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph.egg-info/requires.txt +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/src/ctxgraph.egg-info/top_level.txt +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_analyzers.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_benchmark.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_capsule.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_config.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_integration.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_model_mode.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_models.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/tests/test_query.py +0 -0
- {ctxgraph-0.2.0 → ctxgraph-0.2.1}/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.1
|
|
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
|
|
@@ -37,9 +37,11 @@ pip install ctxgraph
|
|
|
37
37
|
ctx build # Build knowledge graph
|
|
38
38
|
ctx capsule "fix JWT expiry" # 92-99% fewer tokens vs raw code
|
|
39
39
|
ccg "fix the login redirect bug" # Launch Claude with context pre-loaded
|
|
40
|
-
ctx view # Interactive D3.js visualization
|
|
40
|
+
ctx view # Interactive D3.js visualization (or --svg for static)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
<img src="docs/graph.svg" alt="ctxgraph knowledge graph visualization" width="100%">
|
|
44
|
+
|
|
43
45
|
---
|
|
44
46
|
|
|
45
47
|
## Why ctxgraph?
|
|
@@ -248,7 +250,7 @@ ccg --mode deep "redesign the database schema" # Deep mode
|
|
|
248
250
|
exclude = ["legacy/*", "vendor/*"]
|
|
249
251
|
|
|
250
252
|
[ai]
|
|
251
|
-
provider = "ollama" # ollama, claude, openai, custom
|
|
253
|
+
provider = "ollama" # ollama, claude, openai, azure, custom
|
|
252
254
|
model = "qwen2.5-coder:7b"
|
|
253
255
|
endpoint = "http://localhost:11434"
|
|
254
256
|
|
|
@@ -265,6 +267,7 @@ max_depth = 2
|
|
|
265
267
|
| `CTXGRAPH_ENDPOINT` | `ai.endpoint` |
|
|
266
268
|
| `ANTHROPIC_API_KEY` | Claude API |
|
|
267
269
|
| `OPENAI_API_KEY` | OpenAI API |
|
|
270
|
+
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API |
|
|
268
271
|
|
|
269
272
|
```bash
|
|
270
273
|
# Ollama (default)
|
|
@@ -276,6 +279,13 @@ CTXGRAPH_PROVIDER=claude CTXGRAPH_MODEL=claude-sonnet-4-20250514 ctx capsule "qu
|
|
|
276
279
|
# OpenAI
|
|
277
280
|
CTXGRAPH_PROVIDER=openai CTXGRAPH_MODEL=gpt-4o ctx capsule "query"
|
|
278
281
|
|
|
282
|
+
# Azure OpenAI
|
|
283
|
+
CTXGRAPH_PROVIDER=azure \
|
|
284
|
+
CTXGRAPH_MODEL=gpt-4o \
|
|
285
|
+
CTXGRAPH_ENDPOINT=https://my-resource.openai.azure.com \
|
|
286
|
+
AZURE_OPENAI_API_KEY=sk-... \
|
|
287
|
+
ctx capsule "query"
|
|
288
|
+
|
|
279
289
|
# Custom (OpenAI-compatible)
|
|
280
290
|
CTXGRAPH_PROVIDER=custom CTXGRAPH_ENDPOINT=http://my-api/v1 ctx capsule "query"
|
|
281
291
|
```
|
|
@@ -307,6 +317,185 @@ ctx capsule "extract payment processing into separate module" --mode deep
|
|
|
307
317
|
|
|
308
318
|
---
|
|
309
319
|
|
|
320
|
+
## Framework Integrations
|
|
321
|
+
|
|
322
|
+
ctxgraph can be used as a Python library — not just a CLI. This makes it easy to plug into LangChain, LangGraph, OpenAI Agents, or any custom AI pipeline.
|
|
323
|
+
|
|
324
|
+
### Python API
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from pathlib import Path
|
|
328
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
329
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
330
|
+
from ctxgraph.graph.query import search_relevant_nodes
|
|
331
|
+
|
|
332
|
+
# 1. Build the graph (one-time setup)
|
|
333
|
+
stats = build_graph(Path("/path/to/project"))
|
|
334
|
+
print(f"Built: {stats['total_nodes']} nodes, {stats['total_edges']} edges")
|
|
335
|
+
|
|
336
|
+
# 2. Get storage for an existing graph
|
|
337
|
+
storage = get_storage(Path("/path/to/project"))
|
|
338
|
+
|
|
339
|
+
# 3. Generate a context capsule (token-efficient text)
|
|
340
|
+
capsule = render_capsule(storage, "fix JWT token validation", max_nodes=20)
|
|
341
|
+
print(capsule)
|
|
342
|
+
|
|
343
|
+
# 4. Search for relevant nodes programmatically
|
|
344
|
+
results = search_relevant_nodes(storage, "auth login", max_nodes=10, max_depth=2)
|
|
345
|
+
for node, score in results:
|
|
346
|
+
print(f" {node.type}:{node.name} (score={score})")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### LangChain
|
|
350
|
+
|
|
351
|
+
Inject ctxgraph capsules directly into your LangChain prompts — dramatically reducing token usage while providing precise code context.
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from pathlib import Path
|
|
355
|
+
from langchain_openai import ChatOpenAI
|
|
356
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
357
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
358
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
359
|
+
|
|
360
|
+
# Build graph once
|
|
361
|
+
build_graph(Path("./my_project"))
|
|
362
|
+
storage = get_storage(Path("./my_project"))
|
|
363
|
+
|
|
364
|
+
# Generate context for a specific task
|
|
365
|
+
context = render_capsule(storage, "user authentication flow", max_nodes=20)
|
|
366
|
+
|
|
367
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
368
|
+
("system", "You are a senior Python developer. Use the code context below to answer the question.\n\n{context}"),
|
|
369
|
+
("user", "{question}"),
|
|
370
|
+
])
|
|
371
|
+
|
|
372
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
373
|
+
chain = prompt | llm
|
|
374
|
+
|
|
375
|
+
response = chain.invoke({
|
|
376
|
+
"context": context,
|
|
377
|
+
"question": "Where is the login rate limiter implemented?",
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### LangGraph
|
|
382
|
+
|
|
383
|
+
Use ctxgraph as a tool within a LangGraph agent — the agent requests context capsules when it needs to understand the codebase.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from pathlib import Path
|
|
387
|
+
from typing import Literal
|
|
388
|
+
from langgraph.graph import StateGraph, MessagesState
|
|
389
|
+
from langgraph.prebuilt import ToolNode
|
|
390
|
+
from langchain_openai import ChatOpenAI
|
|
391
|
+
from langchain_core.tools import tool
|
|
392
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
393
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
394
|
+
|
|
395
|
+
# Pre-build graph
|
|
396
|
+
build_graph(Path("./my_project"))
|
|
397
|
+
storage = get_storage(Path("./my_project"))
|
|
398
|
+
|
|
399
|
+
@tool
|
|
400
|
+
def code_context(task: str) -> str:
|
|
401
|
+
"""Get code context relevant to a task. Use this before answering code questions."""
|
|
402
|
+
return render_capsule(storage, task, max_nodes=20)
|
|
403
|
+
|
|
404
|
+
tools = [code_context]
|
|
405
|
+
tool_node = ToolNode(tools)
|
|
406
|
+
|
|
407
|
+
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
|
|
408
|
+
|
|
409
|
+
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
|
|
410
|
+
return "tools" if state["messages"][-1].tool_calls else "__end__"
|
|
411
|
+
|
|
412
|
+
def call_model(state: MessagesState):
|
|
413
|
+
return {"messages": [model.invoke(state["messages"])]}
|
|
414
|
+
|
|
415
|
+
graph = StateGraph(MessagesState)
|
|
416
|
+
graph.add_node("agent", call_model)
|
|
417
|
+
graph.add_node("tools", tool_node)
|
|
418
|
+
graph.set_entry_point("agent")
|
|
419
|
+
graph.add_conditional_edges("agent", should_continue)
|
|
420
|
+
graph.add_edge("tools", "agent")
|
|
421
|
+
|
|
422
|
+
app = graph.compile()
|
|
423
|
+
|
|
424
|
+
for chunk in app.stream({"messages": [("user", "Find the bug in the payment processor")]}):
|
|
425
|
+
for node, msg in chunk.items():
|
|
426
|
+
print(f"[{node}]: {msg['messages'][0].content[:200] if msg.get('messages') else ''}")
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### OpenAI Agents SDK
|
|
430
|
+
|
|
431
|
+
Use ctxgraph with the official OpenAI Agents SDK (also works with Azure OpenAI via `AzureOpenAIChatCompletionAgent`).
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from pathlib import Path
|
|
435
|
+
from openai import AzureOpenAI # or OpenAI for standard API
|
|
436
|
+
from agents import Agent, Runner, function_tool
|
|
437
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
438
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
439
|
+
|
|
440
|
+
# Pre-build the graph
|
|
441
|
+
build_graph(Path("./my_project"))
|
|
442
|
+
storage = get_storage(Path("./my_project"))
|
|
443
|
+
|
|
444
|
+
@function_tool
|
|
445
|
+
def fetch_code_context(task_description: str) -> str:
|
|
446
|
+
"""Retrieve relevant code context for a development task."""
|
|
447
|
+
return render_capsule(storage, task_description, max_nodes=20)
|
|
448
|
+
|
|
449
|
+
agent = Agent(
|
|
450
|
+
name="Code Assistant",
|
|
451
|
+
instructions="You are a helpful coding assistant. Use the code context tool to understand the codebase before answering.",
|
|
452
|
+
model="gpt-4o", # or AzureOpenAIChatCompletionAgent(deployment="gpt-4o", ...)
|
|
453
|
+
tools=[fetch_code_context],
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
result = Runner.run_sync(
|
|
457
|
+
agent,
|
|
458
|
+
"How does the JWT authentication middleware work?",
|
|
459
|
+
)
|
|
460
|
+
print(result.final_output)
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Azure OpenAI with Custom Agent
|
|
464
|
+
|
|
465
|
+
For Azure OpenAI, configure the client directly and inject ctxgraph context:
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
import os
|
|
469
|
+
from openai import AzureOpenAI
|
|
470
|
+
from pathlib import Path
|
|
471
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
472
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
473
|
+
|
|
474
|
+
# Build graph
|
|
475
|
+
build_graph(Path("./my_project"))
|
|
476
|
+
storage = get_storage(Path("./my_project"))
|
|
477
|
+
|
|
478
|
+
# Generate context capsule
|
|
479
|
+
context = render_capsule(storage, "authentication and authorization", max_nodes=25)
|
|
480
|
+
|
|
481
|
+
client = AzureOpenAI(
|
|
482
|
+
api_version="2024-08-01-preview",
|
|
483
|
+
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
|
484
|
+
api_key=os.environ["AZURE_OPENAI_API_KEY"],
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
response = client.chat.completions.create(
|
|
488
|
+
model="gpt-4o", # deployment name
|
|
489
|
+
messages=[
|
|
490
|
+
{"role": "system", "content": f"You are a senior Python developer. Use the code context below.\n\n{context}"},
|
|
491
|
+
{"role": "user", "content": "Explain the role-based access control (RBAC) implementation."},
|
|
492
|
+
],
|
|
493
|
+
)
|
|
494
|
+
print(response.choices[0].message.content)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
310
499
|
## Development
|
|
311
500
|
|
|
312
501
|
```bash
|
|
@@ -8,9 +8,11 @@ pip install ctxgraph
|
|
|
8
8
|
ctx build # Build knowledge graph
|
|
9
9
|
ctx capsule "fix JWT expiry" # 92-99% fewer tokens vs raw code
|
|
10
10
|
ccg "fix the login redirect bug" # Launch Claude with context pre-loaded
|
|
11
|
-
ctx view # Interactive D3.js visualization
|
|
11
|
+
ctx view # Interactive D3.js visualization (or --svg for static)
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
<img src="docs/graph.svg" alt="ctxgraph knowledge graph visualization" width="100%">
|
|
15
|
+
|
|
14
16
|
---
|
|
15
17
|
|
|
16
18
|
## Why ctxgraph?
|
|
@@ -219,7 +221,7 @@ ccg --mode deep "redesign the database schema" # Deep mode
|
|
|
219
221
|
exclude = ["legacy/*", "vendor/*"]
|
|
220
222
|
|
|
221
223
|
[ai]
|
|
222
|
-
provider = "ollama" # ollama, claude, openai, custom
|
|
224
|
+
provider = "ollama" # ollama, claude, openai, azure, custom
|
|
223
225
|
model = "qwen2.5-coder:7b"
|
|
224
226
|
endpoint = "http://localhost:11434"
|
|
225
227
|
|
|
@@ -236,6 +238,7 @@ max_depth = 2
|
|
|
236
238
|
| `CTXGRAPH_ENDPOINT` | `ai.endpoint` |
|
|
237
239
|
| `ANTHROPIC_API_KEY` | Claude API |
|
|
238
240
|
| `OPENAI_API_KEY` | OpenAI API |
|
|
241
|
+
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API |
|
|
239
242
|
|
|
240
243
|
```bash
|
|
241
244
|
# Ollama (default)
|
|
@@ -247,6 +250,13 @@ CTXGRAPH_PROVIDER=claude CTXGRAPH_MODEL=claude-sonnet-4-20250514 ctx capsule "qu
|
|
|
247
250
|
# OpenAI
|
|
248
251
|
CTXGRAPH_PROVIDER=openai CTXGRAPH_MODEL=gpt-4o ctx capsule "query"
|
|
249
252
|
|
|
253
|
+
# Azure OpenAI
|
|
254
|
+
CTXGRAPH_PROVIDER=azure \
|
|
255
|
+
CTXGRAPH_MODEL=gpt-4o \
|
|
256
|
+
CTXGRAPH_ENDPOINT=https://my-resource.openai.azure.com \
|
|
257
|
+
AZURE_OPENAI_API_KEY=sk-... \
|
|
258
|
+
ctx capsule "query"
|
|
259
|
+
|
|
250
260
|
# Custom (OpenAI-compatible)
|
|
251
261
|
CTXGRAPH_PROVIDER=custom CTXGRAPH_ENDPOINT=http://my-api/v1 ctx capsule "query"
|
|
252
262
|
```
|
|
@@ -278,6 +288,185 @@ ctx capsule "extract payment processing into separate module" --mode deep
|
|
|
278
288
|
|
|
279
289
|
---
|
|
280
290
|
|
|
291
|
+
## Framework Integrations
|
|
292
|
+
|
|
293
|
+
ctxgraph can be used as a Python library — not just a CLI. This makes it easy to plug into LangChain, LangGraph, OpenAI Agents, or any custom AI pipeline.
|
|
294
|
+
|
|
295
|
+
### Python API
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from pathlib import Path
|
|
299
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
300
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
301
|
+
from ctxgraph.graph.query import search_relevant_nodes
|
|
302
|
+
|
|
303
|
+
# 1. Build the graph (one-time setup)
|
|
304
|
+
stats = build_graph(Path("/path/to/project"))
|
|
305
|
+
print(f"Built: {stats['total_nodes']} nodes, {stats['total_edges']} edges")
|
|
306
|
+
|
|
307
|
+
# 2. Get storage for an existing graph
|
|
308
|
+
storage = get_storage(Path("/path/to/project"))
|
|
309
|
+
|
|
310
|
+
# 3. Generate a context capsule (token-efficient text)
|
|
311
|
+
capsule = render_capsule(storage, "fix JWT token validation", max_nodes=20)
|
|
312
|
+
print(capsule)
|
|
313
|
+
|
|
314
|
+
# 4. Search for relevant nodes programmatically
|
|
315
|
+
results = search_relevant_nodes(storage, "auth login", max_nodes=10, max_depth=2)
|
|
316
|
+
for node, score in results:
|
|
317
|
+
print(f" {node.type}:{node.name} (score={score})")
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### LangChain
|
|
321
|
+
|
|
322
|
+
Inject ctxgraph capsules directly into your LangChain prompts — dramatically reducing token usage while providing precise code context.
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from pathlib import Path
|
|
326
|
+
from langchain_openai import ChatOpenAI
|
|
327
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
328
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
329
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
330
|
+
|
|
331
|
+
# Build graph once
|
|
332
|
+
build_graph(Path("./my_project"))
|
|
333
|
+
storage = get_storage(Path("./my_project"))
|
|
334
|
+
|
|
335
|
+
# Generate context for a specific task
|
|
336
|
+
context = render_capsule(storage, "user authentication flow", max_nodes=20)
|
|
337
|
+
|
|
338
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
339
|
+
("system", "You are a senior Python developer. Use the code context below to answer the question.\n\n{context}"),
|
|
340
|
+
("user", "{question}"),
|
|
341
|
+
])
|
|
342
|
+
|
|
343
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
344
|
+
chain = prompt | llm
|
|
345
|
+
|
|
346
|
+
response = chain.invoke({
|
|
347
|
+
"context": context,
|
|
348
|
+
"question": "Where is the login rate limiter implemented?",
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### LangGraph
|
|
353
|
+
|
|
354
|
+
Use ctxgraph as a tool within a LangGraph agent — the agent requests context capsules when it needs to understand the codebase.
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
from typing import Literal
|
|
359
|
+
from langgraph.graph import StateGraph, MessagesState
|
|
360
|
+
from langgraph.prebuilt import ToolNode
|
|
361
|
+
from langchain_openai import ChatOpenAI
|
|
362
|
+
from langchain_core.tools import tool
|
|
363
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
364
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
365
|
+
|
|
366
|
+
# Pre-build graph
|
|
367
|
+
build_graph(Path("./my_project"))
|
|
368
|
+
storage = get_storage(Path("./my_project"))
|
|
369
|
+
|
|
370
|
+
@tool
|
|
371
|
+
def code_context(task: str) -> str:
|
|
372
|
+
"""Get code context relevant to a task. Use this before answering code questions."""
|
|
373
|
+
return render_capsule(storage, task, max_nodes=20)
|
|
374
|
+
|
|
375
|
+
tools = [code_context]
|
|
376
|
+
tool_node = ToolNode(tools)
|
|
377
|
+
|
|
378
|
+
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
|
|
379
|
+
|
|
380
|
+
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
|
|
381
|
+
return "tools" if state["messages"][-1].tool_calls else "__end__"
|
|
382
|
+
|
|
383
|
+
def call_model(state: MessagesState):
|
|
384
|
+
return {"messages": [model.invoke(state["messages"])]}
|
|
385
|
+
|
|
386
|
+
graph = StateGraph(MessagesState)
|
|
387
|
+
graph.add_node("agent", call_model)
|
|
388
|
+
graph.add_node("tools", tool_node)
|
|
389
|
+
graph.set_entry_point("agent")
|
|
390
|
+
graph.add_conditional_edges("agent", should_continue)
|
|
391
|
+
graph.add_edge("tools", "agent")
|
|
392
|
+
|
|
393
|
+
app = graph.compile()
|
|
394
|
+
|
|
395
|
+
for chunk in app.stream({"messages": [("user", "Find the bug in the payment processor")]}):
|
|
396
|
+
for node, msg in chunk.items():
|
|
397
|
+
print(f"[{node}]: {msg['messages'][0].content[:200] if msg.get('messages') else ''}")
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### OpenAI Agents SDK
|
|
401
|
+
|
|
402
|
+
Use ctxgraph with the official OpenAI Agents SDK (also works with Azure OpenAI via `AzureOpenAIChatCompletionAgent`).
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
from pathlib import Path
|
|
406
|
+
from openai import AzureOpenAI # or OpenAI for standard API
|
|
407
|
+
from agents import Agent, Runner, function_tool
|
|
408
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
409
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
410
|
+
|
|
411
|
+
# Pre-build the graph
|
|
412
|
+
build_graph(Path("./my_project"))
|
|
413
|
+
storage = get_storage(Path("./my_project"))
|
|
414
|
+
|
|
415
|
+
@function_tool
|
|
416
|
+
def fetch_code_context(task_description: str) -> str:
|
|
417
|
+
"""Retrieve relevant code context for a development task."""
|
|
418
|
+
return render_capsule(storage, task_description, max_nodes=20)
|
|
419
|
+
|
|
420
|
+
agent = Agent(
|
|
421
|
+
name="Code Assistant",
|
|
422
|
+
instructions="You are a helpful coding assistant. Use the code context tool to understand the codebase before answering.",
|
|
423
|
+
model="gpt-4o", # or AzureOpenAIChatCompletionAgent(deployment="gpt-4o", ...)
|
|
424
|
+
tools=[fetch_code_context],
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
result = Runner.run_sync(
|
|
428
|
+
agent,
|
|
429
|
+
"How does the JWT authentication middleware work?",
|
|
430
|
+
)
|
|
431
|
+
print(result.final_output)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Azure OpenAI with Custom Agent
|
|
435
|
+
|
|
436
|
+
For Azure OpenAI, configure the client directly and inject ctxgraph context:
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
import os
|
|
440
|
+
from openai import AzureOpenAI
|
|
441
|
+
from pathlib import Path
|
|
442
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
443
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
444
|
+
|
|
445
|
+
# Build graph
|
|
446
|
+
build_graph(Path("./my_project"))
|
|
447
|
+
storage = get_storage(Path("./my_project"))
|
|
448
|
+
|
|
449
|
+
# Generate context capsule
|
|
450
|
+
context = render_capsule(storage, "authentication and authorization", max_nodes=25)
|
|
451
|
+
|
|
452
|
+
client = AzureOpenAI(
|
|
453
|
+
api_version="2024-08-01-preview",
|
|
454
|
+
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
|
455
|
+
api_key=os.environ["AZURE_OPENAI_API_KEY"],
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
response = client.chat.completions.create(
|
|
459
|
+
model="gpt-4o", # deployment name
|
|
460
|
+
messages=[
|
|
461
|
+
{"role": "system", "content": f"You are a senior Python developer. Use the code context below.\n\n{context}"},
|
|
462
|
+
{"role": "user", "content": "Explain the role-based access control (RBAC) implementation."},
|
|
463
|
+
],
|
|
464
|
+
)
|
|
465
|
+
print(response.choices[0].message.content)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
281
470
|
## Development
|
|
282
471
|
|
|
283
472
|
```bash
|
|
@@ -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.1"
|
|
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"}
|
|
@@ -160,14 +160,17 @@ def view(
|
|
|
160
160
|
),
|
|
161
161
|
port: Optional[int] = typer.Option(None, "--port", "-p", help="Port for server"),
|
|
162
162
|
output: Optional[str] = typer.Option(
|
|
163
|
-
None, "--output", "-o", help="Save HTML to file"
|
|
163
|
+
None, "--output", "-o", help="Save HTML/SVG to file"
|
|
164
164
|
),
|
|
165
165
|
open_browser: bool = typer.Option(
|
|
166
166
|
True, "--open/--no-open", help="Open in browser automatically"
|
|
167
167
|
),
|
|
168
|
+
svg: bool = typer.Option(
|
|
169
|
+
False, "--svg", help="Generate static SVG instead of interactive HTML"
|
|
170
|
+
),
|
|
168
171
|
):
|
|
169
172
|
"""Visualize the dependency graph in a browser."""
|
|
170
|
-
from ctxgraph.view.visualizer import render_view
|
|
173
|
+
from ctxgraph.view.visualizer import render_view, render_svg
|
|
171
174
|
|
|
172
175
|
path = Path(repo_path).resolve() if repo_path else Path.cwd()
|
|
173
176
|
storage = get_storage(path)
|
|
@@ -177,19 +180,25 @@ def view(
|
|
|
177
180
|
)
|
|
178
181
|
raise typer.Exit(1)
|
|
179
182
|
|
|
180
|
-
|
|
183
|
+
if svg:
|
|
184
|
+
content = render_svg(storage)
|
|
185
|
+
suffix = ".svg"
|
|
186
|
+
else:
|
|
187
|
+
content = render_view(storage)
|
|
188
|
+
suffix = ".html"
|
|
181
189
|
|
|
182
190
|
if output:
|
|
183
191
|
out_path = Path(output)
|
|
184
|
-
out_path.write_text(
|
|
192
|
+
out_path.write_text(content, encoding="utf-8")
|
|
185
193
|
console.print(f"Saved to [bold]{out_path}[/bold]")
|
|
186
194
|
else:
|
|
187
|
-
|
|
195
|
+
filename = f"graph{suffix}"
|
|
196
|
+
out_path = path / ".ctxgraph" / filename
|
|
188
197
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
189
|
-
out_path.write_text(
|
|
198
|
+
out_path.write_text(content, encoding="utf-8")
|
|
190
199
|
console.print(f"Saved to [bold]{out_path}[/bold]")
|
|
191
200
|
|
|
192
|
-
if open_browser:
|
|
201
|
+
if open_browser and not svg:
|
|
193
202
|
import webbrowser
|
|
194
203
|
|
|
195
204
|
webbrowser.open(f"file://{out_path.absolute()}")
|
|
@@ -21,6 +21,8 @@ def chat_completion(
|
|
|
21
21
|
return _claude_chat(settings, system_prompt, user_prompt)
|
|
22
22
|
elif provider == "openai":
|
|
23
23
|
return _openai_chat(settings, system_prompt, user_prompt)
|
|
24
|
+
elif provider == "azure":
|
|
25
|
+
return _azure_chat(settings, system_prompt, user_prompt)
|
|
24
26
|
else:
|
|
25
27
|
return _custom_chat(settings, system_prompt, user_prompt)
|
|
26
28
|
|
|
@@ -125,6 +127,38 @@ def _openai_chat(settings: Settings, system: str, user: str) -> Optional[str]:
|
|
|
125
127
|
return None
|
|
126
128
|
|
|
127
129
|
|
|
130
|
+
def _azure_chat(settings: Settings, system: str, user: str) -> Optional[str]:
|
|
131
|
+
api_key = settings.api_key
|
|
132
|
+
if not api_key:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
url = settings.get_chat_url()
|
|
136
|
+
payload = {
|
|
137
|
+
"messages": [
|
|
138
|
+
{"role": "system", "content": system},
|
|
139
|
+
{"role": "user", "content": user},
|
|
140
|
+
],
|
|
141
|
+
"max_tokens": settings.max_tokens,
|
|
142
|
+
"temperature": settings.temperature,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
data = json.dumps(payload).encode()
|
|
147
|
+
req = urllib.request.Request(
|
|
148
|
+
url,
|
|
149
|
+
data=data,
|
|
150
|
+
headers={
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"api-key": api_key,
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
156
|
+
result = json.loads(resp.read())
|
|
157
|
+
return result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
158
|
+
except (urllib.error.URLError, json.JSONDecodeError, TimeoutError):
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
128
162
|
def _custom_chat(settings: Settings, system: str, user: str) -> Optional[str]:
|
|
129
163
|
url = settings.get_chat_url()
|
|
130
164
|
payload = {
|
|
@@ -50,6 +50,14 @@ PROVIDER_CONFIGS = {
|
|
|
50
50
|
"chat_endpoint": "/chat/completions",
|
|
51
51
|
"api_key_env": "OPENAI_API_KEY",
|
|
52
52
|
},
|
|
53
|
+
"azure": {
|
|
54
|
+
"endpoint_default": "https://YOUR_RESOURCE.openai.azure.com",
|
|
55
|
+
"api_key_required": True,
|
|
56
|
+
"models": ["gpt-4o", "gpt-4o-mini"],
|
|
57
|
+
"chat_endpoint": "/openai/deployments/{deployment}/chat/completions?api-version=2024-08-01-preview",
|
|
58
|
+
"api_key_env": "AZURE_OPENAI_API_KEY",
|
|
59
|
+
"api_key_header": "api-key",
|
|
60
|
+
},
|
|
53
61
|
"custom": {
|
|
54
62
|
"endpoint_default": None,
|
|
55
63
|
"api_key_required": False,
|
|
@@ -158,7 +166,10 @@ class Settings:
|
|
|
158
166
|
def get_chat_url(self) -> str:
|
|
159
167
|
pconfig = self.get_provider_config()
|
|
160
168
|
endpoint = self.endpoint.rstrip("/")
|
|
161
|
-
|
|
169
|
+
chat_ep = pconfig.get("chat_endpoint", "/v1/chat/completions")
|
|
170
|
+
if "{deployment}" in chat_ep:
|
|
171
|
+
chat_ep = chat_ep.replace("{deployment}", self.model)
|
|
172
|
+
return f"{endpoint}{chat_ep}"
|
|
162
173
|
|
|
163
174
|
def to_dict(self) -> dict:
|
|
164
175
|
return dict(self._data)
|
|
@@ -210,7 +221,7 @@ def create_default_config(repo_path: Path):
|
|
|
210
221
|
exclude = []
|
|
211
222
|
|
|
212
223
|
[ai]
|
|
213
|
-
# Provider: ollama, claude, openai, or custom
|
|
224
|
+
# Provider: ollama, claude, openai, azure, or custom
|
|
214
225
|
provider = "ollama"
|
|
215
226
|
# Model name
|
|
216
227
|
model = "qwen2.5-coder:7b"
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import math
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from ctxgraph.graph.storage import Storage
|
|
7
8
|
|
|
9
|
+
# SVG constants
|
|
10
|
+
SVG_WIDTH = 960
|
|
11
|
+
SVG_HEIGHT = 680
|
|
12
|
+
SVG_CENTER_X = SVG_WIDTH // 2
|
|
13
|
+
SVG_CENTER_Y = SVG_HEIGHT // 2
|
|
14
|
+
SVG_RADIUS = 260
|
|
15
|
+
|
|
16
|
+
COLORS = {
|
|
17
|
+
"file": "#58a6ff",
|
|
18
|
+
"class": "#d29922",
|
|
19
|
+
"function": "#3fb950",
|
|
20
|
+
"module": "#bc8cff",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
NODE_ORDER = {"file": 0, "class": 1, "function": 2, "module": 3}
|
|
24
|
+
|
|
8
25
|
|
|
9
26
|
def render_view(storage: Storage) -> str:
|
|
10
27
|
nodes = storage.get_all_nodes()
|
|
@@ -62,6 +79,103 @@ def render_view(storage: Storage) -> str:
|
|
|
62
79
|
return template.replace("/* GRAPH_DATA */", json_data)
|
|
63
80
|
|
|
64
81
|
|
|
82
|
+
def render_svg(storage: Storage) -> str:
|
|
83
|
+
"""Generate a static SVG visualization of the knowledge graph."""
|
|
84
|
+
nodes = storage.get_all_nodes()
|
|
85
|
+
edges = storage.get_all_edges()
|
|
86
|
+
|
|
87
|
+
file_nodes = [n for n in nodes if n.type == "file"]
|
|
88
|
+
symbol_nodes = [n for n in nodes if n.type != "file"]
|
|
89
|
+
file_node_ids = {n.id for n in file_nodes}
|
|
90
|
+
|
|
91
|
+
# Layout: files in a circle, symbols at parent file position
|
|
92
|
+
n_files = len(file_nodes)
|
|
93
|
+
positions: dict[str, tuple[float, float]] = {}
|
|
94
|
+
|
|
95
|
+
for i, node in enumerate(file_nodes):
|
|
96
|
+
angle = (2 * math.pi * i / n_files) - math.pi / 2
|
|
97
|
+
x = SVG_CENTER_X + SVG_RADIUS * math.cos(angle)
|
|
98
|
+
y = SVG_CENTER_Y + SVG_RADIUS * math.sin(angle)
|
|
99
|
+
positions[node.id] = (x, y)
|
|
100
|
+
|
|
101
|
+
# Place symbol nodes near their parent file (offset slightly)
|
|
102
|
+
for node in symbol_nodes:
|
|
103
|
+
parent_id = f"file:{node.path}" if node.path else None
|
|
104
|
+
if parent_id and parent_id in positions:
|
|
105
|
+
px, py = positions[parent_id]
|
|
106
|
+
offset = 22
|
|
107
|
+
positions[node.id] = (px + offset, py + offset)
|
|
108
|
+
else:
|
|
109
|
+
positions[node.id] = (SVG_CENTER_X, SVG_CENTER_Y)
|
|
110
|
+
|
|
111
|
+
# Build edge lines
|
|
112
|
+
edge_lines: list[str] = []
|
|
113
|
+
for edge in edges:
|
|
114
|
+
src = positions.get(edge.source_id)
|
|
115
|
+
tgt = positions.get(edge.target_id)
|
|
116
|
+
if src and tgt:
|
|
117
|
+
edge_lines.append(
|
|
118
|
+
f'<line x1="{src[0]:.1f}" y1="{src[1]:.1f}" '
|
|
119
|
+
f'x2="{tgt[0]:.1f}" y2="{tgt[1]:.1f}" '
|
|
120
|
+
f'class="edge edge-{edge.relation}" />'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Build node circles + labels
|
|
124
|
+
all_nodes = file_nodes + symbol_nodes
|
|
125
|
+
all_nodes.sort(key=lambda n: NODE_ORDER.get(n.type, 9))
|
|
126
|
+
|
|
127
|
+
node_groups: list[str] = []
|
|
128
|
+
for node in all_nodes:
|
|
129
|
+
x, y = positions.get(node.id, (SVG_CENTER_X, SVG_CENTER_Y))
|
|
130
|
+
r = 5 + (node.importance or 0.5) * 5
|
|
131
|
+
color = COLORS.get(node.type, "#8b949e")
|
|
132
|
+
label = node.name
|
|
133
|
+
node_groups.append(
|
|
134
|
+
f'<g class="node node-{node.type}">'
|
|
135
|
+
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.1f}" fill="{color}" stroke="#161b22" stroke-width="1.5" />'
|
|
136
|
+
f'<text x="{x + r + 6:.1f}" y="{y + 4:.1f}" class="label">{_esc(label)}</text>'
|
|
137
|
+
f'</g>'
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Stats
|
|
141
|
+
n_edges = len(set((e.source_id, e.target_id) for e in edges))
|
|
142
|
+
|
|
143
|
+
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {SVG_WIDTH} {SVG_HEIGHT}" width="100%" height="100%" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0d1117;color:#c9d1d9;">
|
|
144
|
+
<style>
|
|
145
|
+
.edge {{ stroke-opacity:0.4; stroke-width:1.2; }}
|
|
146
|
+
.edge-imports {{ stroke:#58a6ff; stroke-dasharray:4,2; }}
|
|
147
|
+
.edge-calls {{ stroke:#3fb950; stroke-dasharray:2,2; }}
|
|
148
|
+
.edge-defines {{ stroke:#d29922; }}
|
|
149
|
+
.edge-extends {{ stroke:#bc8cff; }}
|
|
150
|
+
.node {{ cursor:pointer; }}
|
|
151
|
+
.node-file circle {{ filter: drop-shadow(0 0 4px #58a6ff55); }}
|
|
152
|
+
.label {{ fill:#8b949e; font-size:10px; }}
|
|
153
|
+
.node-file .label {{ fill:#c9d1d9; font-weight:600; }}
|
|
154
|
+
.title {{ fill:#c9d1d9; font-size:18px; font-weight:700; }}
|
|
155
|
+
.subtitle {{ fill:#8b949e; font-size:12px; }}
|
|
156
|
+
.legend-item text {{ fill:#8b949e; font-size:11px; }}
|
|
157
|
+
</style>
|
|
158
|
+
<rect width="100%" height="100%" fill="#0d1117" />
|
|
159
|
+
<text x="20" y="30" class="title">ctxgraph — Knowledge Graph</text>
|
|
160
|
+
<text x="20" y="48" class="subtitle">{len(nodes)} nodes, {n_edges} edges</text>
|
|
161
|
+
<g id="edges">{''.join(edge_lines)}</g>
|
|
162
|
+
<g id="nodes">{''.join(node_groups)}</g>
|
|
163
|
+
<g id="legend" transform="translate(20, {SVG_HEIGHT - 100})">
|
|
164
|
+
<rect x="0" y="0" width="220" height="80" rx="6" fill="#161b22" stroke="#30363d" stroke-width="1" />
|
|
165
|
+
<circle cx="16" cy="18" r="5" fill="#58a6ff" /><text x="28" y="22" class="legend-item">File</text>
|
|
166
|
+
<circle cx="16" cy="38" r="5" fill="#d29922" /><text x="28" y="42" class="legend-item">Class</text>
|
|
167
|
+
<circle cx="16" cy="58" r="5" fill="#3fb950" /><text x="28" y="62" class="legend-item">Function</text>
|
|
168
|
+
<line x1="110" y1="16" x2="140" y2="16" stroke="#58a6ff" stroke-dasharray="4,2" stroke-width="1.5"/><text x="146" y="20" class="legend-item">Import</text>
|
|
169
|
+
<line x1="110" y1="36" x2="140" y2="36" stroke="#3fb950" stroke-dasharray="2,2" stroke-width="1.5"/><text x="146" y="40" class="legend-item">Call</text>
|
|
170
|
+
<line x1="110" y1="56" x2="140" y2="56" stroke="#d29922" stroke-width="1.5"/><text x="146" y="60" class="legend-item">Defines</text>
|
|
171
|
+
</g>
|
|
172
|
+
</svg>"""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _esc(text: str) -> str:
|
|
176
|
+
return text.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
177
|
+
|
|
178
|
+
|
|
65
179
|
def _short_path(path: str) -> str:
|
|
66
180
|
parts = path.split("/")
|
|
67
181
|
if len(parts) > 3:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctxgraph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -37,9 +37,11 @@ pip install ctxgraph
|
|
|
37
37
|
ctx build # Build knowledge graph
|
|
38
38
|
ctx capsule "fix JWT expiry" # 92-99% fewer tokens vs raw code
|
|
39
39
|
ccg "fix the login redirect bug" # Launch Claude with context pre-loaded
|
|
40
|
-
ctx view # Interactive D3.js visualization
|
|
40
|
+
ctx view # Interactive D3.js visualization (or --svg for static)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
<img src="docs/graph.svg" alt="ctxgraph knowledge graph visualization" width="100%">
|
|
44
|
+
|
|
43
45
|
---
|
|
44
46
|
|
|
45
47
|
## Why ctxgraph?
|
|
@@ -248,7 +250,7 @@ ccg --mode deep "redesign the database schema" # Deep mode
|
|
|
248
250
|
exclude = ["legacy/*", "vendor/*"]
|
|
249
251
|
|
|
250
252
|
[ai]
|
|
251
|
-
provider = "ollama" # ollama, claude, openai, custom
|
|
253
|
+
provider = "ollama" # ollama, claude, openai, azure, custom
|
|
252
254
|
model = "qwen2.5-coder:7b"
|
|
253
255
|
endpoint = "http://localhost:11434"
|
|
254
256
|
|
|
@@ -265,6 +267,7 @@ max_depth = 2
|
|
|
265
267
|
| `CTXGRAPH_ENDPOINT` | `ai.endpoint` |
|
|
266
268
|
| `ANTHROPIC_API_KEY` | Claude API |
|
|
267
269
|
| `OPENAI_API_KEY` | OpenAI API |
|
|
270
|
+
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API |
|
|
268
271
|
|
|
269
272
|
```bash
|
|
270
273
|
# Ollama (default)
|
|
@@ -276,6 +279,13 @@ CTXGRAPH_PROVIDER=claude CTXGRAPH_MODEL=claude-sonnet-4-20250514 ctx capsule "qu
|
|
|
276
279
|
# OpenAI
|
|
277
280
|
CTXGRAPH_PROVIDER=openai CTXGRAPH_MODEL=gpt-4o ctx capsule "query"
|
|
278
281
|
|
|
282
|
+
# Azure OpenAI
|
|
283
|
+
CTXGRAPH_PROVIDER=azure \
|
|
284
|
+
CTXGRAPH_MODEL=gpt-4o \
|
|
285
|
+
CTXGRAPH_ENDPOINT=https://my-resource.openai.azure.com \
|
|
286
|
+
AZURE_OPENAI_API_KEY=sk-... \
|
|
287
|
+
ctx capsule "query"
|
|
288
|
+
|
|
279
289
|
# Custom (OpenAI-compatible)
|
|
280
290
|
CTXGRAPH_PROVIDER=custom CTXGRAPH_ENDPOINT=http://my-api/v1 ctx capsule "query"
|
|
281
291
|
```
|
|
@@ -307,6 +317,185 @@ ctx capsule "extract payment processing into separate module" --mode deep
|
|
|
307
317
|
|
|
308
318
|
---
|
|
309
319
|
|
|
320
|
+
## Framework Integrations
|
|
321
|
+
|
|
322
|
+
ctxgraph can be used as a Python library — not just a CLI. This makes it easy to plug into LangChain, LangGraph, OpenAI Agents, or any custom AI pipeline.
|
|
323
|
+
|
|
324
|
+
### Python API
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from pathlib import Path
|
|
328
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
329
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
330
|
+
from ctxgraph.graph.query import search_relevant_nodes
|
|
331
|
+
|
|
332
|
+
# 1. Build the graph (one-time setup)
|
|
333
|
+
stats = build_graph(Path("/path/to/project"))
|
|
334
|
+
print(f"Built: {stats['total_nodes']} nodes, {stats['total_edges']} edges")
|
|
335
|
+
|
|
336
|
+
# 2. Get storage for an existing graph
|
|
337
|
+
storage = get_storage(Path("/path/to/project"))
|
|
338
|
+
|
|
339
|
+
# 3. Generate a context capsule (token-efficient text)
|
|
340
|
+
capsule = render_capsule(storage, "fix JWT token validation", max_nodes=20)
|
|
341
|
+
print(capsule)
|
|
342
|
+
|
|
343
|
+
# 4. Search for relevant nodes programmatically
|
|
344
|
+
results = search_relevant_nodes(storage, "auth login", max_nodes=10, max_depth=2)
|
|
345
|
+
for node, score in results:
|
|
346
|
+
print(f" {node.type}:{node.name} (score={score})")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### LangChain
|
|
350
|
+
|
|
351
|
+
Inject ctxgraph capsules directly into your LangChain prompts — dramatically reducing token usage while providing precise code context.
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from pathlib import Path
|
|
355
|
+
from langchain_openai import ChatOpenAI
|
|
356
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
357
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
358
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
359
|
+
|
|
360
|
+
# Build graph once
|
|
361
|
+
build_graph(Path("./my_project"))
|
|
362
|
+
storage = get_storage(Path("./my_project"))
|
|
363
|
+
|
|
364
|
+
# Generate context for a specific task
|
|
365
|
+
context = render_capsule(storage, "user authentication flow", max_nodes=20)
|
|
366
|
+
|
|
367
|
+
prompt = ChatPromptTemplate.from_messages([
|
|
368
|
+
("system", "You are a senior Python developer. Use the code context below to answer the question.\n\n{context}"),
|
|
369
|
+
("user", "{question}"),
|
|
370
|
+
])
|
|
371
|
+
|
|
372
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
373
|
+
chain = prompt | llm
|
|
374
|
+
|
|
375
|
+
response = chain.invoke({
|
|
376
|
+
"context": context,
|
|
377
|
+
"question": "Where is the login rate limiter implemented?",
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### LangGraph
|
|
382
|
+
|
|
383
|
+
Use ctxgraph as a tool within a LangGraph agent — the agent requests context capsules when it needs to understand the codebase.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from pathlib import Path
|
|
387
|
+
from typing import Literal
|
|
388
|
+
from langgraph.graph import StateGraph, MessagesState
|
|
389
|
+
from langgraph.prebuilt import ToolNode
|
|
390
|
+
from langchain_openai import ChatOpenAI
|
|
391
|
+
from langchain_core.tools import tool
|
|
392
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
393
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
394
|
+
|
|
395
|
+
# Pre-build graph
|
|
396
|
+
build_graph(Path("./my_project"))
|
|
397
|
+
storage = get_storage(Path("./my_project"))
|
|
398
|
+
|
|
399
|
+
@tool
|
|
400
|
+
def code_context(task: str) -> str:
|
|
401
|
+
"""Get code context relevant to a task. Use this before answering code questions."""
|
|
402
|
+
return render_capsule(storage, task, max_nodes=20)
|
|
403
|
+
|
|
404
|
+
tools = [code_context]
|
|
405
|
+
tool_node = ToolNode(tools)
|
|
406
|
+
|
|
407
|
+
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
|
|
408
|
+
|
|
409
|
+
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]:
|
|
410
|
+
return "tools" if state["messages"][-1].tool_calls else "__end__"
|
|
411
|
+
|
|
412
|
+
def call_model(state: MessagesState):
|
|
413
|
+
return {"messages": [model.invoke(state["messages"])]}
|
|
414
|
+
|
|
415
|
+
graph = StateGraph(MessagesState)
|
|
416
|
+
graph.add_node("agent", call_model)
|
|
417
|
+
graph.add_node("tools", tool_node)
|
|
418
|
+
graph.set_entry_point("agent")
|
|
419
|
+
graph.add_conditional_edges("agent", should_continue)
|
|
420
|
+
graph.add_edge("tools", "agent")
|
|
421
|
+
|
|
422
|
+
app = graph.compile()
|
|
423
|
+
|
|
424
|
+
for chunk in app.stream({"messages": [("user", "Find the bug in the payment processor")]}):
|
|
425
|
+
for node, msg in chunk.items():
|
|
426
|
+
print(f"[{node}]: {msg['messages'][0].content[:200] if msg.get('messages') else ''}")
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### OpenAI Agents SDK
|
|
430
|
+
|
|
431
|
+
Use ctxgraph with the official OpenAI Agents SDK (also works with Azure OpenAI via `AzureOpenAIChatCompletionAgent`).
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from pathlib import Path
|
|
435
|
+
from openai import AzureOpenAI # or OpenAI for standard API
|
|
436
|
+
from agents import Agent, Runner, function_tool
|
|
437
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
438
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
439
|
+
|
|
440
|
+
# Pre-build the graph
|
|
441
|
+
build_graph(Path("./my_project"))
|
|
442
|
+
storage = get_storage(Path("./my_project"))
|
|
443
|
+
|
|
444
|
+
@function_tool
|
|
445
|
+
def fetch_code_context(task_description: str) -> str:
|
|
446
|
+
"""Retrieve relevant code context for a development task."""
|
|
447
|
+
return render_capsule(storage, task_description, max_nodes=20)
|
|
448
|
+
|
|
449
|
+
agent = Agent(
|
|
450
|
+
name="Code Assistant",
|
|
451
|
+
instructions="You are a helpful coding assistant. Use the code context tool to understand the codebase before answering.",
|
|
452
|
+
model="gpt-4o", # or AzureOpenAIChatCompletionAgent(deployment="gpt-4o", ...)
|
|
453
|
+
tools=[fetch_code_context],
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
result = Runner.run_sync(
|
|
457
|
+
agent,
|
|
458
|
+
"How does the JWT authentication middleware work?",
|
|
459
|
+
)
|
|
460
|
+
print(result.final_output)
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Azure OpenAI with Custom Agent
|
|
464
|
+
|
|
465
|
+
For Azure OpenAI, configure the client directly and inject ctxgraph context:
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
import os
|
|
469
|
+
from openai import AzureOpenAI
|
|
470
|
+
from pathlib import Path
|
|
471
|
+
from ctxgraph.graph.builder import build_graph, get_storage
|
|
472
|
+
from ctxgraph.capsule.renderer import render_capsule
|
|
473
|
+
|
|
474
|
+
# Build graph
|
|
475
|
+
build_graph(Path("./my_project"))
|
|
476
|
+
storage = get_storage(Path("./my_project"))
|
|
477
|
+
|
|
478
|
+
# Generate context capsule
|
|
479
|
+
context = render_capsule(storage, "authentication and authorization", max_nodes=25)
|
|
480
|
+
|
|
481
|
+
client = AzureOpenAI(
|
|
482
|
+
api_version="2024-08-01-preview",
|
|
483
|
+
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
|
484
|
+
api_key=os.environ["AZURE_OPENAI_API_KEY"],
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
response = client.chat.completions.create(
|
|
488
|
+
model="gpt-4o", # deployment name
|
|
489
|
+
messages=[
|
|
490
|
+
{"role": "system", "content": f"You are a senior Python developer. Use the code context below.\n\n{context}"},
|
|
491
|
+
{"role": "user", "content": "Explain the role-based access control (RBAC) implementation."},
|
|
492
|
+
],
|
|
493
|
+
)
|
|
494
|
+
print(response.choices[0].message.content)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
310
499
|
## Development
|
|
311
500
|
|
|
312
501
|
```bash
|
|
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
|