Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions echo/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from copilotkit.langgraph import CopilotKitState
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langchain_google_genai import ChatGoogleGenerativeAI
from google.oauth2 import service_account
from langchain_google_vertexai import ChatVertexAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, StateGraph
from langgraph.prebuilt import ToolNode
Expand Down Expand Up @@ -94,14 +95,29 @@
)


def _build_llm() -> ChatGoogleGenerativeAI:
VERTEX_AUTH_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]


def _build_llm() -> ChatVertexAI:
settings = get_settings()
if not settings.gemini_api_key:
raise ValueError("GEMINI_API_KEY is required")

return ChatGoogleGenerativeAI(
model=settings.llm_model,
google_api_key=settings.gemini_api_key,
credentials = None
project = settings.vertex_project or None
credentials_payload = settings.vertex_credentials or settings.gcp_sa_json
if credentials_payload:
credentials = service_account.Credentials.from_service_account_info(
credentials_payload,
scopes=VERTEX_AUTH_SCOPES,
)
if not project:
project = credentials_payload.get("project_id")

return ChatVertexAI(
model_name=settings.llm_model,
project=project,
location=settings.vertex_location,
api_endpoint=settings.vertex_api_endpoint or None,
credentials=credentials,
)


Expand Down
3 changes: 2 additions & 1 deletion echo/agent/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ requires-python = ">=3.11"
dependencies = [
"copilotkit>=0.1.77",
"langgraph>=0.2",
"langchain-google-genai>=2.0",
"langchain-google-vertexai>=3.2.2",
"google-auth>=2.35",
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
"httpx>=0.27",
Expand Down
35 changes: 32 additions & 3 deletions echo/agent/settings.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import json
from functools import lru_cache
from typing import Any, Optional

from pydantic import Field
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
echo_api_url: str = Field(default="http://localhost:8000/api", alias="ECHO_API_URL")
gemini_api_key: str = Field(default="", alias="GEMINI_API_KEY")
llm_model: str = Field(default="gemini-3-pro-preview", alias="LLM_MODEL")
# Vertex model id (no provider prefix). Gemini 3.x is served on the
# global Vertex host only; see vertex_api_endpoint below.
llm_model: str = Field(default="gemini-3.5-flash", alias="LLM_MODEL")
vertex_location: str = Field(default="eu", alias="VERTEX_LOCATION")
# Pinning the global host while keeping locations/<region> in the request
# path mirrors the server's LiteLLM config (validated in production):
# the regional eu-aiplatform host 404s for gemini-3.x models.
vertex_api_endpoint: str = Field(
default="aiplatform.googleapis.com", alias="VERTEX_API_ENDPOINT"
)
vertex_project: str = Field(default="", alias="VERTEX_PROJECT")
# Service-account JSON blob. VERTEX_CREDENTIALS wins over GCP_SA_JSON;
# with neither set, Application Default Credentials apply.
vertex_credentials: Optional[dict[str, Any]] = Field(
default=None, alias="VERTEX_CREDENTIALS"
)
gcp_sa_json: Optional[dict[str, Any]] = Field(default=None, alias="GCP_SA_JSON")
agent_graph_recursion_limit: int = Field(
default=80,
alias="AGENT_GRAPH_RECURSION_LIMIT",
Expand All @@ -17,6 +34,18 @@ class Settings(BaseSettings):
alias="AGENT_CORS_ORIGINS",
)

@field_validator("vertex_credentials", "gcp_sa_json", mode="before")
@classmethod
def _parse_service_account_json(cls, value: Any) -> Optional[dict[str, Any]]:
if value is None or isinstance(value, dict):
return value
if isinstance(value, str):
stripped = value.strip()
if not stripped:
return None
return json.loads(stripped)
raise ValueError("Expected a JSON object or JSON string")

class Config:
env_file = ".env"
extra = "ignore"
Expand Down
7 changes: 5 additions & 2 deletions echo/agent/tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
def test_settings_reads_env(monkeypatch):
get_settings.cache_clear()
monkeypatch.setenv("ECHO_API_URL", "http://example.test/api")
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
monkeypatch.setenv("GCP_SA_JSON", '{"type": "service_account", "project_id": "proj-1"}')
monkeypatch.setenv("VERTEX_LOCATION", "europe-west4")
monkeypatch.setenv("LLM_MODEL", "gemini-test")
monkeypatch.setenv("AGENT_GRAPH_RECURSION_LIMIT", "64")
monkeypatch.setenv("AGENT_CORS_ORIGINS", "http://localhost:1111,http://localhost:2222")

settings = get_settings()

assert settings.echo_api_url == "http://example.test/api"
assert settings.gemini_api_key == "test-key"
assert settings.gcp_sa_json == {"type": "service_account", "project_id": "proj-1"}
assert settings.vertex_location == "europe-west4"
assert settings.vertex_api_endpoint == "aiplatform.googleapis.com"
assert settings.llm_model == "gemini-test"
assert settings.agent_graph_recursion_limit == 64
assert settings.agent_cors_origins == "http://localhost:1111,http://localhost:2222"
Expand Down
Loading
Loading