Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion backend/app/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-License-Identifier: Apache-2.0

from app.api.endpoints import auth, users, repository, oidc, quota, admin
from app.api.endpoints.adapter import models, agents, bots, teams, tasks, executors
from app.api.endpoints.adapter import models, agents, bots, teams, tasks, executors, dify
from app.api.endpoints.kind import k_router
from app.api.router import api_router

Expand All @@ -19,4 +19,5 @@
api_router.include_router(repository.router, prefix="/git", tags=["repository"])
api_router.include_router(executors.router, prefix="/executors", tags=["executors"])
api_router.include_router(quota.router, prefix="/quota", tags=["quota"])
api_router.include_router(dify.router, prefix="/dify", tags=["dify"])
api_router.include_router(k_router)
155 changes: 155 additions & 0 deletions backend/app/api/endpoints/adapter/dify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
#
# SPDX-License-Identifier: Apache-2.0

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import Dict, Any
from pydantic import BaseModel
import requests

from app.api.dependencies import get_db
from app.core import security
from app.models.user import User
from shared.logger import setup_logger
from shared.utils.crypto import decrypt_sensitive_data, is_data_encrypted

logger = setup_logger("dify_api")

router = APIRouter()


class DifyAppInfoRequest(BaseModel):
"""Request to get Dify app info"""
api_key: str
base_url: str = "https://api.dify.ai"


@router.post("/app/info")
def get_dify_app_info(
request: DifyAppInfoRequest,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user)
) -> Dict[str, Any]:
"""
Get Dify application information using API key

Uses Dify's /v1/info endpoint to retrieve basic app information.
This can be used to validate the API key and get app details.

Args:
request: Contains api_key and base_url

Returns:
App information including name, description, mode, etc.
"""

try:
# Decrypt API key if it's encrypted
api_key = request.api_key
if api_key and is_data_encrypted(api_key):
api_key = decrypt_sensitive_data(api_key) or api_key
logger.info("Decrypted API key for Dify app info request")

api_url = f"{request.base_url}/v1/info"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}

logger.info(f"Fetching Dify app info from: {api_url}")

response = requests.get(
api_url,
headers=headers,
timeout=10
)

response.raise_for_status()
data = response.json()

logger.info(f"Successfully fetched Dify app info: {data.get('name', 'Unknown')}")
return data

except requests.exceptions.HTTPError as e:
error_msg = f"Dify API HTTP error: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_msg = f"Dify API error: {error_data.get('message', str(e))}"
except:
pass
logger.error(error_msg)
raise HTTPException(status_code=502, detail=error_msg)

except requests.exceptions.RequestException as e:
error_msg = f"Failed to connect to Dify API: {str(e)}"
logger.error(error_msg)
raise HTTPException(status_code=502, detail=error_msg)

except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
logger.error(error_msg)
raise HTTPException(status_code=500, detail=error_msg)


@router.post("/app/parameters")
def get_dify_app_parameters(
request: DifyAppInfoRequest,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user)
) -> Dict[str, Any]:
"""
Get parameters schema for a Dify application

Uses Dify's /v1/parameters endpoint to retrieve app input parameters schema.

Args:
request: Contains api_key and base_url

Returns:
Parameters schema with user_input_form and system_parameters
"""

try:
# Decrypt API key if it's encrypted
api_key = request.api_key
if api_key and is_data_encrypted(api_key):
api_key = decrypt_sensitive_data(api_key) or api_key
logger.info("Decrypted API key for Dify app parameters request")

api_url = f"{request.base_url}/v1/parameters"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}

logger.info(f"Fetching Dify app parameters from: {api_url}")

response = requests.get(
api_url,
headers=headers,
timeout=10
)

response.raise_for_status()
data = response.json()

logger.info("Successfully fetched Dify app parameters")
return data

except requests.exceptions.HTTPError as e:
error_msg = f"Dify API HTTP error: {e}"
if e.response is not None:
try:
error_data = e.response.json()
error_msg = f"Dify API error: {error_data.get('message', str(e))}"
except:
pass
logger.error(error_msg)
raise HTTPException(status_code=502, detail=error_msg)

except Exception as e:
error_msg = f"Failed to fetch app parameters: {str(e)}"
logger.error(error_msg)
raise HTTPException(status_code=500, detail=error_msg)
13 changes: 13 additions & 0 deletions backend/app/api/endpoints/adapter/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ def share_team(
user_id=current_user.id,
)

@router.get("/{team_id}/input-parameters")
def get_team_input_parameters(
team_id: int,
current_user: User = Depends(security.get_current_user),
db: Session = Depends(get_db)
):
"""Get input parameters required by the team's external API bots"""
return team_kinds_service.get_team_input_parameters(
db=db,
team_id=team_id,
user_id=current_user.id
)

@router.get("/share/info")
def get_share_info(
share_token: str = Query(..., description="Share token"),
Expand Down
1 change: 1 addition & 0 deletions backend/app/schemas/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class TeamInDB(TeamBase):
updated_at: datetime
user: Optional[dict[str, Any]] = None
share_status: int = 0 # 0-private, 1-sharing, 2-shared from others
agent_type: Optional[str] = None # agno, claude, dify, etc.

class Config:
from_attributes = True
Expand Down
72 changes: 62 additions & 10 deletions backend/app/services/adapters/bot_kinds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime
from typing import List, Optional, Dict, Any
import json
import copy

from fastapi import HTTPException
from sqlalchemy.orm import Session
Expand All @@ -17,13 +18,45 @@
from app.schemas.bot import BotCreate, BotUpdate, BotInDB, BotDetail
from app.schemas.kind import Ghost, Bot, Shell, Model, Team
from app.services.base import BaseService
from app.services.adapters.shell_utils import get_shell_type
from shared.utils.crypto import encrypt_sensitive_data, is_data_encrypted


class BotKindsService(BaseService[Kind, BotCreate, BotUpdate]):
"""
Bot service class using kinds table
"""

# List of sensitive keys that should be encrypted in agent_config
SENSITIVE_CONFIG_KEYS = [
"DIFY_API_KEY",
# Add more sensitive keys here as needed
]

def _encrypt_agent_config(self, agent_config: Dict[str, Any]) -> Dict[str, Any]:
"""
Encrypt sensitive data in agent_config before storing

Args:
agent_config: Original agent config dictionary

Returns:
Agent config with encrypted sensitive fields
"""
# Create a deep copy to avoid modifying the original
encrypted_config = copy.deepcopy(agent_config)

# Encrypt sensitive keys in env section
if "env" in encrypted_config:
for key in self.SENSITIVE_CONFIG_KEYS:
if key in encrypted_config["env"]:
value = encrypted_config["env"][key]
# Only encrypt if not already encrypted
if value and not is_data_encrypted(str(value)):
encrypted_config["env"][key] = encrypt_sensitive_data(str(value))

return encrypted_config

def create_with_user(
self, db: Session, *, obj_in: BotCreate, user_id: int
) -> Dict[str, Any]:
Expand All @@ -48,6 +81,9 @@ def create_with_user(
if obj_in.skills:
self._validate_skills(db, obj_in.skills, user_id)

# Encrypt sensitive data in agent_config before storing
encrypted_agent_config = self._encrypt_agent_config(obj_in.agent_config)

# Create Ghost
ghost_spec = {
"systemPrompt": obj_in.system_prompt or "",
Expand Down Expand Up @@ -83,7 +119,7 @@ def create_with_user(
model_json = {
"kind": "Model",
"spec": {
"modelConfig": obj_in.agent_config
"modelConfig": encrypted_agent_config
},
"status": {
"state": "Available"
Expand All @@ -104,8 +140,8 @@ def create_with_user(
is_active=True
)
db.add(model)

support_model = []
shell_type = "local_engine" # Default shell type
if obj_in.agent_name:
public_shell = db.query(PublicShell).filter(
PublicShell.name == obj_in.agent_name,
Expand All @@ -115,19 +151,25 @@ def create_with_user(
if public_shell and isinstance(public_shell.json, dict):
shell_crd = Shell.model_validate(public_shell.json)
support_model = shell_crd.spec.supportModel or []
# Get shell type from metadata.labels
if shell_crd.metadata.labels and "type" in shell_crd.metadata.labels:
shell_type = shell_crd.metadata.labels["type"]

shell_json = {
"kind": "Shell",
"spec": {
"runtime": obj_in.agent_name,
"supportModel": support_model
},
"status": {
"state": "Available"
},
"metadata": {
"name": f"{obj_in.name}-shell",
"namespace": "default"
"namespace": "default",
"labels": {
"type": shell_type
}
},
"status": {
"state": "Available"
},
"apiVersion": "agent.wecode.io/v1"
}
Expand Down Expand Up @@ -305,8 +347,9 @@ def update_with_user(
flag_modified(bot, "json") # Mark JSON field as modified

if "agent_name" in update_data and shell:
# Query public_shells table to get supportModel based on new agent_name
# Query public_shells table to get supportModel and shell type based on new agent_name
support_model = []
shell_type = "local_engine" # Default shell type
new_agent_name = update_data["agent_name"]
if new_agent_name:
public_shell = db.query(PublicShell).filter(
Expand All @@ -315,18 +358,27 @@ def update_with_user(
).first()

if public_shell and isinstance(public_shell.json, dict):
shell_crd = Shell.model_validate(public_shell.json)
support_model = shell_crd.spec.supportModel or []
public_shell_crd = Shell.model_validate(public_shell.json)
support_model = public_shell_crd.spec.supportModel or []
# Get shell type from metadata.labels
if public_shell_crd.metadata.labels and "type" in public_shell_crd.metadata.labels:
shell_type = public_shell_crd.metadata.labels["type"]

shell_crd = Shell.model_validate(shell.json)
shell_crd.spec.runtime = new_agent_name
shell_crd.spec.supportModel = support_model
# Update shell type in metadata.labels
if not shell_crd.metadata.labels:
shell_crd.metadata.labels = {}
shell_crd.metadata.labels["type"] = shell_type
shell.json = shell_crd.model_dump()
flag_modified(shell, "json") # Mark JSON field as modified

if "agent_config" in update_data and model:
model_crd = Model.model_validate(model.json)
model_crd.spec.modelConfig = update_data["agent_config"]
# Encrypt sensitive data before updating
encrypted_agent_config = self._encrypt_agent_config(update_data["agent_config"])
model_crd.spec.modelConfig = encrypted_agent_config
model.json = model_crd.model_dump()
flag_modified(model, "json") # Mark JSON field as modified

Expand Down
Loading