Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(integration-service): Add integrations service #520

Merged
merged 28 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8852ecf
Setup FastAPI skeleton server for integrations
HamadaSalhab Sep 24, 2024
ae6c127
wip
HamadaSalhab Sep 24, 2024
49d9fa9
Add twitter_loader & wikipedia_query integrations
HamadaSalhab Sep 24, 2024
80d77ee
Huge refactor
HamadaSalhab Sep 24, 2024
6f1df21
add hackernews_query integration
HamadaSalhab Sep 24, 2024
5b3a3c5
add tts_query integration
HamadaSalhab Sep 24, 2024
619e182
add weather_data integration
HamadaSalhab Sep 24, 2024
ec76b93
Add DocStrings to integrations implementations
HamadaSalhab Sep 24, 2024
0aba583
Reformat
HamadaSalhab Sep 24, 2024
b5e6092
Rename integrations
HamadaSalhab Sep 25, 2024
2c1b58e
fix(integrations-service): Fix bugs | Restructure files
HamadaSalhab Sep 25, 2024
0efe761
wip
HamadaSalhab Sep 25, 2024
2fb627c
add dummy send mail integration
HamadaSalhab Sep 25, 2024
e10ae08
feat(integrations-service): Add endpoint for getting available integr…
HamadaSalhab Sep 25, 2024
ed96695
feat(integrations-service): Add endpoint for getting integration as o…
HamadaSalhab Sep 25, 2024
71cd206
Rename parameters to arguments
HamadaSalhab Sep 25, 2024
b124f24
Add weather arguments & setup
HamadaSalhab Sep 25, 2024
13976a8
feat(integrations-service): Add setup to execute integration endpoint
HamadaSalhab Sep 25, 2024
79cef3d
fix(integrations-service): fix get integrations & get integration too…
HamadaSalhab Sep 25, 2024
4cbad4c
feat(integrations-service): add setup to dalle image generator integr…
HamadaSalhab Sep 25, 2024
d576843
Disable low-priority integrations
HamadaSalhab Sep 25, 2024
078321e
feat(integrations-service): Add arguments & setups
HamadaSalhab Sep 25, 2024
e197dbe
feat(integrations-service): Add pyowm package
HamadaSalhab Sep 25, 2024
d59b085
Remove print
HamadaSalhab Sep 25, 2024
8e0de53
feat(integrations-service): Add asserts for integrations setup & argu…
HamadaSalhab Sep 25, 2024
00a198e
Run poe check
HamadaSalhab Sep 25, 2024
7284c94
Merge branch 'dev' into f/integrations-service
HamadaSalhab Sep 25, 2024
36d61e9
Remove some checks
HamadaSalhab Sep 25, 2024
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
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ include:
- ./agents-api/docker-compose.yml
- ./scheduler/docker-compose.yml
- ./llm-proxy/docker-compose.yml
- ./integrations-service/docker-compose.yml

# TODO: Enable after testing
# - ./monitoring/docker-compose.yml
Expand Down
19 changes: 19 additions & 0 deletions integrations-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.11-slim

WORKDIR /app

# Install Poetry
RUN pip install poetry

# Copy only requirements to cache them in docker layer
COPY pyproject.toml poetry.lock* /app/

# Project initialization:
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi

# Copy project
COPY . ./

# Run the application
CMD ["python", "-m", "integrations.web", "--host", "0.0.0.0", "--port", "8000"]
26 changes: 26 additions & 0 deletions integrations-service/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: julep-integrations

# Shared environment variables
x--shared-environment: &shared-environment
OPENAI_API_KEY: ${OPENAI_API_KEY}

services:
integrations:
environment:
<<: *shared-environment

build: .
ports:
- "8000:8000"

develop:
watch:
- action: sync+restart
path: ./
target: /app/
ignore:
- ./**/*.pyc
- action: rebuild
path: poetry.lock
- action: rebuild
path: Dockerfile
Empty file.
17 changes: 17 additions & 0 deletions integrations-service/integrations/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from .dalle_image_generator import (
DalleImageGeneratorArguments,
DalleImageGeneratorSetup,
)
from .duckduckgo_search import DuckDuckGoSearchExecutionArguments
from .hacker_news import HackerNewsExecutionArguments

# TODO: Move these models somewhere else
from .models import (
ExecuteIntegrationArguments,
ExecuteIntegrationSetup,
IntegrationDef,
IntegrationExecutionRequest,
IntegrationExecutionResponse,
)
from .weather import WeatherExecutionArguments, WeatherExecutionSetup
from .wikipedia import WikipediaExecutionArguments
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic import BaseModel, Field


class DalleImageGeneratorSetup(BaseModel):
api_key: str = Field(str, description="The API key for DALL-E")


class DalleImageGeneratorArguments(BaseModel):
prompt: str = Field(str, description="The image generation prompt")
5 changes: 5 additions & 0 deletions integrations-service/integrations/models/duckduckgo_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel, Field


class DuckDuckGoSearchExecutionArguments(BaseModel):
query: str = Field(..., description="The search query string")
5 changes: 5 additions & 0 deletions integrations-service/integrations/models/hacker_news.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel, Field


class HackerNewsExecutionArguments(BaseModel):
url: str = Field(..., description="The URL of the Hacker News thread to fetch")
81 changes: 81 additions & 0 deletions integrations-service/integrations/models/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Literal, Union

from pydantic import BaseModel

from .dalle_image_generator import (
DalleImageGeneratorArguments,
DalleImageGeneratorSetup,
)
from .duckduckgo_search import DuckDuckGoSearchExecutionArguments
from .hacker_news import HackerNewsExecutionArguments
from .weather import WeatherExecutionArguments, WeatherExecutionSetup
from .wikipedia import WikipediaExecutionArguments

ExecuteIntegrationArguments = Union[
WikipediaExecutionArguments,
DuckDuckGoSearchExecutionArguments,
DalleImageGeneratorArguments,
WeatherExecutionArguments,
HackerNewsExecutionArguments,
]

ExecuteIntegrationSetup = Union[
DalleImageGeneratorSetup,
WeatherExecutionSetup,
]


class IntegrationExecutionRequest(BaseModel):
setup: ExecuteIntegrationSetup | None = None
"""
The setup parameters the integration accepts (such as API keys)
"""
arguments: ExecuteIntegrationArguments
"""
The arguments to pass to the integration
"""


class IntegrationExecutionResponse(BaseModel):
result: str
"""
The result of the integration execution
"""


class IntegrationDef(BaseModel):
provider: (
Literal[
"dummy",
"dalle_image_generator",
"duckduckgo_search",
"hacker_news",
"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
"""
13 changes: 13 additions & 0 deletions integrations-service/integrations/models/weather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pydantic import BaseModel, Field


class WeatherExecutionSetup(BaseModel):
openweathermap_api_key: str = Field(
..., description="The location for which to fetch weather data"
)


class WeatherExecutionArguments(BaseModel):
location: str = Field(
..., description="The location for which to fetch weather data"
)
6 changes: 6 additions & 0 deletions integrations-service/integrations/models/wikipedia.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel, Field


class WikipediaExecutionArguments(BaseModel):
query: str = Field(..., description="The search query string")
load_max_docs: int = Field(2, description="Maximum number of documents to load")
2 changes: 2 additions & 0 deletions integrations-service/integrations/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .execution.router import router as execution_router
from .integrations.router import router as integrations_router
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .execute import execute
19 changes: 19 additions & 0 deletions integrations-service/integrations/routers/execution/execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import Body, HTTPException, Path

from ...models import IntegrationExecutionRequest, IntegrationExecutionResponse
from ...utils.execute_integration import execute_integration
from .router import router


@router.post("/execute/{provider}", tags=["execution"])
async def execute(
provider: str = Path(..., description="The integration provider"),
request: IntegrationExecutionRequest = Body(
..., description="The integration execution request"
),
) -> IntegrationExecutionResponse:
try:
result = await execute_integration(provider, request.setup, request.arguments)
return IntegrationExecutionResponse(result=result)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
3 changes: 3 additions & 0 deletions integrations-service/integrations/routers/execution/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from fastapi import APIRouter

router: APIRouter = APIRouter()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .get_integration_tool import get_integration_tool
from .get_integrations import get_integrations
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import Optional

from fastapi import HTTPException

from ...models.models import IntegrationDef
from .get_integrations import get_integrations
from .router import router


def convert_to_openai_tool(integration: IntegrationDef) -> dict:
return {
"type": "function",
"function": {
"name": integration.provider,
"description": integration.description,
"parameters": {
"type": "object",
"properties": integration.arguments,
"required": [
k
for k, v in integration.arguments.items()
if v.get("required", False)
],
},
},
}


@router.get("/integrations/{provider}/tool", tags=["integration_tool"])
@router.get("/integrations/{provider}/{method}/tool", tags=["integration_tool"])
async def get_integration_tool(provider: str, method: Optional[str] = None):
integrations = await get_integrations()

for integration in integrations:
if integration.provider == provider and (
method is None or integration.method == method
):
return convert_to_openai_tool(integration)

raise HTTPException(status_code=404, detail="Integration not found")
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import importlib
import inspect
import os
from typing import Any, List

from pydantic import BaseModel

from ...models.models import IntegrationDef
from ...utils import integrations
from .router import router


def create_integration_def(module: Any) -> IntegrationDef:
module_parts = module.__name__.split(".")
if len(module_parts) > 4: # Nested integration
provider = module_parts[-2]
method = module_parts[-1]
else: # Top-level integration
provider = module_parts[-1]
method = None

# Find the first function in the module
function_name = next(
name
for name, obj in inspect.getmembers(module)
if inspect.isfunction(obj) and not name.startswith("_")
)
function = getattr(module, function_name)
signature = inspect.signature(function)

# Get the Pydantic model for the parameters
params_model = next(iter(signature.parameters.values())).annotation

# Check if the params_model is a Pydantic model
if issubclass(params_model, BaseModel):
arguments = {}
for field_name, field in params_model.model_fields.items():
field_type = field.annotation
arguments[field_name] = {
"type": field_type.__name__.lower(),
"description": field.description,
}
else:
# Fallback to a dictionary if it's not a Pydantic model
arguments = {
param.name: {"type": str(param.annotation.__name__).lower()}
for param in signature.parameters.values()
if param.name != "parameters"
}

return IntegrationDef(
provider=provider,
method=method,
description=function.__doc__.strip() if function.__doc__ else None,
arguments=arguments,
)


@router.get("/integrations", tags=["integrations"])
async def get_integrations() -> List[IntegrationDef]:
integration_defs = []
integrations_dir = os.path.dirname(integrations.__file__)

for item in os.listdir(integrations_dir):
item_path = os.path.join(integrations_dir, item)

if os.path.isdir(item_path):
# This is a toolkit
for file in os.listdir(item_path):
if file.endswith(".py") and not file.startswith("__"):
module = importlib.import_module(
f"...utils.integrations.{item}.{file[:-3]}", package=__package__
)
integration_defs.append(create_integration_def(module))
elif item.endswith(".py") and not item.startswith("__"):
# This is a single-file tool
module = importlib.import_module(
f"...utils.integrations.{item[:-3]}", package=__package__
)
integration_defs.append(create_integration_def(module))

return integration_defs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from fastapi import APIRouter

router: APIRouter = APIRouter()
26 changes: 26 additions & 0 deletions integrations-service/integrations/utils/execute_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from ..models import ExecuteIntegrationArguments, ExecuteIntegrationSetup
from .integrations.dalle_image_generator import dalle_image_generator
from .integrations.duckduckgo_search import duckduckgo_search
from .integrations.hacker_news import hacker_news
from .integrations.weather import weather
from .integrations.wikipedia import wikipedia


async def execute_integration(
provider: str,
setup: ExecuteIntegrationSetup | None,
arguments: ExecuteIntegrationArguments,
) -> str:
match provider:
case "duckduckgo_search":
return await duckduckgo_search(arguments=arguments)
case "dalle_image_generator":
return await dalle_image_generator(setup=setup, arguments=arguments)
case "wikipedia":
return await wikipedia(arguments=arguments)
case "weather":
return await weather(setup=setup, arguments=arguments)
case "hacker_news":
return await hacker_news(arguments=arguments)
case _:
raise ValueError(f"Unknown integration: {provider}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .dalle_image_generator import dalle_image_generator
from .duckduckgo_search import duckduckgo_search
from .hacker_news import hacker_news
from .weather import weather
from .wikipedia import wikipedia
Loading
Loading