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: `neo4j://localhost:7687` (the `bolt://` form is also supported)
- Username: `neo4j`
- Database: `neo4j`
- Password: Generated automatically (check logs)

### 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)")
@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 @@ -187,6 +189,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 @@ -209,7 +212,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
25 changes: 22 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"
except ImportError:
pass

Expand All @@ -70,7 +73,23 @@ def _llm_generate(client, provider: str, prompt: str, system: str = "") -> str:
system=system,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
if not response.content:
return ""
# 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:
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
4 changes: 2 additions & 2 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 @@ -300,6 +301,7 @@ 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
data_dir.mkdir(parents=True, exist_ok=True)
(data_dir / "documents").mkdir(exist_ok=True)

Expand All @@ -308,8 +310,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,7 +19,12 @@ 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

import anthropic

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ 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)
{% raw %}
client_kwargs = {"api_key": settings.anthropic_api_key}
if settings.anthropic_base_url:
client_kwargs["base_url"] = settings.anthropic_base_url
{% endraw %}
client = anthropic.AsyncAnthropic(**client_kwargs)

# ---------------------------------------------------------------------------
# Tool definitions for {{ domain.name }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ TOOLS = [
]

{% raw %}
model = ChatAnthropic(
model="claude-sonnet-4-20250514",
api_key=settings.anthropic_api_key,
)
_anthropic_kwargs = {"model": "claude-sonnet-4-20250514", "api_key": settings.anthropic_api_key}
if settings.anthropic_base_url:
_anthropic_kwargs["base_url"] = settings.anthropic_base_url
model = ChatAnthropic(**_anthropic_kwargs)
{% endraw %}

graph = create_react_agent(model, TOOLS, prompt=SYSTEM_PROMPT)

Expand Down Expand Up @@ -178,4 +179,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,7 +20,12 @@ 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 %}

from pydantic_ai import Agent, RunContext

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ 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 %}


_main_loop: asyncio.AbstractEventLoop | None = None
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,9 @@ services:
NEO4J_PASSWORD: ${NEO4J_PASSWORD:-password}
{% if 'anthropic' in framework or framework == 'pydanticai' or framework == 'claude-agent-sdk' %}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
{% if anthropic_base_url %}
ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL}
{% endif %}
{% endif %}
{% if 'openai' in framework or framework == 'langgraph' %}
OPENAI_API_KEY: ${OPENAI_API_KEY}
Expand Down
3 changes: 2 additions & 1 deletion src/create_context_graph/templates/base/dot_env.j2
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ NEO4J_PASSWORD={{ neo4j_password }}

# LLM API Keys
ANTHROPIC_API_KEY={{ anthropic_api_key }}
OPENAI_API_KEY={{ openai_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 }}

# Application
Expand Down
3 changes: 2 additions & 1 deletion src/create_context_graph/templates/base/dot_env_example.j2
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ NEO4J_PASSWORD=your-password-here

# LLM API Keys
ANTHROPIC_API_KEY=your-anthropic-key-here
OPENAI_API_KEY= # Optional: enables OpenAI embeddings for conversation memory (local sentence-transformers used by default)
# ANTHROPIC_BASE_URL=http://127.0.0.1:8082 # Override default Anthropic API endpoint
OPENAI_API_KEY= # Optional: enables OpenAI embeddings for conversation memory (local sentence-transformers used by default)
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