Skip to content

Commit

Permalink
Merge branch 'dev' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
ntindle authored Nov 18, 2024
2 parents 65ac6f0 + 8fccf2e commit 257cbfc
Show file tree
Hide file tree
Showing 31 changed files with 345 additions and 91 deletions.
9 changes: 7 additions & 2 deletions autogpt_platform/autogpt_libs/autogpt_libs/auth/depends.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fastapi

from .middleware import auth_middleware
from .models import User
from .models import User, DEFAULT_USER_ID, DEFAULT_EMAIL
from .config import Settings


def requires_user(payload: dict = fastapi.Depends(auth_middleware)) -> User:
Expand All @@ -16,8 +17,12 @@ def requires_admin_user(

def verify_user(payload: dict | None, admin_only: bool) -> User:
if not payload:
if Settings.ENABLE_AUTH:
raise fastapi.HTTPException(
status_code=401, detail="Authorization header is missing"
)
# This handles the case when authentication is disabled
payload = {"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
payload = {"sub": DEFAULT_USER_ID, "role": "admin"}

user_id = payload.get("sub")

Expand Down
3 changes: 3 additions & 0 deletions autogpt_platform/autogpt_libs/autogpt_libs/auth/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from dataclasses import dataclass

DEFAULT_USER_ID = "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
DEFAULT_EMAIL = "[email protected]"


# Using dataclass here to avoid adding dependency on pydantic
@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,20 @@
title="Use Credits for Jina",
expires_at=None,
)
unreal_credentials = APIKeyCredentials(#
unreal_credentials = APIKeyCredentials(
id="66f20754-1b81-48e4-91d0-f4f0dd82145f",
provider="unreal",
api_key=SecretStr(settings.secrets.unreal_speech_api_key),
title="Use Credits for Unreal",
expires_at=None,
)
open_router_credentials = APIKeyCredentials(
id="b5a0e27d-0c98-4df3-a4b9-10193e1f3c40",
provider="open_router",
api_key=SecretStr(settings.secrets.open_router_api_key),
title="Use Credits for Open Router",
expires_at=None,
)


DEFAULT_CREDENTIALS = [
Expand All @@ -98,6 +105,7 @@
did_credentials,
jina_credentials,
unreal_credentials,
open_router_credentials,
]


Expand Down Expand Up @@ -145,6 +153,8 @@ def get_all_creds(self, user_id: str) -> list[Credentials]:
all_credentials.append(jina_credentials)
if settings.secrets.unreal_speech_api_key:
all_credentials.append(unreal_credentials)
if settings.secrets.open_router_api_key:
all_credentials.append(open_router_credentials)
return all_credentials

def get_creds_by_id(self, user_id: str, credentials_id: str) -> Credentials | None:
Expand Down
1 change: 1 addition & 0 deletions autogpt_platform/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ GOOGLE_CLIENT_SECRET=
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=
OPEN_ROUTER_API_KEY=

# Reddit
REDDIT_CLIENT_ID=
Expand Down
55 changes: 53 additions & 2 deletions autogpt_platform/backend/backend/blocks/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
# "ollama": BlockSecret(value=""),
# }

LLMProviderName = Literal["anthropic", "groq", "openai", "ollama"]
LLMProviderName = Literal["anthropic", "groq", "openai", "ollama", "open_router"]
AICredentials = CredentialsMetaInput[LLMProviderName, Literal["api_key"]]

TEST_CREDENTIALS = APIKeyCredentials(
Expand All @@ -51,7 +51,7 @@
def AICredentialsField() -> AICredentials:
return CredentialsField(
description="API key for the LLM provider.",
provider=["anthropic", "groq", "openai", "ollama"],
provider=["anthropic", "groq", "openai", "ollama", "open_router"],
supported_credential_types={"api_key"},
discriminator="model",
discriminator_mapping={
Expand Down Expand Up @@ -108,6 +108,18 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
# Ollama models
OLLAMA_LLAMA3_8B = "llama3"
OLLAMA_LLAMA3_405B = "llama3.1:405b"
# OpenRouter models
GEMINI_FLASH_1_5_8B = "google/gemini-flash-1.5"
GEMINI_FLASH_1_5_EXP = "google/gemini-flash-1.5-exp"
GROK_BETA = "x-ai/grok-beta"
MISTRAL_NEMO = "mistralai/mistral-nemo"
COHERE_COMMAND_R_08_2024 = "cohere/command-r-08-2024"
COHERE_COMMAND_R_PLUS_08_2024 = "cohere/command-r-plus-08-2024"
EVA_QWEN_2_5_32B = "eva-unit-01/eva-qwen-2.5-32b"
DEEPSEEK_CHAT = "deepseek/deepseek-chat"
PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE = (
"perplexity/llama-3.1-sonar-large-128k-online"
)

@property
def metadata(self) -> ModelMetadata:
Expand Down Expand Up @@ -142,6 +154,17 @@ def context_window(self) -> int:
LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 131072),
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192),
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192),
LlmModel.GEMINI_FLASH_1_5_8B: ModelMetadata("open_router", 8192),
LlmModel.GEMINI_FLASH_1_5_EXP: ModelMetadata("open_router", 8192),
LlmModel.GROK_BETA: ModelMetadata("open_router", 8192),
LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 4000),
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 4000),
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata("open_router", 4000),
LlmModel.EVA_QWEN_2_5_32B: ModelMetadata("open_router", 4000),
LlmModel.DEEPSEEK_CHAT: ModelMetadata("open_router", 8192),
LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: ModelMetadata(
"open_router", 8192
),
}

for model in LlmModel:
Expand Down Expand Up @@ -354,6 +377,34 @@ def llm_call(
response.get("prompt_eval_count") or 0,
response.get("eval_count") or 0,
)
elif provider == "open_router":
client = openai.OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=credentials.api_key.get_secret_value(),
)

response = client.chat.completions.create(
extra_headers={
"HTTP-Referer": "https://agpt.co",
"X-Title": "AutoGPT",
},
model=llm_model.value,
messages=prompt, # type: ignore
max_tokens=max_tokens,
)

# If there's no response, raise an error
if not response.choices:
if response:
raise ValueError(f"OpenRouter error: {response}")
else:
raise ValueError("No response from OpenRouter.")

return (
response.choices[0].message.content or "",
response.usage.prompt_tokens if response.usage else 0,
response.usage.completion_tokens if response.usage else 0,
)
else:
raise ValueError(f"Unsupported LLM provider: {provider}")

Expand Down
27 changes: 27 additions & 0 deletions autogpt_platform/backend/backend/data/block_cost_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
groq_credentials,
ideogram_credentials,
jina_credentials,
open_router_credentials,
openai_credentials,
replicate_credentials,
revid_credentials,
Expand Down Expand Up @@ -54,6 +55,15 @@
LlmModel.LLAMA3_1_8B: 1,
LlmModel.OLLAMA_LLAMA3_8B: 1,
LlmModel.OLLAMA_LLAMA3_405B: 1,
LlmModel.GEMINI_FLASH_1_5_8B: 1,
LlmModel.GEMINI_FLASH_1_5_EXP: 1,
LlmModel.GROK_BETA: 5,
LlmModel.MISTRAL_NEMO: 1,
LlmModel.COHERE_COMMAND_R_08_2024: 1,
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: 3,
LlmModel.EVA_QWEN_2_5_32B: 1,
LlmModel.DEEPSEEK_CHAT: 2,
LlmModel.PERPLEXITY_LLAMA_3_1_SONAR_LARGE_128K_ONLINE: 1,
}

for model in LlmModel:
Expand Down Expand Up @@ -124,6 +134,23 @@
cost_filter={"api_key": None},
),
]
# Open Router Models
+ [
BlockCost(
cost_type=BlockCostType.RUN,
cost_filter={
"model": model,
"credentials": {
"id": open_router_credentials.id,
"provider": open_router_credentials.provider,
"type": open_router_credentials.type,
},
},
cost_amount=cost,
)
for model, cost in MODEL_COST.items()
if MODEL_METADATA[model].provider == "open_router"
]
)

# =============== This is the exhaustive list of cost for each Block =============== #
Expand Down
82 changes: 82 additions & 0 deletions autogpt_platform/backend/backend/data/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime, timezone
from typing import Any, Literal, Type

import prisma
from prisma.models import AgentGraph, AgentGraphExecution, AgentNode, AgentNodeLink
from prisma.types import AgentGraphWhereInput
from pydantic.fields import computed_field
Expand Down Expand Up @@ -528,3 +529,84 @@ async def __create_graph(tx, graph: Graph, user_id: str):
for link in graph.links
]
)


# ------------------------ UTILITIES ------------------------ #


async def fix_llm_provider_credentials():
"""Fix node credentials with provider `llm`"""
from autogpt_libs.supabase_integration_credentials_store import (
SupabaseIntegrationCredentialsStore,
)

from .redis import get_redis
from .user import get_user_integrations

store = SupabaseIntegrationCredentialsStore(get_redis())

broken_nodes = await prisma.get_client().query_raw(
"""
SELECT "User".id user_id,
node.id node_id,
node."constantInput" node_preset_input
FROM platform."AgentNode" node
LEFT JOIN platform."AgentGraph" graph
ON node."agentGraphId" = graph.id
LEFT JOIN platform."User" "User"
ON graph."userId" = "User".id
WHERE node."constantInput"::jsonb->'credentials'->>'provider' = 'llm'
ORDER BY user_id;
"""
)
logger.info(f"Fixing LLM credential inputs on {len(broken_nodes)} nodes")

user_id: str = ""
user_integrations = None
for node in broken_nodes:
if node["user_id"] != user_id:
# Save queries by only fetching once per user
user_id = node["user_id"]
user_integrations = await get_user_integrations(user_id)
elif not user_integrations:
raise RuntimeError(f"Impossible state while processing node {node}")

node_id: str = node["node_id"]
node_preset_input: dict = json.loads(node["node_preset_input"])
credentials_meta: dict = node_preset_input["credentials"]

credentials = next(
(
c
for c in user_integrations.credentials
if c.id == credentials_meta["id"]
),
None,
)
if not credentials:
continue
if credentials.type != "api_key":
logger.warning(
f"User {user_id} credentials {credentials.id} with provider 'llm' "
f"has invalid type '{credentials.type}'"
)
continue

api_key = credentials.api_key.get_secret_value()
if api_key.startswith("sk-ant-api03-"):
credentials.provider = credentials_meta["provider"] = "anthropic"
elif api_key.startswith("sk-"):
credentials.provider = credentials_meta["provider"] = "openai"
elif api_key.startswith("gsk_"):
credentials.provider = credentials_meta["provider"] = "groq"
else:
logger.warning(
f"Could not identify provider from key prefix {api_key[:13]}*****"
)
continue

store.update_creds(user_id, credentials)
await AgentNode.prisma().update(
where={"id": node_id},
data={"constantInput": json.dumps(node_preset_input)},
)
4 changes: 1 addition & 3 deletions autogpt_platform/backend/backend/data/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from typing import Optional, cast

from autogpt_libs.auth.models import DEFAULT_USER_ID
from autogpt_libs.supabase_integration_credentials_store.types import (
UserIntegrations,
UserMetadata,
Expand All @@ -15,9 +16,6 @@

logger = logging.getLogger(__name__)

DEFAULT_USER_ID = "3e53486c-cf57-477e-ba2a-cb02dc828e1a"
DEFAULT_EMAIL = "[email protected]"


async def get_or_create_user(user_data: dict) -> User:
user_id = user_data.get("sub")
Expand Down
11 changes: 6 additions & 5 deletions autogpt_platform/backend/backend/server/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import enum
import typing
from typing import Any, List, Optional, Union

import pydantic

Expand All @@ -12,11 +12,12 @@ class Methods(enum.Enum):
UNSUBSCRIBE = "unsubscribe"
EXECUTION_EVENT = "execution_event"
ERROR = "error"
HEARTBEAT = "heartbeat"


class WsMessage(pydantic.BaseModel):
method: Methods
data: typing.Dict[str, typing.Any] | list[typing.Any] | None = None
data: Optional[Union[dict[str, Any], list[Any], str]] = None
success: bool | None = None
channel: str | None = None
error: str | None = None
Expand All @@ -40,8 +41,8 @@ class CreateGraph(pydantic.BaseModel):

class CreateAPIKeyRequest(pydantic.BaseModel):
name: str
permissions: typing.List[APIKeyPermission]
description: typing.Optional[str] = None
permissions: List[APIKeyPermission]
description: Optional[str] = None


class CreateAPIKeyResponse(pydantic.BaseModel):
Expand All @@ -54,4 +55,4 @@ class SetGraphActiveVersion(pydantic.BaseModel):


class UpdatePermissionsRequest(pydantic.BaseModel):
permissions: typing.List[APIKeyPermission]
permissions: List[APIKeyPermission]
2 changes: 2 additions & 0 deletions autogpt_platform/backend/backend/server/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import backend.data.block
import backend.data.db
import backend.data.graph
import backend.data.user
import backend.server.routers.v1
import backend.util.service
Expand All @@ -23,6 +24,7 @@ async def lifespan_context(app: fastapi.FastAPI):
await backend.data.db.connect()
await backend.data.block.initialize_blocks()
await backend.data.user.migrate_and_encrypt_user_integrations()
await backend.data.graph.fix_llm_provider_credentials()
yield
await backend.data.db.disconnect()

Expand Down
3 changes: 2 additions & 1 deletion autogpt_platform/backend/backend/server/routers/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def execute_graph_block(block_id: str, data: BlockInput) -> CompletedBlockOutput
async def get_user_credits(
user_id: Annotated[str, Depends(get_user_id)]
) -> dict[str, int]:
return {"credits": await _user_credit_model.get_or_refill_credit(user_id)}
# Credits can go negative, so ensure it's at least 0 for user to see.
return {"credits": max(await _user_credit_model.get_or_refill_credit(user_id), 0)}


########################################################
Expand Down
Loading

0 comments on commit 257cbfc

Please sign in to comment.