Skip to content
Open
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,29 @@ make start # Start backend (port 8000) + frontend (port 3000)
- **Backend API:** http://localhost:8000/docs — FastAPI auto-generated docs
- **Neo4j Browser:** http://localhost:7474 — Query the graph directly

## Known Issues

### Neo4j Authentication

If Neo4j connection fails or the password is lost, reset the database:

```bash
make neo4j-stop && rm -rf ~/.local/share/neo4j-local/default && make neo4j-start
cat /tmp/neo4j-local.log # View generated password
Comment on lines +123 to +127
```

**Connection Details:**
- URI: `bolt://localhost:7687`
Comment thread
toughcoding marked this conversation as resolved.
Outdated
- Username: `neo4j`
- Database: `neo4j`
- Password: Generated automatically (check logs)

Comment on lines +123 to +135

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The neo4j-local troubleshooting guidance says the password is “Generated automatically (check logs)” and references ~/.local/share/neo4j-local/default, but the existing docs/docs/how-to/use-neo4j-local.md states default credentials are neo4j/neo4j and points to a different data directory (usually ~/.neo4j-local/data/). Please align these instructions with the documented behavior used elsewhere in the repo to avoid sending users to the wrong reset location / credentials.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

### Common Problems

- **Port conflicts:** Ensure ports 7687 and 7474 are available
- **Permissions:** Verify write access to `~/.local/share/neo4j-local/`
- **Dependencies:** `neo4j-local` requires Node.js; Docker mode requires Docker Desktop

## Supported Domains

22 industry domains, each with a purpose-built ontology, sample data, agent tools, and demo scenarios:
Expand Down
21 changes: 19 additions & 2 deletions src/create_context_graph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
@click.option("--neo4j-aura-env", type=click.Path(exists=True), help="Path to Neo4j Aura .env file with credentials")
@click.option("--neo4j-local", is_flag=True, help="Use @johnymontana/neo4j-local for local Neo4j (no Docker)")
@click.option("--anthropic-api-key", envvar="ANTHROPIC_API_KEY", help="Anthropic API key for LLM generation")
@click.option("--anthropic-base-url", envvar="ANTHROPIC_BASE_URL", help="Anthropic-compatible API base URL (e.g., http://127.0.0.1:8082)")
@click.option("--openai-api-key", envvar="OPENAI_API_KEY", help="OpenAI API key for LLM generation")
@click.option("--google-api-key", envvar="GOOGLE_API_KEY", help="Google/Gemini API key (required for google-adk framework)")
Comment on lines 50 to 55

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new CLI option --anthropic-base-url is introduced here, but the CLI test suite currently has coverage for --google-api-key and --openai-api-key flowing into the rendered .env (see tests/test_cli.py). Consider adding a similar test to assert ANTHROPIC_BASE_URL=... is present in the generated .env when this flag is provided, to prevent regressions in local inference support.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

@click.option("--custom-domain", type=str, help="Natural language description for custom domain generation (requires --anthropic-api-key)")
Expand All @@ -73,6 +74,7 @@ def main(
neo4j_aura_env: str | None,
neo4j_local: bool,
anthropic_api_key: str | None,
anthropic_base_url: str | None,
openai_api_key: str | None,
google_api_key: str | None,
custom_domain: str | None,
Expand Down Expand Up @@ -124,7 +126,7 @@ def main(
console.print("[bold]Generating custom domain ontology...[/bold]")
try:
custom_ontology, custom_domain_yaml = generate_custom_domain(
custom_domain, anthropic_api_key
custom_domain, anthropic_api_key, base_url=anthropic_base_url
)
except ValueError as e:
console.print(f"[red]Error:[/red] {e}")
Expand Down Expand Up @@ -169,6 +171,7 @@ def main(
neo4j_password=neo4j_password,
neo4j_type=neo4j_type_resolved,
anthropic_api_key=anthropic_api_key,
anthropic_base_url=anthropic_base_url,
openai_api_key=openai_api_key,
google_api_key=google_api_key,
generate_data=demo_data,
Expand All @@ -185,7 +188,21 @@ def main(
# Launch interactive wizard
from create_context_graph.wizard import run_wizard

config = run_wizard()
config = run_wizard(
project_name=project_name,
domain=domain,
framework=framework,
anthropic_api_key=anthropic_api_key,
Comment on lines +215 to +219
anthropic_base_url=anthropic_base_url,
openai_api_key=openai_api_key,
connector=connector,
demo_data=demo_data,
neo4j_uri=neo4j_uri,
neo4j_username=neo4j_username,
neo4j_password=neo4j_password,
neo4j_local=neo4j_local,
neo4j_aura_env=neo4j_aura_env,
)

# Resolve output directory
out = Path(output_dir) if output_dir else Path.cwd() / config.project_slug
Expand Down
1 change: 1 addition & 0 deletions src/create_context_graph/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class ProjectConfig(BaseModel):
neo4j_password: str = Field(default="password")
neo4j_type: Literal["docker", "existing", "aura", "local"] = Field(default="docker")
anthropic_api_key: str | None = Field(default=None)
anthropic_base_url: str | None = Field(default=None)
openai_api_key: str | None = Field(default=None)
google_api_key: str | None = Field(default=None)
generate_data: bool = Field(default=False)
Expand Down
3 changes: 2 additions & 1 deletion src/create_context_graph/custom_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,14 @@ def generate_custom_domain(
api_key: str,
provider: str = "anthropic",
max_retries: int = 3,
base_url: str | None = None,
) -> tuple[DomainOntology, str]:
"""Generate a complete domain ontology from a natural language description.

Returns (DomainOntology, raw_yaml_string) on success.
Raises ValueError if generation fails after max_retries.
"""
client, resolved_provider = _get_llm_client(api_key, provider)
client, resolved_provider = _get_llm_client(api_key, provider, base_url)
if client is None:
raise ValueError(
"Could not initialize LLM client. Install 'anthropic' or 'openai' package."
Expand Down
23 changes: 20 additions & 3 deletions src/create_context_graph/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@
# ---------------------------------------------------------------------------


def _get_llm_client(api_key: str, provider: str = "anthropic"):
def _get_llm_client(api_key: str, provider: str = "anthropic", base_url: str | None = None):
"""Get an LLM client for generation."""
if provider == "anthropic":
try:
import anthropic
return anthropic.Anthropic(api_key=api_key), "anthropic"
client_kwargs = {"api_key": api_key}
if base_url:
client_kwargs["base_url"] = base_url
return anthropic.Anthropic(**client_kwargs), "anthropic"
Comment thread
toughcoding marked this conversation as resolved.
except ImportError:
pass

Expand All @@ -70,7 +73,21 @@ def _llm_generate(client, provider: str, prompt: str, system: str = "") -> str:
system=system,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
# Handle both text content and thinking blocks
content = response.content[0]
if hasattr(content, 'text'):
return content.text
elif hasattr(content, 'thinking'):
# Find the next text block after thinking
for block in response.content:
if hasattr(block, 'text'):
return block.text
# Fallback: extract text from all blocks
text_parts = []
for block in response.content:
Comment thread
toughcoding marked this conversation as resolved.
if hasattr(block, 'text'):
text_parts.append(block.text)
return ''.join(text_parts)
elif provider == "openai":
messages = []
if system:
Expand Down
8 changes: 7 additions & 1 deletion src/create_context_graph/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,15 @@ def _merge_base(base: dict, domain_data: dict) -> dict:

def load_domain(domain_id: str) -> DomainOntology:
"""Load a domain ontology by ID, merging with base definitions."""
# Check main domains directory first, then custom domains
domains_dir = _get_domains_path()
domain_path = domains_dir / f"{domain_id}.yaml"


if not domain_path.exists():
# Try custom domains directory
custom_domains_dir = _get_custom_domains_path()
domain_path = custom_domains_dir / f"{domain_id}.yaml"

Comment on lines +244 to +249
if not domain_path.exists():
raise FileNotFoundError(f"Domain ontology not found: {domain_id}")

Expand Down
7 changes: 4 additions & 3 deletions src/create_context_graph/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def _context(self) -> dict:
"neo4j_password": self.config.neo4j_password,
"neo4j_type": self.config.neo4j_type,
"anthropic_api_key": self.config.anthropic_api_key or "",
"anthropic_base_url": self.config.anthropic_base_url or "",
"openai_api_key": self.config.openai_api_key or "",
"google_api_key": self.config.google_api_key or "",
"system_prompt": self.ontology.system_prompt,
Expand Down Expand Up @@ -203,7 +204,7 @@ def _render_backend(self, backend_dir: Path, ctx: dict) -> None:
agent_template = f"backend/agents/{fw_key}/agent.py.j2"
try:
self._render_template(agent_template, backend_dir / "app" / "agent.py", ctx)
except Exception:
except Exception as e:
# Fallback: render a minimal agent stub
self._render_template(
"backend/shared/agent_stub.py.j2",
Expand Down Expand Up @@ -300,6 +301,8 @@ def _render_cypher(self, cypher_dir: Path, ctx: dict) -> None:

def _render_data(self, data_dir: Path, ctx: dict) -> None:
"""Copy ontology and create data directory structure."""
from create_context_graph.ontology import _get_domains_path

Comment thread
johnymontana marked this conversation as resolved.
Outdated
data_dir.mkdir(parents=True, exist_ok=True)
(data_dir / "documents").mkdir(exist_ok=True)

Expand All @@ -308,8 +311,6 @@ def _render_data(self, data_dir: Path, ctx: dict) -> None:
# Write custom domain YAML directly
(data_dir / "ontology.yaml").write_text(self.config.custom_domain_yaml)
else:
from create_context_graph.ontology import _get_domains_path

domain_yaml = _get_domains_path() / f"{self.config.domain}.yaml"
if domain_yaml.exists():
shutil.copy2(domain_yaml, data_dir / "ontology.yaml")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ if not os.environ.get("ANTHROPIC_API_KEY"):
_key = dotenv_values("../.env").get("ANTHROPIC_API_KEY", "")
if _key:
os.environ["ANTHROPIC_API_KEY"] = _key

# Set ANTHROPIC_BASE_URL if configured
{% endraw %}
{% if anthropic_base_url %}
os.environ["ANTHROPIC_BASE_URL"] = "{{ anthropic_base_url }}"
{% endif %}
Comment on lines +23 to +27
{% raw %}
{% endraw %}
Comment thread
toughcoding marked this conversation as resolved.
Outdated

import anthropic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ IMPORTANT: You MUST use the available tools to query the knowledge graph before

CRITICAL: Call tools DIRECTLY without any introductory text. Do NOT say "I'll search for..." or "Let me look up..." before calling a tool — just call the tool immediately. Only generate text AFTER you have received the tool results and are ready to provide your final answer."""

client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
client_kwargs = {"api_key": settings.anthropic_api_key}
{% if anthropic_base_url %}
client_kwargs["base_url"] = "{{ anthropic_base_url }}"
{% endif %}
Comment thread
toughcoding marked this conversation as resolved.
Outdated
client = anthropic.AsyncAnthropic(**client_kwargs)

# ---------------------------------------------------------------------------
# Tool definitions for {{ domain.name }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from __future__ import annotations
import json
import uuid

import langgraph

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import langgraph appears unused in the generated agent (everything referenced comes from langgraph.prebuilt). Consider removing it to avoid unused-import lint issues in the scaffolded project.

Suggested change
import langgraph

Copilot uses AI. Check for mistakes.
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
Expand Down Expand Up @@ -41,7 +42,6 @@ async def {{ tool.name }}({% for param in tool.parameters %}{{ param.name }}: {{

{% endfor %}

{% raw %}
@tool
async def run_cypher(query: str, parameters: str = "{}") -> str:
"""Execute a read-only Cypher query against the knowledge graph."""
Expand All @@ -56,13 +56,11 @@ async def run_cypher(query: str, parameters: str = "{}") -> str:
except Exception as e:
return json.dumps({"error": f"Cypher query failed: {e}"})


@tool
async def get_graph_schema() -> str:
"""Get the knowledge graph schema (node labels and relationship types)."""
result = await get_schema()
return json.dumps(result, default=str)
{% endraw %}

TOOLS = [
{% for tool in agent_tools %}
Expand All @@ -72,10 +70,12 @@ TOOLS = [
get_graph_schema,
]

{% raw %}
model = ChatAnthropic(
model="claude-sonnet-4-20250514",
api_key=settings.anthropic_api_key,
{% if anthropic_base_url %}
base_url="{{ anthropic_base_url }}",
{% endif %}
Comment thread
toughcoding marked this conversation as resolved.
Outdated
)

graph = create_react_agent(model, TOOLS, prompt=SYSTEM_PROMPT)
Expand Down Expand Up @@ -178,4 +178,3 @@ async def handle_message_stream(message: str, session_id: str | None = None) ->
"session_id": session_id,
"graph_data": None,
}
{% endraw %}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ if not os.environ.get("ANTHROPIC_API_KEY"):
_key = dotenv_values("../.env").get("ANTHROPIC_API_KEY", "")
if _key:
os.environ["ANTHROPIC_API_KEY"] = _key

# Set ANTHROPIC_BASE_URL if configured
{% endraw %}
{% if anthropic_base_url %}
os.environ["ANTHROPIC_BASE_URL"] = "{{ anthropic_base_url }}"
{% endif %}
{% raw %}
{% endraw %}
Comment thread
toughcoding marked this conversation as resolved.
Outdated

from pydantic_ai import Agent, RunContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ if not os.environ.get("ANTHROPIC_API_KEY"):
_key = dotenv_values("../.env").get("ANTHROPIC_API_KEY", "")
if _key:
os.environ["ANTHROPIC_API_KEY"] = _key

# Set ANTHROPIC_BASE_URL if configured
{% endraw %}
{% if anthropic_base_url %}
os.environ["ANTHROPIC_BASE_URL"] = "{{ anthropic_base_url }}"
Comment on lines 32 to +34
{% endif %}
{% raw %}
{% endraw %}
Comment thread
toughcoding marked this conversation as resolved.
Outdated


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Settings(BaseSettings):
neo4j_username: str = "{{ neo4j_username }}"
neo4j_password: str = "{{ neo4j_password }}"
anthropic_api_key: str = ""
anthropic_base_url: str = ""
openai_api_key: str = ""
domain_id: str = "{{ domain.id }}"
backend_port: int = 8000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ services:
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-password}
{% if 'anthropic' in framework or framework == 'pydanticai' or framework == 'claude-agent-sdk' %}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL}
{% endif %}
{% if 'openai' in framework or framework == 'langgraph' %}
OPENAI_API_KEY: ${OPENAI_API_KEY}
Expand Down
2 changes: 2 additions & 0 deletions src/create_context_graph/templates/base/dot_env.j2
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ NEO4J_PASSWORD={{ neo4j_password }}

# LLM API Keys
ANTHROPIC_API_KEY={{ anthropic_api_key }}
{% if anthropic_base_url %}ANTHROPIC_BASE_URL={{ anthropic_base_url }}
{% endif %}
OPENAI_API_KEY={{ openai_api_key }}
GOOGLE_API_KEY={{ google_api_key }}

Expand Down
1 change: 1 addition & 0 deletions src/create_context_graph/templates/base/dot_env_example.j2
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ NEO4J_PASSWORD=your-password-here

# LLM API Keys
ANTHROPIC_API_KEY=your-anthropic-key-here
# ANTHROPIC_BASE_URL=http://127.0.0.1:8082 # Override default Anthropic API endpoint
OPENAI_API_KEY=your-openai-key-here
GOOGLE_API_KEY=your-google-api-key-here # Required for google-adk framework
# ANTHROPIC_MODEL=claude-sonnet-4-20250514 # Override default model for Claude Agent SDK
Expand Down
Loading
Loading