openCanvas
This commit is contained in:
1 parent
050b1ad78e
commit
1be600155b
18 files changed
+754
-101
No files matched your search
@@ -3,9 +3,50 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.gateway.deps import get_config
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.model_config import ModelConfig
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["models"])
|
||||
|
||||
_PROVIDER_KEYWORDS: list[tuple[str, str]] = [
|
||||
("langchain_openai", "openai"),
|
||||
("langchain_anthropic", "anthropic"),
|
||||
("langchain_google", "google"),
|
||||
("langchain_groq", "groq"),
|
||||
("langchain_mistralai", "mistral"),
|
||||
("langchain_cohere", "cohere"),
|
||||
("ollama", "ollama"),
|
||||
("vllm", "vllm"),
|
||||
]
|
||||
|
||||
_LOCAL_PROVIDERS = {"ollama", "vllm"}
|
||||
|
||||
|
||||
def _derive_provider(use: str) -> str | None:
|
||||
use_lower = use.lower()
|
||||
for keyword, provider in _PROVIDER_KEYWORDS:
|
||||
if keyword in use_lower:
|
||||
return provider
|
||||
return None
|
||||
|
||||
|
||||
def _build_model_response(model: ModelConfig) -> "ModelResponse":
|
||||
extra = model.model_extra or {}
|
||||
provider = str(extra["provider"]) if "provider" in extra else _derive_provider(model.use)
|
||||
is_local = bool(extra["is_local"]) if "is_local" in extra else (provider in _LOCAL_PROVIDERS)
|
||||
supports_tool_calling = bool(extra["supports_tool_calling"]) if "supports_tool_calling" in extra else True
|
||||
return ModelResponse(
|
||||
name=model.name,
|
||||
model=model.model,
|
||||
display_name=model.display_name,
|
||||
description=model.description,
|
||||
supports_thinking=model.supports_thinking,
|
||||
supports_reasoning_effort=model.supports_reasoning_effort,
|
||||
supports_vision=model.supports_vision,
|
||||
supports_tool_calling=supports_tool_calling,
|
||||
is_local=is_local,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
|
||||
class ModelResponse(BaseModel):
|
||||
"""Response model for model information."""
|
||||
@@ -16,6 +57,10 @@ class ModelResponse(BaseModel):
|
||||
description: str | None = Field(None, description="Model description")
|
||||
supports_thinking: bool = Field(default=False, description="Whether model supports thinking mode")
|
||||
supports_reasoning_effort: bool = Field(default=False, description="Whether model supports reasoning effort")
|
||||
supports_vision: bool = Field(default=False, description="Whether model supports image inputs")
|
||||
supports_tool_calling: bool = Field(default=True, description="Whether model supports tool/function calling")
|
||||
is_local: bool = Field(default=False, description="Whether this is a locally-hosted model")
|
||||
provider: str | None = Field(None, description="Model provider name (openai, anthropic, ollama, etc.)")
|
||||
|
||||
|
||||
class TokenUsageResponse(BaseModel):
|
||||
@@ -73,17 +118,7 @@ async def list_models(config: AppConfig = Depends(get_config)) -> ModelsListResp
|
||||
}
|
||||
```
|
||||
"""
|
||||
models = [
|
||||
ModelResponse(
|
||||
name=model.name,
|
||||
model=model.model,
|
||||
display_name=model.display_name,
|
||||
description=model.description,
|
||||
supports_thinking=model.supports_thinking,
|
||||
supports_reasoning_effort=model.supports_reasoning_effort,
|
||||
)
|
||||
for model in config.models
|
||||
]
|
||||
models = [_build_model_response(model) for model in config.models]
|
||||
return ModelsListResponse(
|
||||
models=models,
|
||||
token_usage=TokenUsageResponse(enabled=config.token_usage.enabled),
|
||||
@@ -122,11 +157,4 @@ async def get_model(model_name: str, config: AppConfig = Depends(get_config)) ->
|
||||
if model is None:
|
||||
raise HTTPException(status_code=404, detail=f"Model '{model_name}' not found")
|
||||
|
||||
return ModelResponse(
|
||||
name=model.name,
|
||||
model=model.model,
|
||||
display_name=model.display_name,
|
||||
description=model.description,
|
||||
supports_thinking=model.supports_thinking,
|
||||
supports_reasoning_effort=model.supports_reasoning_effort,
|
||||
)
|
||||
return _build_model_response(model)
|
||||
@@ -97,6 +97,10 @@ def normalize_input(raw_input: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
_DEFAULT_ASSISTANT_ID = "lead_agent"
|
||||
|
||||
# Built-in LangGraph graph IDs registered in langgraph.json.
|
||||
# These are NOT stored in the agents DB and must bypass the custom-agent access check.
|
||||
_BUILTIN_GRAPH_IDS: frozenset[str] = frozenset({"lead_agent", "canvas_agent"})
|
||||
|
||||
|
||||
# Whitelist of run-context keys that the langgraph-compat layer forwards from
|
||||
# ``body.context`` into the run config. ``config["context"]`` exists in
|
||||
@@ -140,12 +144,15 @@ def merge_run_context_overrides(config: dict[str, Any], context: Mapping[str, An
|
||||
def resolve_agent_factory(assistant_id: str | None):
|
||||
"""Resolve the agent factory callable from config.
|
||||
|
||||
Custom agents are implemented as ``lead_agent`` + an ``agent_id``
|
||||
injected into ``configurable`` or ``context`` — see
|
||||
:func:`build_run_config`. All ``assistant_id`` values therefore map to the
|
||||
same factory; the routing happens inside ``make_lead_agent`` when it reads
|
||||
``cfg["agent_id"]``.
|
||||
Built-in graphs (``lead_agent``, ``canvas_agent``) are dispatched directly
|
||||
to their own factory. Custom agents share the ``lead_agent`` factory with
|
||||
``agent_id`` injected into configurable/context by :func:`build_run_config`.
|
||||
"""
|
||||
if assistant_id == "canvas_agent":
|
||||
from deerflow.agents.canvas_agent.agent import make_canvas_agent
|
||||
|
||||
return make_canvas_agent
|
||||
|
||||
from deerflow.agents.lead_agent.agent import make_lead_agent
|
||||
|
||||
return make_lead_agent
|
||||
@@ -204,8 +211,9 @@ def build_run_config(
|
||||
config["configurable"] = {"thread_id": thread_id}
|
||||
|
||||
# Inject custom agent id when the caller specified a non-default assistant.
|
||||
# Honour an explicit agent_id in the active runtime options container.
|
||||
if assistant_id and assistant_id != _DEFAULT_ASSISTANT_ID:
|
||||
# Built-in graphs (lead_agent, canvas_agent) are dispatched via their own
|
||||
# factory and must NOT receive an agent_id override.
|
||||
if assistant_id and assistant_id not in _BUILTIN_GRAPH_IDS:
|
||||
normalized = assistant_id.strip()
|
||||
if not normalized or not re.fullmatch(r"[A-Za-z0-9_-]+", normalized):
|
||||
raise ValueError(f"Invalid assistant_id {assistant_id!r}: must contain only letters, digits, hyphens, and underscores.")
|
||||
@@ -225,7 +233,7 @@ def build_run_config(
|
||||
def _collect_requested_agent_ids(body: Any) -> set[str]:
|
||||
"""Collect agent ids from assistant_id/config/context for access checks."""
|
||||
agent_ids: set[str] = set()
|
||||
if body.assistant_id and body.assistant_id != _DEFAULT_ASSISTANT_ID:
|
||||
if body.assistant_id and body.assistant_id not in _BUILTIN_GRAPH_IDS:
|
||||
agent_ids.add(str(body.assistant_id))
|
||||
|
||||
def collect(container: Mapping[str, Any] | None) -> None:
|
||||
|
||||
Reference in new issue
Block a user