diff --git a/README.md b/README.md index cc66638..8436b50 100644 --- a/README.md +++ b/README.md @@ -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 +``` + +**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: diff --git a/src/create_context_graph/cli.py b/src/create_context_graph/cli.py index ab7fde2..fdfb7a3 100644 --- a/src/create_context_graph/cli.py +++ b/src/create_context_graph/cli.py @@ -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)") @@ -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, @@ -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}") @@ -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, @@ -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, + 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 diff --git a/src/create_context_graph/config.py b/src/create_context_graph/config.py index dd4adad..8067841 100644 --- a/src/create_context_graph/config.py +++ b/src/create_context_graph/config.py @@ -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) diff --git a/src/create_context_graph/custom_domain.py b/src/create_context_graph/custom_domain.py index f4d77ef..768ad7c 100644 --- a/src/create_context_graph/custom_domain.py +++ b/src/create_context_graph/custom_domain.py @@ -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." diff --git a/src/create_context_graph/generator.py b/src/create_context_graph/generator.py index 093ba5a..a0dea6a 100644 --- a/src/create_context_graph/generator.py +++ b/src/create_context_graph/generator.py @@ -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 @@ -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: diff --git a/src/create_context_graph/ontology.py b/src/create_context_graph/ontology.py index b7348b8..76ddb91 100644 --- a/src/create_context_graph/ontology.py +++ b/src/create_context_graph/ontology.py @@ -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" + if not domain_path.exists(): raise FileNotFoundError(f"Domain ontology not found: {domain_id}") diff --git a/src/create_context_graph/renderer.py b/src/create_context_graph/renderer.py index 510e2b1..f6af602 100644 --- a/src/create_context_graph/renderer.py +++ b/src/create_context_graph/renderer.py @@ -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, @@ -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) @@ -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") diff --git a/src/create_context_graph/templates/backend/agents/anthropic_tools/agent.py.j2 b/src/create_context_graph/templates/backend/agents/anthropic_tools/agent.py.j2 index 2dbbacc..722337d 100644 --- a/src/create_context_graph/templates/backend/agents/anthropic_tools/agent.py.j2 +++ b/src/create_context_graph/templates/backend/agents/anthropic_tools/agent.py.j2 @@ -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 %} import anthropic diff --git a/src/create_context_graph/templates/backend/agents/claude_agent_sdk/agent.py.j2 b/src/create_context_graph/templates/backend/agents/claude_agent_sdk/agent.py.j2 index 998de79..681c76a 100644 --- a/src/create_context_graph/templates/backend/agents/claude_agent_sdk/agent.py.j2 +++ b/src/create_context_graph/templates/backend/agents/claude_agent_sdk/agent.py.j2 @@ -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 }} diff --git a/src/create_context_graph/templates/backend/agents/langgraph/agent.py.j2 b/src/create_context_graph/templates/backend/agents/langgraph/agent.py.j2 index 15f9e25..d2c77c9 100644 --- a/src/create_context_graph/templates/backend/agents/langgraph/agent.py.j2 +++ b/src/create_context_graph/templates/backend/agents/langgraph/agent.py.j2 @@ -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) @@ -178,4 +179,3 @@ async def handle_message_stream(message: str, session_id: str | None = None) -> "session_id": session_id, "graph_data": None, } -{% endraw %} diff --git a/src/create_context_graph/templates/backend/agents/pydanticai/agent.py.j2 b/src/create_context_graph/templates/backend/agents/pydanticai/agent.py.j2 index 58471f6..d9d362c 100644 --- a/src/create_context_graph/templates/backend/agents/pydanticai/agent.py.j2 +++ b/src/create_context_graph/templates/backend/agents/pydanticai/agent.py.j2 @@ -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 diff --git a/src/create_context_graph/templates/backend/agents/strands/agent.py.j2 b/src/create_context_graph/templates/backend/agents/strands/agent.py.j2 index aeca983..e10baad 100644 --- a/src/create_context_graph/templates/backend/agents/strands/agent.py.j2 +++ b/src/create_context_graph/templates/backend/agents/strands/agent.py.j2 @@ -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 }}" +{% endif %} _main_loop: asyncio.AbstractEventLoop | None = None diff --git a/src/create_context_graph/templates/backend/shared/config.py.j2 b/src/create_context_graph/templates/backend/shared/config.py.j2 index 0bde5e6..dd885ca 100644 --- a/src/create_context_graph/templates/backend/shared/config.py.j2 +++ b/src/create_context_graph/templates/backend/shared/config.py.j2 @@ -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 diff --git a/src/create_context_graph/templates/base/docker-compose.prod.yml.j2 b/src/create_context_graph/templates/base/docker-compose.prod.yml.j2 index bc065d5..c3893a7 100644 --- a/src/create_context_graph/templates/base/docker-compose.prod.yml.j2 +++ b/src/create_context_graph/templates/base/docker-compose.prod.yml.j2 @@ -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} diff --git a/src/create_context_graph/templates/base/dot_env.j2 b/src/create_context_graph/templates/base/dot_env.j2 index c6c3b87..2b94b02 100644 --- a/src/create_context_graph/templates/base/dot_env.j2 +++ b/src/create_context_graph/templates/base/dot_env.j2 @@ -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 diff --git a/src/create_context_graph/templates/base/dot_env_example.j2 b/src/create_context_graph/templates/base/dot_env_example.j2 index 4698a59..58c16dd 100644 --- a/src/create_context_graph/templates/base/dot_env_example.j2 +++ b/src/create_context_graph/templates/base/dot_env_example.j2 @@ -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 diff --git a/src/create_context_graph/wizard.py b/src/create_context_graph/wizard.py index 3b6c7c6..4aa67fb 100644 --- a/src/create_context_graph/wizard.py +++ b/src/create_context_graph/wizard.py @@ -74,48 +74,74 @@ def _banner() -> None: ) -def run_wizard() -> ProjectConfig: +def run_wizard( + project_name: str | None = None, + domain: str | None = None, + framework: str | None = None, + anthropic_api_key: str | None = None, + anthropic_base_url: str | None = None, + openai_api_key: str | None = None, + connector: tuple[str, ...] = (), + demo_data: bool = False, + neo4j_uri: str | None = None, + neo4j_username: str | None = None, + neo4j_password: str | None = None, + neo4j_local: bool = False, + neo4j_aura_env: str | None = None, +) -> ProjectConfig: """Run the interactive wizard and return a ProjectConfig.""" _banner() # Step 1: Project name - project_name = questionary.text( - "What is your project name?", - default="my-context-graph", - ).ask() - if not project_name: - raise SystemExit("Aborted.") + if project_name: + # Use provided project name + pass + else: + project_name = questionary.text( + "What is your project name?", + default="my-context-graph", + ).ask() + if not project_name: + raise SystemExit("Aborted.") # Step 2: Data source - data_source = questionary.select( - "How would you like to populate your context graph?", - choices=[ - questionary.Choice("Generate demo data (synthetic documents & entities)", value="demo"), - questionary.Choice("Connect to SaaS services (Gmail, Slack, Jira, etc.)", value="saas"), - ], - ).ask() - if not data_source: - raise SystemExit("Aborted.") + if demo_data or connector: + # Use provided data source + data_source = "saas" if connector else ("demo" if demo_data else "none") + else: + data_source = questionary.select( + "How would you like to populate your context graph?", + choices=[ + questionary.Choice("Generate demo data (synthetic documents & entities)", value="demo"), + questionary.Choice("Connect to SaaS services (Gmail, Slack, Jira, etc.)", value="saas"), + ], + ).ask() + if not data_source: + raise SystemExit("Aborted.") # Step 2b: SaaS connector selection (if SaaS data source) selected_connectors: list[str] = [] saas_credentials: dict[str, dict[str, str]] = {} if data_source == "saas": - from create_context_graph.connectors import list_connectors, get_connector - from create_context_graph.connectors.oauth import check_gws_cli, install_gws_cli - - available = list_connectors() - connector_choices = [ - questionary.Choice(f"{c['name']} — {c['description']}", value=c["id"]) - for c in available - ] - - selected_connectors = questionary.checkbox( - "Select services to connect:", - choices=connector_choices, - ).ask() - if not selected_connectors: - raise SystemExit("Aborted. Select at least one connector.") + if connector: + # Use provided connectors + selected_connectors = list(connector) + else: + from create_context_graph.connectors import list_connectors, get_connector + from create_context_graph.connectors.oauth import check_gws_cli, install_gws_cli + + available = list_connectors() + connector_choices = [ + questionary.Choice(f"{c['name']} — {c['description']}", value=c["id"]) + for c in available + ] + + selected_connectors = questionary.checkbox( + "Select services to connect:", + choices=connector_choices, + ).ask() + if not selected_connectors: + raise SystemExit("Aborted. Select at least one connector.") # Check for Google Workspace CLI for Gmail/GCal google_connectors = {"gmail", "gcal"} @@ -152,22 +178,25 @@ def run_wizard() -> ProjectConfig: saas_credentials[conn_id] = creds # Step 3: Domain selection - domains = list_available_domains() - domain_choices = [ - questionary.Choice(d["name"], value=d["id"]) for d in domains - ] - domain_choices.append(questionary.Choice("Custom (describe your domain)", value="custom")) - - domain = questionary.select( - "Select your industry domain:", - choices=domain_choices, - ).ask() - if not domain: - raise SystemExit("Aborted.") + if domain: + # Use provided domain + pass + else: + domains = list_available_domains() + domain_choices = [ + questionary.Choice(d["name"], value=d["id"]) for d in domains + ] + domain_choices.append(questionary.Choice("Custom (describe your domain)", value="custom")) + + domain = questionary.select( + "Select your industry domain:", + choices=domain_choices, + ).ask() + if not domain: + raise SystemExit("Aborted.") custom_domain_yaml = None custom_ontology = None - anthropic_api_key = None if domain == "custom": # Collect domain description domain_description = questionary.text( @@ -177,12 +206,26 @@ def run_wizard() -> ProjectConfig: raise SystemExit("Aborted.") # Need an API key for LLM generation - custom_api_key = questionary.password( - "Anthropic API key (required for custom domain generation):", - ).ask() - if not custom_api_key: - console.print("[red]An API key is required for custom domain generation.[/red]") - raise SystemExit("API key required.") + if anthropic_api_key: + # Use provided API key + custom_api_key = anthropic_api_key + else: + custom_api_key = questionary.password( + "Anthropic API key (required for custom domain generation):", + ).ask() + if not custom_api_key: + console.print("[red]An API key is required for custom domain generation.[/red]") + raise SystemExit("API key required.") + + # Ask for custom base URL + if anthropic_base_url: + # Use provided base URL + custom_base_url = anthropic_base_url + else: + custom_base_url = questionary.text( + "Anthropic-compatible API base URL (Enter to use default):", + default="", + ).ask() # Generate the domain from create_context_graph.custom_domain import ( @@ -195,7 +238,7 @@ def run_wizard() -> ProjectConfig: with console.status("[bold cyan]Generating custom domain ontology..."): try: custom_ontology, custom_domain_yaml = generate_custom_domain( - domain_description, custom_api_key + domain_description, custom_api_key, base_url=custom_base_url ) except ValueError as e: console.print(f"[red]Generation failed: {e}[/red]") @@ -235,83 +278,127 @@ def run_wizard() -> ProjectConfig: if save: save_custom_domain(custom_ontology, custom_domain_yaml) - # Store the API key for later use + # Store the API key and base URL for later use anthropic_api_key = custom_api_key + anthropic_base_url = custom_base_url # Step 4: Agent framework - framework_choices = [ - questionary.Choice(FRAMEWORK_DISPLAY_NAMES[fw], value=fw) - for fw in SUPPORTED_FRAMEWORKS - ] - framework = questionary.select( - "Select your agent framework:", - choices=framework_choices, - ).ask() - if not framework: - raise SystemExit("Aborted.") + if framework: + # Use provided framework + pass + else: + framework_choices = [ + questionary.Choice(FRAMEWORK_DISPLAY_NAMES[fw], value=fw) + for fw in SUPPORTED_FRAMEWORKS + ] + framework = questionary.select( + "Select your agent framework:", + choices=framework_choices, + ).ask() + if not framework: + raise SystemExit("Aborted.") # Step 5: Neo4j connection - neo4j_type = questionary.select( - "How would you like to connect to Neo4j?", - choices=[ - questionary.Choice("Neo4j Aura (cloud — free tier available)", value="aura"), - questionary.Choice("Local Neo4j via neo4j-local (no Docker required)", value="local"), - questionary.Choice("Local Neo4j via Docker", value="docker"), - questionary.Choice("Existing Neo4j instance", value="existing"), - ], - ).ask() - if not neo4j_type: - raise SystemExit("Aborted.") - - if neo4j_type == "aura": - console.print(Panel( - "[bold]Neo4j Aura — Free Cloud Database[/bold]\n\n" - "1. Sign up at [cyan]https://console.neo4j.io[/cyan]\n" - "2. Create a free AuraDB instance\n" - "3. Download the [bold].env[/bold] file with your credentials\n" - "4. Provide the path to the downloaded file below", - border_style="cyan", - title="Setup", - )) - aura_env_path = questionary.path( - "Path to Neo4j Aura .env file:", + if neo4j_aura_env: + neo4j_type = "aura" + elif neo4j_local: + neo4j_type = "local" + elif neo4j_uri and "aura" in (neo4j_uri or ""): + neo4j_type = "aura" + elif neo4j_uri: + neo4j_type = "existing" + else: + neo4j_type = questionary.select( + "How would you like to connect to Neo4j?", + choices=[ + questionary.Choice("Neo4j Aura (cloud — free tier available)", value="aura"), + questionary.Choice("Local Neo4j via neo4j-local (no Docker required)", value="local"), + questionary.Choice("Local Neo4j via Docker", value="docker"), + questionary.Choice("Existing Neo4j instance", value="existing"), + ], ).ask() - if not aura_env_path: + if not neo4j_type: raise SystemExit("Aborted.") - neo4j_uri, neo4j_username, neo4j_password = _parse_aura_env(aura_env_path) + + if neo4j_type == "aura": + if neo4j_aura_env: + # Use provided Aura env file + neo4j_uri, neo4j_username, neo4j_password = _parse_aura_env(neo4j_aura_env) + else: + console.print(Panel( + "[bold]Neo4j Aura — Free Cloud Database[/bold]\n\n" + "1. Sign up at [cyan]https://console.neo4j.io[/cyan]\n" + "2. Create a free AuraDB instance\n" + "3. Download the [bold].env[/bold] file with your credentials\n" + "4. Provide the path to the downloaded file below", + border_style="cyan", + title="Setup", + )) + aura_env_path = questionary.path( + "Path to Neo4j Aura .env file:", + ).ask() + if not aura_env_path: + raise SystemExit("Aborted.") + neo4j_uri, neo4j_username, neo4j_password = _parse_aura_env(aura_env_path) elif neo4j_type == "local": - neo4j_uri = "neo4j://localhost:7687" - neo4j_username = "neo4j" - neo4j_password = "password" + neo4j_uri = neo4j_uri or "neo4j://localhost:7687" + neo4j_username = neo4j_username or "neo4j" + neo4j_password = neo4j_password or "password" console.print( "[dim]Will use [bold]@johnymontana/neo4j-local[/bold] — " "run [bold]make neo4j-start[/bold] to launch Neo4j (requires Node.js)[/dim]" ) elif neo4j_type == "docker": - neo4j_uri = "neo4j://localhost:7687" - neo4j_username = "neo4j" - neo4j_password = "password" - else: - neo4j_uri = questionary.text( - "Neo4j URI:", - default="neo4j+s://xxxx.databases.neo4j.io", - ).ask() - neo4j_username = questionary.text( - "Neo4j Username:", - default="neo4j", - ).ask() - neo4j_password = questionary.password("Neo4j Password:").ask() + neo4j_uri = neo4j_uri or "neo4j://localhost:7687" + neo4j_username = neo4j_username or "neo4j" + neo4j_password = neo4j_password or "password" + else: # existing + if neo4j_uri and neo4j_username and neo4j_password: + # Use provided values + pass + else: + neo4j_uri = questionary.text( + "Neo4j URI:", + default=neo4j_uri or "neo4j+s://xxxx.databases.neo4j.io", + ).ask() + neo4j_username = questionary.text( + "Neo4j Username:", + default=neo4j_username or "neo4j", + ).ask() + neo4j_password = questionary.password("Neo4j Password:").ask() if not neo4j_uri: raise SystemExit("Aborted.") # Step 6: API Keys (skip Anthropic if already collected for custom domain) if custom_domain_yaml is None: - anthropic_api_key = questionary.password( - "Anthropic API key (for AI agent):", + if anthropic_api_key: + # Use provided API key + pass + else: + anthropic_api_key = questionary.password( + "Anthropic API key (for AI agent):", + default="", + ).ask() + # anthropic_api_key already set from custom domain flow otherwise + + if openai_api_key: + # Use provided OpenAI API key + pass + else: + openai_api_key = questionary.password( + "OpenAI API key (for embeddings, or Enter to skip):", + default="", + ).ask() + + if anthropic_base_url: + # Use provided base URL + pass + else: + anthropic_base_url = questionary.text( + "Anthropic-compatible API base URL (Enter to use default):", default="", ).ask() - # anthropic_api_key already set from custom domain flow otherwise if framework == "openai-agents": openai_api_key = questionary.password( @@ -346,6 +433,7 @@ def run_wizard() -> ProjectConfig: neo4j_password=neo4j_password or "password", neo4j_type=neo4j_type, anthropic_api_key=anthropic_api_key or None, + anthropic_base_url=anthropic_base_url or None, openai_api_key=openai_api_key or None, google_api_key=google_api_key or None, generate_data=data_source == "demo", @@ -377,6 +465,7 @@ def _show_summary(config: ProjectConfig) -> None: table.add_row("Connectors", ", ".join(config.saas_connectors)) table.add_row("Neo4j", f"{config.neo4j_type} ({config.neo4j_uri})") table.add_row("Anthropic Key", "***" if config.anthropic_api_key else "(not set)") + table.add_row("Anthropic Base URL", config.anthropic_base_url or "(default)") table.add_row("OpenAI Key", "***" if config.openai_api_key else "(not set)") if config.google_api_key or config.resolved_framework == "google-adk": table.add_row("Google Key", "***" if config.google_api_key else "(not set)")