Skip to content

Commit 29fba0c

Browse files
author
tomd
committed
Add local model inference support and improve Neo4j troubleshooting
- Add support for localhost model inference (local LLM servers) - Extend CLI and wizard to accept local model endpoints - Add Google/Gemini API key support for google-adk framework - Add --demo shortcut flag for quick demo setup - Improve README with comprehensive Neo4j authentication troubleshooting
1 parent 91f955d commit 29fba0c

16 files changed

Lines changed: 281 additions & 126 deletions

File tree

src/create_context_graph/cli.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
@click.option("--neo4j-aura-env", type=click.Path(exists=True), help="Path to Neo4j Aura .env file with credentials")
5151
@click.option("--neo4j-local", is_flag=True, help="Use @johnymontana/neo4j-local for local Neo4j (no Docker)")
5252
@click.option("--anthropic-api-key", envvar="ANTHROPIC_API_KEY", help="Anthropic API key for LLM generation")
53+
@click.option("--anthropic-base-url", envvar="ANTHROPIC_BASE_URL", help="Anthropic-compatible API base URL (e.g., http://127.0.0.1:8082)")
5354
@click.option("--openai-api-key", envvar="OPENAI_API_KEY", help="OpenAI API key for LLM generation")
5455
@click.option("--google-api-key", envvar="GOOGLE_API_KEY", help="Google/Gemini API key (required for google-adk framework)")
5556
@click.option("--custom-domain", type=str, help="Natural language description for custom domain generation (requires --anthropic-api-key)")
@@ -73,6 +74,7 @@ def main(
7374
neo4j_aura_env: str | None,
7475
neo4j_local: bool,
7576
anthropic_api_key: str | None,
77+
anthropic_base_url: str | None,
7678
openai_api_key: str | None,
7779
google_api_key: str | None,
7880
custom_domain: str | None,
@@ -124,7 +126,7 @@ def main(
124126
console.print("[bold]Generating custom domain ontology...[/bold]")
125127
try:
126128
custom_ontology, custom_domain_yaml = generate_custom_domain(
127-
custom_domain, anthropic_api_key
129+
custom_domain, anthropic_api_key, base_url=anthropic_base_url
128130
)
129131
except ValueError as e:
130132
console.print(f"[red]Error:[/red] {e}")
@@ -169,6 +171,7 @@ def main(
169171
neo4j_password=neo4j_password,
170172
neo4j_type=neo4j_type_resolved,
171173
anthropic_api_key=anthropic_api_key,
174+
anthropic_base_url=anthropic_base_url,
172175
openai_api_key=openai_api_key,
173176
google_api_key=google_api_key,
174177
generate_data=demo_data,
@@ -185,7 +188,21 @@ def main(
185188
# Launch interactive wizard
186189
from create_context_graph.wizard import run_wizard
187190

188-
config = run_wizard()
191+
config = run_wizard(
192+
project_name=project_name,
193+
domain=domain,
194+
framework=framework,
195+
anthropic_api_key=anthropic_api_key,
196+
anthropic_base_url=anthropic_base_url,
197+
openai_api_key=openai_api_key,
198+
connector=connector,
199+
demo_data=demo_data,
200+
neo4j_uri=neo4j_uri,
201+
neo4j_username=neo4j_username,
202+
neo4j_password=neo4j_password,
203+
neo4j_local=neo4j_local,
204+
neo4j_aura_env=neo4j_aura_env,
205+
)
189206

190207
# Resolve output directory
191208
out = Path(output_dir) if output_dir else Path.cwd() / config.project_slug

src/create_context_graph/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class ProjectConfig(BaseModel):
7373
neo4j_password: str = Field(default="password")
7474
neo4j_type: Literal["docker", "existing", "aura", "local"] = Field(default="docker")
7575
anthropic_api_key: str | None = Field(default=None)
76+
anthropic_base_url: str | None = Field(default=None)
7677
openai_api_key: str | None = Field(default=None)
7778
google_api_key: str | None = Field(default=None)
7879
generate_data: bool = Field(default=False)

src/create_context_graph/custom_domain.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,14 @@ def generate_custom_domain(
211211
api_key: str,
212212
provider: str = "anthropic",
213213
max_retries: int = 3,
214+
base_url: str | None = None,
214215
) -> tuple[DomainOntology, str]:
215216
"""Generate a complete domain ontology from a natural language description.
216217
217218
Returns (DomainOntology, raw_yaml_string) on success.
218219
Raises ValueError if generation fails after max_retries.
219220
"""
220-
client, resolved_provider = _get_llm_client(api_key, provider)
221+
client, resolved_provider = _get_llm_client(api_key, provider, base_url)
221222
if client is None:
222223
raise ValueError(
223224
"Could not initialize LLM client. Install 'anthropic' or 'openai' package."

src/create_context_graph/generator.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@
4242
# ---------------------------------------------------------------------------
4343

4444

45-
def _get_llm_client(api_key: str, provider: str = "anthropic"):
45+
def _get_llm_client(api_key: str, provider: str = "anthropic", base_url: str | None = None):
4646
"""Get an LLM client for generation."""
4747
if provider == "anthropic":
4848
try:
4949
import anthropic
50-
return anthropic.Anthropic(api_key=api_key), "anthropic"
50+
client_kwargs = {"api_key": api_key}
51+
if base_url:
52+
client_kwargs["base_url"] = base_url
53+
return anthropic.Anthropic(**client_kwargs), "anthropic"
5154
except ImportError:
5255
pass
5356

@@ -70,7 +73,21 @@ def _llm_generate(client, provider: str, prompt: str, system: str = "") -> str:
7073
system=system,
7174
messages=[{"role": "user", "content": prompt}],
7275
)
73-
return response.content[0].text
76+
# Handle both text content and thinking blocks
77+
content = response.content[0]
78+
if hasattr(content, 'text'):
79+
return content.text
80+
elif hasattr(content, 'thinking'):
81+
# Find the next text block after thinking
82+
for block in response.content:
83+
if hasattr(block, 'text'):
84+
return block.text
85+
# Fallback: extract text from all blocks
86+
text_parts = []
87+
for block in response.content:
88+
if hasattr(block, 'text'):
89+
text_parts.append(block.text)
90+
return ''.join(text_parts)
7491
elif provider == "openai":
7592
messages = []
7693
if system:

src/create_context_graph/ontology.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,15 @@ def _merge_base(base: dict, domain_data: dict) -> dict:
238238

239239
def load_domain(domain_id: str) -> DomainOntology:
240240
"""Load a domain ontology by ID, merging with base definitions."""
241+
# Check main domains directory first, then custom domains
241242
domains_dir = _get_domains_path()
242243
domain_path = domains_dir / f"{domain_id}.yaml"
243-
244+
245+
if not domain_path.exists():
246+
# Try custom domains directory
247+
custom_domains_dir = _get_custom_domains_path()
248+
domain_path = custom_domains_dir / f"{domain_id}.yaml"
249+
244250
if not domain_path.exists():
245251
raise FileNotFoundError(f"Domain ontology not found: {domain_id}")
246252

src/create_context_graph/renderer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def _context(self) -> dict:
109109
"neo4j_password": self.config.neo4j_password,
110110
"neo4j_type": self.config.neo4j_type,
111111
"anthropic_api_key": self.config.anthropic_api_key or "",
112+
"anthropic_base_url": self.config.anthropic_base_url or "",
112113
"openai_api_key": self.config.openai_api_key or "",
113114
"google_api_key": self.config.google_api_key or "",
114115
"system_prompt": self.ontology.system_prompt,
@@ -203,7 +204,7 @@ def _render_backend(self, backend_dir: Path, ctx: dict) -> None:
203204
agent_template = f"backend/agents/{fw_key}/agent.py.j2"
204205
try:
205206
self._render_template(agent_template, backend_dir / "app" / "agent.py", ctx)
206-
except Exception:
207+
except Exception as e:
207208
# Fallback: render a minimal agent stub
208209
self._render_template(
209210
"backend/shared/agent_stub.py.j2",
@@ -300,6 +301,8 @@ def _render_cypher(self, cypher_dir: Path, ctx: dict) -> None:
300301

301302
def _render_data(self, data_dir: Path, ctx: dict) -> None:
302303
"""Copy ontology and create data directory structure."""
304+
from create_context_graph.ontology import _get_domains_path
305+
303306
data_dir.mkdir(parents=True, exist_ok=True)
304307
(data_dir / "documents").mkdir(exist_ok=True)
305308

@@ -308,8 +311,6 @@ def _render_data(self, data_dir: Path, ctx: dict) -> None:
308311
# Write custom domain YAML directly
309312
(data_dir / "ontology.yaml").write_text(self.config.custom_domain_yaml)
310313
else:
311-
from create_context_graph.ontology import _get_domains_path
312-
313314
domain_yaml = _get_domains_path() / f"{self.config.domain}.yaml"
314315
if domain_yaml.exists():
315316
shutil.copy2(domain_yaml, data_dir / "ontology.yaml")

src/create_context_graph/templates/backend/agents/anthropic_tools/agent.py.j2

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ if not os.environ.get("ANTHROPIC_API_KEY"):
1919
_key = dotenv_values("../.env").get("ANTHROPIC_API_KEY", "")
2020
if _key:
2121
os.environ["ANTHROPIC_API_KEY"] = _key
22+
23+
# Set ANTHROPIC_BASE_URL if configured
24+
{% endraw %}
25+
{% if anthropic_base_url %}
26+
os.environ["ANTHROPIC_BASE_URL"] = "{{ anthropic_base_url }}"
27+
{% endif %}
28+
{% raw %}
2229
{% endraw %}
2330

2431
import anthropic

src/create_context_graph/templates/backend/agents/claude_agent_sdk/agent.py.j2

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ IMPORTANT: You MUST use the available tools to query the knowledge graph before
2121

2222
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."""
2323

24-
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
24+
client_kwargs = {"api_key": settings.anthropic_api_key}
25+
{% if anthropic_base_url %}
26+
client_kwargs["base_url"] = "{{ anthropic_base_url }}"
27+
{% endif %}
28+
client = anthropic.AsyncAnthropic(**client_kwargs)
2529

2630
# ---------------------------------------------------------------------------
2731
# Tool definitions for {{ domain.name }}

src/create_context_graph/templates/backend/agents/langgraph/agent.py.j2

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ from __future__ import annotations
55
import json
66
import uuid
77

8+
import langgraph
89
from langchain_anthropic import ChatAnthropic
910
from langchain_core.tools import tool
1011
from langgraph.prebuilt import create_react_agent
@@ -41,7 +42,6 @@ async def {{ tool.name }}({% for param in tool.parameters %}{{ param.name }}: {{
4142

4243
{% endfor %}
4344

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

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

6765
TOOLS = [
6866
{% for tool in agent_tools %}
@@ -72,10 +70,12 @@ TOOLS = [
7270
get_graph_schema,
7371
]
7472

75-
{% raw %}
7673
model = ChatAnthropic(
7774
model="claude-sonnet-4-20250514",
7875
api_key=settings.anthropic_api_key,
76+
{% if anthropic_base_url %}
77+
base_url="{{ anthropic_base_url }}",
78+
{% endif %}
7979
)
8080

8181
graph = create_react_agent(model, TOOLS, prompt=SYSTEM_PROMPT)
@@ -178,4 +178,3 @@ async def handle_message_stream(message: str, session_id: str | None = None) ->
178178
"session_id": session_id,
179179
"graph_data": None,
180180
}
181-
{% endraw %}

src/create_context_graph/templates/backend/agents/pydanticai/agent.py.j2

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ if not os.environ.get("ANTHROPIC_API_KEY"):
2020
_key = dotenv_values("../.env").get("ANTHROPIC_API_KEY", "")
2121
if _key:
2222
os.environ["ANTHROPIC_API_KEY"] = _key
23+
24+
# Set ANTHROPIC_BASE_URL if configured
25+
{% endraw %}
26+
{% if anthropic_base_url %}
27+
os.environ["ANTHROPIC_BASE_URL"] = "{{ anthropic_base_url }}"
28+
{% endif %}
29+
{% raw %}
2330
{% endraw %}
2431

2532
from pydantic_ai import Agent, RunContext

0 commit comments

Comments
 (0)