Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
7 changes: 7 additions & 0 deletions backend/app/api/endpoints/adapter/bots.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ def update_bot(
db: Session = Depends(get_db)
):
"""Update Bot information"""
import logging
logger = logging.getLogger(__name__)
logger.info(f"[DEBUG] update_bot called with bot_id={bot_id}")
logger.info(f"[DEBUG] bot_update raw: {bot_update}")
logger.info(f"[DEBUG] bot_update.agent_config: {bot_update.agent_config}")
logger.info(f"[DEBUG] bot_update.model_dump(exclude_unset=True): {bot_update.model_dump(exclude_unset=True)}")
Comment on lines +67 to +72
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove development debug logging before merging.

This appears to be temporary debug code:

  1. Import should be at module level, not inside the function.
  2. Using logger.info with "[DEBUG]" prefix is semantically incorrect—use logger.debug for debug-level logs.
  3. Logging full bot_update payloads (including agent_config) may expose sensitive configuration data in production logs.

If logging is needed for observability, consider a minimal, structured approach:

+import logging
+
+logger = logging.getLogger(__name__)
+
 router = APIRouter()

 # ... in update_bot function:
-    import logging
-    logger = logging.getLogger(__name__)
-    logger.info(f"[DEBUG] update_bot called with bot_id={bot_id}")
-    logger.info(f"[DEBUG] bot_update raw: {bot_update}")
-    logger.info(f"[DEBUG] bot_update.agent_config: {bot_update.agent_config}")
-    logger.info(f"[DEBUG] bot_update.model_dump(exclude_unset=True): {bot_update.model_dump(exclude_unset=True)}")
-    
+    logger.debug("update_bot called with bot_id=%s", bot_id)
🤖 Prompt for AI Agents
In backend/app/api/endpoints/adapter/bots.py around lines 67 to 72, remove the
ad-hoc development debug logging: move the logging import to the module top if
not already, remove the four info statements that print the full bot_update and
agent_config, and if observability is required replace them with a single
logger.debug call that logs only non-sensitive minimal fields (e.g., bot_id and
a trimmed status or masked/hashed indicator for agent_config) or structured
metadata; ensure you use logger.debug (not logger.info with “[DEBUG]”) and do
not emit full payloads or sensitive configuration data.


bot_dict = bot_kinds_service.update_with_user(
db=db,
bot_id=bot_id,
Expand Down
235 changes: 231 additions & 4 deletions backend/app/api/endpoints/adapter/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.orm import Session
from typing import List
from typing import List, Optional
import logging

from app.api.dependencies import get_db
from app.core import security
from app.models.user import User
from app.models.kind import Kind
from app.schemas.model import (
ModelCreate,
ModelUpdate,
Expand All @@ -18,8 +20,10 @@
ModelBulkCreateItem,
)
from app.services.adapters import public_model_service
from app.services.model_aggregation_service import model_aggregation_service, ModelType

router = APIRouter()
logger = logging.getLogger(__name__)


@router.get("", response_model=ModelListResponse)
Expand All @@ -46,19 +50,107 @@ def list_model_names(
current_user: User = Depends(security.get_current_user),
):
"""
Get all active model names
Get all active model names (legacy API, use /unified for new implementations)

Response:
{
"data": [
{"name": "string"}
{"name": "string", "displayName": "string"}
]
}
"""
data = public_model_service.list_model_names(db=db, current_user=current_user, agent_name=agent_name)
return {"data": data}


@router.get("/unified")
def list_unified_models(
agent_name: Optional[str] = Query(None, description="Agent name to filter compatible models (Agno, ClaudeCode)"),
include_config: bool = Query(False, description="Whether to include full config in response"),
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""
Get unified list of all available models (both public and user-defined).

This endpoint aggregates models from:
- Public models (type='public'): Shared across all users
- User-defined models (type='user'): Private to the current user

Each model includes a 'type' field to identify its source, which is
important for avoiding naming conflicts when binding models.

Parameters:
- agent_name: Optional agent name to filter compatible models
- include_config: Whether to include full model config in response

Response:
{
"data": [
{
"name": "model-name",
"type": "public" | "user",
"displayName": "Human Readable Name",
"provider": "openai" | "claude",
"modelId": "gpt-4"
}
]
}
"""
data = model_aggregation_service.list_available_models(
db=db,
current_user=current_user,
agent_name=agent_name,
include_config=include_config
)
return {"data": data}


@router.get("/unified/{model_name}")
def get_unified_model(
model_name: str,
model_type: Optional[str] = Query(None, description="Model type ('public' or 'user')"),
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
):
"""
Get a specific model by name, optionally with type hint.

If model_type is not provided, it will try to find the model
in the following order:
1. User's own models (type='user')
2. Public models (type='public')

Parameters:
- model_name: Model name
- model_type: Optional model type hint ('public' or 'user')

Response:
{
"name": "model-name",
"type": "public" | "user",
"displayName": "Human Readable Name",
"provider": "openai" | "claude",
"modelId": "gpt-4",
"config": {...},
"isActive": true
}
"""
from fastapi import HTTPException

result = model_aggregation_service.resolve_model(
db=db,
current_user=current_user,
name=model_name,
model_type=model_type
)

if not result:
raise HTTPException(status_code=404, detail="Model not found")

return result


@router.post("", response_model=ModelInDB, status_code=status.HTTP_201_CREATED)
def create_model(
model_create: ModelCreate,
Expand Down Expand Up @@ -165,4 +257,139 @@ def delete_model(
Soft delete Model (set is_active to False)
"""
public_model_service.delete_model(db=db, model_id=model_id, current_user=current_user)
return {"message": "Model deleted successfully"}
return {"message": "Model deleted successfully"}


@router.post("/test-connection")
def test_model_connection(
test_data: dict,
current_user: User = Depends(security.get_current_user),
):
"""
Test model connection

Request body:
{
"provider_type": "openai" | "anthropic",
"model_id": "gpt-4",
"api_key": "sk-...",
"base_url": "https://api.openai.com/v1" // optional
}

Response:
{
"success": true | false,
"message": "Connection successful" | "Error message"
}
"""
provider_type = test_data.get("provider_type")
model_id = test_data.get("model_id")
api_key = test_data.get("api_key")
base_url = test_data.get("base_url")

if not provider_type or not model_id or not api_key:
return {
"success": False,
"message": "Missing required fields: provider_type, model_id, api_key"
}

try:
if provider_type == "openai":
import openai
client = openai.OpenAI(
api_key=api_key,
base_url=base_url or "https://api.openai.com/v1"
)
# Send minimal test request
response = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": "hi"}],
max_tokens=1
)
return {
"success": True,
"message": f"Successfully connected to {model_id}"
}

elif provider_type == "anthropic":
import anthropic
client = anthropic.Anthropic(api_key=api_key)
if base_url:
client.base_url = base_url

response = client.messages.create(
model=model_id,
max_tokens=1,
messages=[{"role": "user", "content": "hi"}]
)
return {
"success": True,
"message": f"Successfully connected to {model_id}"
}

else:
return {
"success": False,
"message": "Unsupported provider type"
}

except Exception as e:
logger.error(f"Model connection test failed: {str(e)}")
return {
"success": False,
"message": f"Connection failed: {str(e)}"
}


@router.get("/compatible")
def get_compatible_models(
agent_name: str = Query(..., description="Agent name (Agno or ClaudeCode)"),
current_user: User = Depends(security.get_current_user),
db: Session = Depends(get_db)
):
"""
Get models compatible with a specific agent type

Parameters:
- agent_name: "Agno" or "ClaudeCode"

Response:
{
"models": [
{"name": "my-gpt4-model"},
{"name": "my-gpt4o-model"}
]
}
"""
from app.schemas.kind import Model as ModelCRD

# Query all active Model CRDs from kinds table
models = db.query(Kind).filter(
Kind.user_id == current_user.id,
Kind.kind == "Model",
Kind.namespace == "default",
Kind.is_active == True
).all()

compatible_models = []

for model_kind in models:
try:
if not model_kind.json:
continue
model_crd = ModelCRD.model_validate(model_kind.json)
model_config = model_crd.spec.modelConfig
if isinstance(model_config, dict):
env = model_config.get("env", {})
model_type = env.get("model", "")

# Filter compatible models
if agent_name == "Agno" and model_type == "openai":
compatible_models.append({"name": model_kind.name})
elif agent_name == "ClaudeCode" and model_type == "claude":
compatible_models.append({"name": model_kind.name})
except Exception as e:
logger.warning(f"Failed to parse model {model_kind.name}: {e}")
continue

return {"models": compatible_models}
4 changes: 2 additions & 2 deletions backend/app/api/endpoints/kind/kinds.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async def update_resource(
return schema_class.parse_obj(formatted_resource)


@router.delete("/namespaces/{namespace}/{kinds}/{name}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/namespaces/{namespace}/{kinds}/{name}")
async def delete_resource(
namespace: str = Path(..., description="Resource namespace"),
kinds: str = Path(..., description="Resource type. Valid options: ghosts, models, shells, bots, teams, workspaces, tasks"),
Expand All @@ -148,7 +148,7 @@ async def delete_resource(
Delete a resource

Deletes a resource of the specified kind with the given name in the namespace.
Returns no content on success.
Returns a success message on completion.
"""
# Validate resource type
kind = validate_resource_type(kinds)
Expand Down
5 changes: 4 additions & 1 deletion backend/app/schemas/kind.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ObjectMeta(BaseModel):
"""Standard Kubernetes object metadata"""
name: str
namespace: str = "default"
displayName: Optional[str] = None # Human-readable display name
labels: Optional[Dict[str, str]] = None
# annotations: Optional[Dict[str, str]] = None

Expand Down Expand Up @@ -58,6 +59,8 @@ class GhostList(BaseModel):
class ModelSpec(BaseModel):
"""Model specification"""
modelConfig: Dict[str, Any]
isCustomConfig: Optional[bool] = None # True if user customized the config, False/None if using predefined model
protocol: Optional[str] = None # Model protocol type: 'openai', 'claude', etc. Required for custom configs


class ModelStatus(Status):
Expand Down Expand Up @@ -132,7 +135,7 @@ class BotSpec(BaseModel):
"""Bot specification"""
ghostRef: GhostRef
shellRef: ShellRef
modelRef: ModelRef
modelRef: Optional[ModelRef] = None


class BotStatus(Status):
Expand Down
3 changes: 3 additions & 0 deletions backend/app/schemas/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class TaskCreate(BaseModel):
task_type: Optional[str] = "chat" # chat、code
auto_delete_executor: Optional[str] = "false" # true、fasle
source: Optional[str] = "web"
# Model selection fields
model_id: Optional[str] = None # Model name (not database ID)
force_override_bot_model: Optional[bool] = False


class TaskUpdate(BaseModel):
Expand Down
6 changes: 6 additions & 0 deletions backend/app/schemas/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@
from app.schemas.user import UserInDB
from app.schemas.bot import BotInDB

class BotSummary(BaseModel):
"""Bot summary model with only necessary fields for team list"""
agent_config: Optional[dict[str, Any]] = None
agent_name: Optional[str] = None

class BotInfo(BaseModel):
"""Bot information model"""
bot_id: int
bot_prompt: Optional[str] = None
role: Optional[str] = None
bot: Optional[BotSummary] = None

class BotDetailInfo(BaseModel):
"""Bot detail information model with bot object"""
Expand Down
Loading
Loading