diff --git a/README.md b/README.md index fc49e78a0..dad5ebd68 100644 --- a/README.md +++ b/README.md @@ -1143,8 +1143,6 @@ main: Whenever julep encounters a _user-defined function_, it pauses, giving control back to the client and waits for the client to run the function call and give the results back to julep. -> [!TIP] > **Example cookbook**: [cookbooks/13-Error_Handling_and_Recovery.py](https://github.com/julep-ai/julep/blob/dev/cookbooks/13-Error_Handling_and_Recovery.py) - ### `system` tools Built-in tools that can be used to call the julep APIs themselves, like triggering a task execution, appending to a metadata field, etc. @@ -1224,7 +1222,7 @@ Additional operations available for some resources: Note: The availability of these operations may vary depending on the specific resource and implementation details. -> [!TIP] > **Example cookbook**: [cookbooks/10-Document_Management_and_Search.py](https://github.com/julep-ai/julep/blob/dev/cookbooks/10-Document_Management_and_Search.py) +> [!TIP] > **Example cookbook**: [cookbooks/06-browser-use.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/06-browser-use.ipynb) ### Built-in `integrations` @@ -1232,7 +1230,7 @@ Julep comes with a number of built-in integrations (as described in the section See [Integrations](#integrations) for details on the available integrations. -> [!TIP] > **Example cookbook**: [cookbooks/01-Website_Crawler_using_Spider.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/01-Website_Crawler_using_Spider.ipynb) +> [!TIP] > **Example cookbook**: [cookbooks/01-website-crawler.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/01-website-crawler.ipynb) ### Direct `api_calls` @@ -1290,7 +1288,7 @@ output: -**Example cookbook**: [cookbooks/03-SmartResearcher_With_WebSearch.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/03-SmartResearcher_With_WebSearch.ipynb) +**Example cookbook**: [cookbooks/02-sarcastic-news-headline-generator.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/02-sarcastic-news-headline-generator.ipynb) @@ -1313,6 +1311,11 @@ output: + + +**Example cookbook**: [cookbooks/06-browser-use.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/06-browser-use.ipynb) + + Email @@ -1364,7 +1367,7 @@ output: -**Example cookbook**: [cookbooks/01-Website_Crawler_using_Spider.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/01-Website_Crawler_using_Spider.ipynb) +**Example cookbook**: [cookbooks/01-website-crawler.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/01-website-crawler.ipynb) @@ -1387,7 +1390,7 @@ output: -**Example cookbook**: [cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb) +**Example cookbook**: [cookbooks/03-trip-planning-assistant.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/03-trip-planning-assistant.ipynb) @@ -1409,10 +1412,87 @@ output: -**Example cookbook**: [cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/04-TripPlanner_With_Weather_And_WikiInfo.ipynb) +**Example cookbook**: [cookbooks/03-trip-planning-assistant.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/03-trip-planning-assistant.ipynb) + + + + + + FFmpeg + + +```yaml +arguments: + cmd: string # The FFmpeg command to execute + file: string # The base64 encoded file to process + +output: + fileoutput: string # The output file from the FFmpeg command in base64 encoding + result: boolean # Whether the FFmpeg command was executed successfully + mime_type: string # The MIME type of the output file +``` + + + + Llama Parse + + +```yaml +setup: + llamaparse_api_key: string # The API key for Llama Parse + +arguments: + file: string # The base64 encoded file to parse + filename: string # The filename of the file + +output: + documents: list # The parsed data from the document +``` + + + + + + + Cloudinary + + +```yaml + +method: media_upload | media_edit # The method to use for the Cloudinary integration + +setup: + cloudinary_cloud_name: string # Your Cloudinary cloud name + cloudinary_api_key: string # Your Cloudinary API key + cloudinary_api_secret: string # Your Cloudinary API secret + +arguments: + file: string # The URL of the file upload. Only available for media_upload method. + upload_params: dict # Additional parameters for the upload. Only available for media_upload method. + public_id: string # The public ID for the file. For media_edit method it is MANDATORY. For media_upload method it is optional. + transformation: list[dict] # The transformations to apply to the file + return_base64: boolean # Whether to return the file in base64 encoding + +output: + url: string # The URL of the uploaded file. Only available for media_upload method. + meta_data: dict # Additional metadata from the upload response. Only available for media_upload method. + public_id: string # The public ID of the uploaded file. Only available for media_upload method. + transformed_url: string # The transformed URL. Only available for media_edit method. + base64: string # The base64 encoded file. Only available for media_edit method. +``` + + + + + +**Example cookbook**: [cookbooks/05-video-processing-with-natural-language.ipynb](https://github.com/julep-ai/julep/blob/dev/cookbooks/05-video-processing-with-natural-language.ipynb) + + + + For more details, refer to our [Integrations Documentation](#integrations). diff --git a/agents-api/agents_api/activities/task_steps/prompt_step.py b/agents-api/agents_api/activities/task_steps/prompt_step.py index bf2b413ae..f365fa286 100644 --- a/agents-api/agents_api/activities/task_steps/prompt_step.py +++ b/agents-api/agents_api/activities/task_steps/prompt_step.py @@ -167,10 +167,25 @@ async def prompt_step(context: StepContext) -> StepOutcome: }, } formatted_tools.append(tool) - + # For non-Claude models, we don't need to send tools + # FIXME: Enable formatted_tools once format-tools PR is merged. if not is_claude_model: formatted_tools = None + # HOTFIX: for groq calls, litellm expects tool_calls_id not to be in the messages + # FIXME: This is a temporary fix. We need to update the agent-api to use the new tool calling format + # FIXME: Enable formatted_tools once format-tools PR is merged. + is_groq_model = agent_model.lower().startswith("llama-3.1") + if is_groq_model: + prompt = [ + { + k: v + for k, v in message.items() + if k not in ["tool_calls", "tool_call_id", "user", "continue_", "name"] + } + for message in prompt + ] + # Use litellm for other models completion_data: dict = { "model": agent_model, diff --git a/agents-api/agents_api/routers/sessions/chat.py b/agents-api/agents_api/routers/sessions/chat.py index f4cc7420e..830273a09 100644 --- a/agents-api/agents_api/routers/sessions/chat.py +++ b/agents-api/agents_api/routers/sessions/chat.py @@ -168,11 +168,24 @@ async def chat( } formatted_tools.append(tool) - # If not using Claude model, - + # If not using Claude model + # FIXME: Enable formatted_tools once format-tools PR is merged. if not is_claude_model: formatted_tools = None + # HOTFIX: for groq calls, litellm expects tool_calls_id not to be in the messages + # FIXME: This is a temporary fix. We need to update the agent-api to use the new tool calling format + is_groq_model = settings["model"].lower().startswith("llama-3.1") + if is_groq_model: + messages = [ + { + k: v + for k, v in message.items() + if k not in ["tool_calls", "tool_call_id", "user", "continue_", "name"] + } + for message in messages + ] + # Use litellm for other models model_response = await litellm.acompletion( messages=messages, diff --git a/integrations-service/integrations/models/models.py b/integrations-service/integrations/models/models.py deleted file mode 100644 index 8d0119e53..000000000 --- a/integrations-service/integrations/models/models.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Literal - -from pydantic import BaseModel - - -class IntegrationExecutionResponse(BaseModel): - result: str - """ - The result of the integration execution - """ - - -class IntegrationDef(BaseModel): - provider: ( - Literal[ - "dummy", - "weather", - "wikipedia", - "twitter", - "web_base", - "requests", - "gmail", - "tts_query", - ] - | None - ) = None - """ - The provider of the integration - """ - method: str | None = None - """ - The specific method of the integration to call - """ - description: str | None = None - """ - Optional description of the integration - """ - setup: dict | None = None - """ - The setup parameters the integration accepts - """ - arguments: dict | None = None - """ - The arguments to pre-apply to the integration call - """ diff --git a/integrations-service/integrations/utils/execute_integration.py b/integrations-service/integrations/utils/execute_integration.py index f98f1eec8..5fd298344 100644 --- a/integrations-service/integrations/utils/execute_integration.py +++ b/integrations-service/integrations/utils/execute_integration.py @@ -21,17 +21,13 @@ async def execute_integration( setup: ExecutionSetup | None = None, arguments: ExecutionArguments, ) -> ExecutionResponse: - provider_obj: BaseProvider | None = getattr(available_providers, provider, None) - - if not provider_obj: + provider_obj = getattr(available_providers, provider, None) + if not provider_obj or not isinstance(provider_obj, BaseProvider): raise HTTPException(status_code=400, detail=f"Unknown provider: {provider}") - assert isinstance(provider_obj, BaseProvider) - - if method is None: - method = provider_obj.methods[0].method - - elif method not in [method.method for method in provider_obj.methods]: + method = method or provider_obj.methods[0].method + method_config = next((m for m in provider_obj.methods if m.method == method), None) + if not method_config: raise HTTPException( status_code=400, detail=f"Unknown method: {method} for provider: {provider}" ) @@ -41,28 +37,22 @@ async def execute_integration( package="integrations", ) - execution_function = getattr(provider_module, method) - - setup_obj = setup - - if setup is not None: - setup_class = provider_obj.setup - - if setup_class and not isinstance(setup, setup_class): - setup_obj = setup_class(**setup.model_dump()) - - arguments_class = next( - m for m in provider_obj.methods if m.method == method - ).arguments + if ( + setup is not None + and provider_obj.setup + and not isinstance(setup, provider_obj.setup) + ): + setup = provider_obj.setup(**setup.model_dump()) + + arguments = ( + method_config.arguments(**arguments.model_dump()) + if not isinstance(arguments, method_config.arguments) + else arguments + ) - if not isinstance(arguments, arguments_class): - parsed_arguments = arguments_class(**arguments.model_dump()) - else: - parsed_arguments = arguments try: - if setup_obj: - return await execution_function(setup=setup_obj, arguments=parsed_arguments) - else: - return await execution_function(arguments=parsed_arguments) + return await getattr(provider_module, method)( + **({"setup": setup} if setup else {}), arguments=arguments + ) except BaseException as e: return ExecutionError(error=str(e)) diff --git a/integrations-service/integrations/utils/integrations/browserbase.py b/integrations-service/integrations/utils/integrations/browserbase.py index a3fd1f2f8..c59e69138 100644 --- a/integrations-service/integrations/utils/integrations/browserbase.py +++ b/integrations-service/integrations/utils/integrations/browserbase.py @@ -38,10 +38,14 @@ def get_browserbase_client(setup: BrowserbaseSetup) -> Browserbase: - if setup.api_key == "DEMO_API_KEY": - setup.api_key = browserbase_api_key - if setup.project_id == "DEMO_PROJECT_ID": - setup.project_id = browserbase_project_id + setup.api_key = ( + browserbase_api_key if setup.api_key == "DEMO_API_KEY" else setup.api_key + ) + setup.project_id = ( + browserbase_project_id + if setup.project_id == "DEMO_PROJECT_ID" + else setup.project_id + ) return Browserbase( api_key=setup.api_key, diff --git a/integrations-service/integrations/utils/integrations/llama_parse.py b/integrations-service/integrations/utils/integrations/llama_parse.py index 5d15debe5..509b6dfb4 100644 --- a/integrations-service/integrations/utils/integrations/llama_parse.py +++ b/integrations-service/integrations/utils/integrations/llama_parse.py @@ -26,23 +26,23 @@ async def parse( assert isinstance(setup, LlamaParseSetup), "Invalid setup" assert isinstance(arguments, LlamaParseFetchArguments), "Invalid arguments" - if setup.llamaparse_api_key == "DEMO_API_KEY": - setup.llamaparse_api_key = llama_api_key + # Use walrus operator to simplify assignment and condition + if (api_key := setup.llamaparse_api_key) == "DEMO_API_KEY": + api_key = llama_api_key parser = LlamaParse( - api_key=setup.llamaparse_api_key, + api_key=api_key, # Use the local variable instead result_type=arguments.result_format, num_workers=arguments.num_workers, language=arguments.language, ) - # Decode base64 file content - file_content = base64.b64decode(arguments.file) - extra_info = { - "file_name": arguments.filename if arguments.filename else str(uuid.uuid4()) - } + # Simplify filename assignment using or operator + extra_info = {"file_name": arguments.filename or str(uuid.uuid4())} - # Parse the document - documents = await parser.aload_data(file_content, extra_info=extra_info) + # Parse the document (decode inline) + documents = await parser.aload_data( + base64.b64decode(arguments.file), extra_info=extra_info + ) return LlamaParseFetchOutput(documents=documents) diff --git a/integrations-service/integrations/utils/integrations/spider.py b/integrations-service/integrations/utils/integrations/spider.py index baac7c5e6..a8403b3cf 100644 --- a/integrations-service/integrations/utils/integrations/spider.py +++ b/integrations-service/integrations/utils/integrations/spider.py @@ -1,3 +1,7 @@ +import asyncio +from functools import lru_cache +from typing import Any, Dict + from beartype import beartype from langchain_community.document_loaders import SpiderLoader from tenacity import retry, stop_after_attempt, wait_exponential @@ -7,6 +11,11 @@ from ...models import SpiderFetchOutput +# Spider client instances +def get_spider_client(api_key: str, **kwargs) -> SpiderLoader: + return SpiderLoader(api_key=api_key, **kwargs) + + @beartype @retry( wait=wait_exponential(multiplier=1, min=4, max=10), @@ -23,21 +32,18 @@ async def crawl( assert isinstance(setup, SpiderSetup), "Invalid setup" assert isinstance(arguments, SpiderFetchArguments), "Invalid arguments" - url = arguments.url - - if not url: - raise ValueError("URL parameter is required for spider") - - if setup.spider_api_key == "DEMO_API_KEY": - setup.spider_api_key = spider_api_key + api_key = ( + setup.spider_api_key + if setup.spider_api_key != "DEMO_API_KEY" + else spider_api_key + ) - spider_loader = SpiderLoader( - api_key=setup.spider_api_key, - url=str(url), + spider_loader = get_spider_client( + api_key=api_key, + url=str(arguments.url), mode=arguments.mode, params=arguments.params, ) - documents = spider_loader.load() - + documents = await asyncio.to_thread(spider_loader.load) return SpiderFetchOutput(documents=documents) diff --git a/integrations-service/integrations/web.py b/integrations-service/integrations/web.py index a1c77f130..905d9abc1 100644 --- a/integrations-service/integrations/web.py +++ b/integrations-service/integrations/web.py @@ -7,17 +7,29 @@ import uvloop from fastapi import FastAPI, Request, status from fastapi.exceptions import HTTPException, RequestValidationError +from fastapi.middleware.gzip import GZipMiddleware from fastapi.responses import JSONResponse from .routers.execution.router import router as execution_router from .routers.integrations.router import router as integrations_router -app: FastAPI = FastAPI() +app: FastAPI = FastAPI( + title="Integrations Service", + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", + default_response_class=JSONResponse, +) + +# Add GZIP compression +app.add_middleware(GZipMiddleware, minimum_size=1000) # Add routers app.include_router(integrations_router) app.include_router(execution_router) +# Optimize event loop policy +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) logger: logging.Logger = logging.getLogger(__name__) @@ -84,6 +96,3 @@ def main( workers=workers, reload=reload, ) - - -asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())