Merge remote-tracking branch 'origin/dev' into NGNMergeDev
This commit is contained in:
commit
88d91c48d8
274 changed files with 11682 additions and 3913 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from importlib import metadata
|
||||
|
||||
# Deactivate cache manager for now
|
||||
# from langflow.services.cache import cache_manager
|
||||
# from langflow.services.cache import cache_service
|
||||
from langflow.processing.process import load_flow_from_json
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
|
@ -12,4 +12,4 @@ except metadata.PackageNotFoundError:
|
|||
__version__ = ""
|
||||
del metadata # optional, avoids polluting the results of dir(__package__)
|
||||
|
||||
__all__ = ["load_flow_from_json", "cache_manager", "CustomComponent"]
|
||||
__all__ = ["load_flow_from_json", "cache_service", "CustomComponent"]
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
import platform
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import httpx
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.manager import initialize_services, initialize_settings_manager
|
||||
from langflow.services.utils import get_db_manager, get_settings_manager
|
||||
|
||||
from multiprocess import Process, cpu_count # type: ignore
|
||||
import platform
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import socket
|
||||
from rich.panel import Panel
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from dotenv import load_dotenv
|
||||
from langflow.main import setup_app
|
||||
from langflow.services.database.utils import session_getter
|
||||
from langflow.services.getters import get_db_service, get_settings_service
|
||||
from langflow.services.utils import initialize_services, initialize_settings_service
|
||||
from langflow.utils.logger import configure, logger
|
||||
from multiprocess import Process, cpu_count # type: ignore
|
||||
from rich import box
|
||||
from rich import print as rprint
|
||||
from rich.table import Table
|
||||
import typer
|
||||
from langflow.main import setup_app
|
||||
from langflow.utils.logger import configure, logger
|
||||
import webbrowser
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
app = typer.Typer()
|
||||
app = typer.Typer(no_args_is_help=True)
|
||||
|
||||
|
||||
def get_number_of_workers(workers=None):
|
||||
|
|
@ -53,9 +52,21 @@ def display_results(results):
|
|||
console.print() # Print a new line
|
||||
|
||||
|
||||
def set_var_for_macos_issue():
|
||||
# OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
|
||||
# we need to set this var is we are running on MacOS
|
||||
# otherwise we get an error when running gunicorn
|
||||
|
||||
if platform.system() in ["Darwin"]:
|
||||
import os
|
||||
|
||||
os.environ["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
||||
logger.debug("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY to YES to avoid error")
|
||||
|
||||
|
||||
def update_settings(
|
||||
config: str,
|
||||
cache: str,
|
||||
cache: Optional[str] = None,
|
||||
dev: bool = False,
|
||||
remove_api_keys: bool = False,
|
||||
components_path: Optional[Path] = None,
|
||||
|
|
@ -63,66 +74,20 @@ def update_settings(
|
|||
"""Update the settings from a config file."""
|
||||
|
||||
# Check for database_url in the environment variables
|
||||
initialize_settings_manager()
|
||||
settings_manager = get_settings_manager()
|
||||
initialize_settings_service()
|
||||
settings_service = get_settings_service()
|
||||
if config:
|
||||
logger.debug(f"Loading settings from {config}")
|
||||
settings_manager.settings.update_from_yaml(config, dev=dev)
|
||||
settings_service.settings.update_from_yaml(config, dev=dev)
|
||||
if remove_api_keys:
|
||||
logger.debug(f"Setting remove_api_keys to {remove_api_keys}")
|
||||
settings_manager.settings.update_settings(REMOVE_API_KEYS=remove_api_keys)
|
||||
settings_service.settings.update_settings(REMOVE_API_KEYS=remove_api_keys)
|
||||
if cache:
|
||||
logger.debug(f"Setting cache to {cache}")
|
||||
settings_manager.settings.update_settings(CACHE=cache)
|
||||
settings_service.settings.update_settings(CACHE=cache)
|
||||
if components_path:
|
||||
logger.debug(f"Adding component path {components_path}")
|
||||
settings_manager.settings.update_settings(COMPONENTS_PATH=components_path)
|
||||
|
||||
|
||||
def serve_on_jcloud():
|
||||
"""
|
||||
Deploy Langflow server on Jina AI Cloud
|
||||
"""
|
||||
import asyncio
|
||||
from importlib.metadata import version as mod_version
|
||||
|
||||
import click
|
||||
|
||||
try:
|
||||
from lcserve.__main__ import serve_on_jcloud # type: ignore
|
||||
except ImportError:
|
||||
click.secho(
|
||||
"🚨 Please install langchain-serve to deploy Langflow server on Jina AI Cloud "
|
||||
"using `pip install langchain-serve`",
|
||||
fg="red",
|
||||
)
|
||||
return
|
||||
|
||||
app_name = "langflow.lcserve:app"
|
||||
app_dir = str(Path(__file__).parent)
|
||||
version = mod_version("langflow")
|
||||
base_image = "jinaai+docker://deepankarm/langflow"
|
||||
|
||||
click.echo("🚀 Deploying Langflow server on Jina AI Cloud")
|
||||
app_id = asyncio.run(
|
||||
serve_on_jcloud(
|
||||
fastapi_app_str=app_name,
|
||||
app_dir=app_dir,
|
||||
uses=f"{base_image}:{version}",
|
||||
name="langflow",
|
||||
)
|
||||
)
|
||||
click.secho(
|
||||
"🎉 Langflow server successfully deployed on Jina AI Cloud 🎉", fg="green"
|
||||
)
|
||||
click.secho(
|
||||
"🔗 Click on the link to open the server (please allow ~1-2 minutes for the server to startup): ",
|
||||
nl=False,
|
||||
fg="green",
|
||||
)
|
||||
click.secho(f"https://{app_id}.wolf.jina.ai/", fg="blue")
|
||||
click.secho("📖 Read more about managing the server: ", nl=False, fg="green")
|
||||
click.secho("https://github.com/jina-ai/langchain-serve", fg="blue")
|
||||
settings_service.settings.update_settings(COMPONENTS_PATH=components_path)
|
||||
|
||||
|
||||
@app.command()
|
||||
|
|
@ -131,7 +96,7 @@ def run(
|
|||
"127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"
|
||||
),
|
||||
workers: int = typer.Option(
|
||||
2, help="Number of worker processes.", envvar="LANGFLOW_WORKERS"
|
||||
1, help="Number of worker processes.", envvar="LANGFLOW_WORKERS"
|
||||
),
|
||||
timeout: int = typer.Option(300, help="Worker timeout in seconds."),
|
||||
port: int = typer.Option(7860, help="Port to listen on.", envvar="LANGFLOW_PORT"),
|
||||
|
|
@ -153,12 +118,11 @@ def run(
|
|||
log_file: Path = typer.Option(
|
||||
"logs/langflow.log", help="Path to the log file.", envvar="LANGFLOW_LOG_FILE"
|
||||
),
|
||||
cache: str = typer.Option(
|
||||
cache: Optional[str] = typer.Option(
|
||||
envvar="LANGFLOW_LANGCHAIN_CACHE",
|
||||
help="Type of cache to use. (InMemoryCache, SQLiteCache)",
|
||||
default="SQLiteCache",
|
||||
default=None,
|
||||
),
|
||||
jcloud: bool = typer.Option(False, help="Deploy on Jina AI Cloud"),
|
||||
dev: bool = typer.Option(False, help="Run in development mode (may contain bugs)"),
|
||||
# This variable does not work but is set by the .env file
|
||||
# and works with Pydantic
|
||||
|
|
@ -189,15 +153,15 @@ def run(
|
|||
),
|
||||
):
|
||||
"""
|
||||
Run the Langflow server.
|
||||
Run the Langflow.
|
||||
"""
|
||||
|
||||
set_var_for_macos_issue()
|
||||
# override env variables with .env file
|
||||
|
||||
if env_file:
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
if jcloud:
|
||||
return serve_on_jcloud()
|
||||
|
||||
configure(log_level=log_level, log_file=log_file)
|
||||
update_settings(
|
||||
config,
|
||||
|
|
@ -216,7 +180,6 @@ def run(
|
|||
options = {
|
||||
"bind": f"{host}:{port}",
|
||||
"workers": get_number_of_workers(workers),
|
||||
"worker_class": "uvicorn.workers.UvicornWorker",
|
||||
"timeout": timeout,
|
||||
}
|
||||
|
||||
|
|
@ -350,18 +313,25 @@ def superuser(
|
|||
password: str = typer.Option(
|
||||
..., prompt=True, hide_input=True, help="Password for the superuser."
|
||||
),
|
||||
log_level: str = typer.Option(
|
||||
"critical", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"
|
||||
),
|
||||
):
|
||||
"""
|
||||
Create a superuser.
|
||||
"""
|
||||
configure(log_level=log_level)
|
||||
initialize_services()
|
||||
db_manager = get_db_manager()
|
||||
with session_getter(db_manager) as session:
|
||||
db_service = get_db_service()
|
||||
with session_getter(db_service) as session:
|
||||
from langflow.services.auth.utils import create_super_user
|
||||
|
||||
if create_super_user(db=session, username=username, password=password):
|
||||
# Verify that the superuser was created
|
||||
from langflow.services.database.models.user.user import User
|
||||
|
||||
user = session.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
user: User = session.query(User).filter(User.username == username).first()
|
||||
if user is None or not user.is_superuser:
|
||||
typer.echo("Superuser creation failed.")
|
||||
return
|
||||
|
||||
|
|
@ -372,12 +342,15 @@ def superuser(
|
|||
|
||||
|
||||
@app.command()
|
||||
def migration(test: bool = typer.Option(False, help="Run migrations in test mode.")):
|
||||
def migration(test: bool = typer.Option(True, help="Run migrations in test mode.")):
|
||||
"""
|
||||
Run or test migrations.
|
||||
"""
|
||||
initialize_services()
|
||||
db_manager = get_db_manager()
|
||||
db_service = get_db_service()
|
||||
if not test:
|
||||
db_manager.run_migrations()
|
||||
results = db_manager.run_migrations_test()
|
||||
db_service.run_migrations()
|
||||
results = db_service.run_migrations_test()
|
||||
display_results(results)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
"""Add profile-image column
|
||||
|
||||
Revision ID: 67cc006d50bf
|
||||
Revises: 260dbcc8b680
|
||||
Create Date: 2023-09-08 07:36:13.387318
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "67cc006d50bf"
|
||||
down_revision: Union[str, None] = "260dbcc8b680"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
if "user" in inspector.get_table_names() and "profile_image" not in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"profile_image", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
if "user" in inspector.get_table_names() and "profile_image" in [
|
||||
column["name"] for column in inspector.get_columns("user")
|
||||
]:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_column("profile_image")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
"""Change columns to be nullable
|
||||
|
||||
Revision ID: eb5866d51fd2
|
||||
Revises: 67cc006d50bf
|
||||
Create Date: 2023-10-04 10:18:25.640458
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel # noqa: F401
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "eb5866d51fd2"
|
||||
down_revision: Union[str, None] = "67cc006d50bf"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
op.drop_table("flowstyle")
|
||||
with op.batch_alter_table("component", schema=None) as batch_op:
|
||||
batch_op.drop_index("ix_component_frontend_node_id")
|
||||
batch_op.drop_index("ix_component_name")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
op.drop_table("component")
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
op.create_table(
|
||||
"component",
|
||||
sa.Column("id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("frontend_node_id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("name", sa.VARCHAR(), nullable=False),
|
||||
sa.Column("description", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("python_code", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("return_type", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("is_disabled", sa.BOOLEAN(), nullable=False),
|
||||
sa.Column("is_read_only", sa.BOOLEAN(), nullable=False),
|
||||
sa.Column("create_at", sa.DATETIME(), nullable=False),
|
||||
sa.Column("update_at", sa.DATETIME(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
with op.batch_alter_table("component", schema=None) as batch_op:
|
||||
batch_op.create_index("ix_component_name", ["name"], unique=False)
|
||||
batch_op.create_index(
|
||||
"ix_component_frontend_node_id", ["frontend_node_id"], unique=False
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
op.create_table(
|
||||
"flowstyle",
|
||||
sa.Column("color", sa.VARCHAR(), nullable=False),
|
||||
sa.Column("emoji", sa.VARCHAR(), nullable=False),
|
||||
sa.Column("flow_id", sa.CHAR(length=32), nullable=True),
|
||||
sa.Column("id", sa.CHAR(length=32), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["flow_id"],
|
||||
["flow.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -59,33 +59,6 @@ def build_input_keys_response(langchain_object, artifacts):
|
|||
return input_keys_response
|
||||
|
||||
|
||||
def merge_nested_dicts(dict1, dict2):
|
||||
for key, value in dict2.items():
|
||||
if isinstance(value, dict) and isinstance(dict1.get(key), dict):
|
||||
dict1[key] = merge_nested_dicts(dict1[key], value)
|
||||
else:
|
||||
dict1[key] = value
|
||||
return dict1
|
||||
|
||||
|
||||
def merge_nested_dicts_with_renaming(dict1, dict2):
|
||||
for key, value in dict2.items():
|
||||
if (
|
||||
key in dict1
|
||||
and isinstance(value, dict)
|
||||
and isinstance(dict1.get(key), dict)
|
||||
):
|
||||
for sub_key, sub_value in value.items():
|
||||
if sub_key in dict1[key]:
|
||||
new_key = get_new_key(dict1[key], sub_key)
|
||||
dict1[key][new_key] = sub_value
|
||||
else:
|
||||
dict1[key][sub_key] = sub_value
|
||||
else:
|
||||
dict1[key] = value
|
||||
return dict1
|
||||
|
||||
|
||||
def get_new_key(dictionary, original_key):
|
||||
counter = 1
|
||||
new_key = original_key + " (" + str(counter) + ")"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from langflow.services.database.models.api_key.crud import (
|
|||
delete_api_key,
|
||||
)
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.getters import get_session
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import asyncio
|
||||
from uuid import UUID
|
||||
|
||||
from langchain.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
|
||||
|
||||
from langflow.api.v1.schemas import ChatResponse
|
||||
from langflow.api.v1.schemas import ChatResponse, PromptResponse
|
||||
|
||||
|
||||
from typing import Any, Dict, List, Union
|
||||
from fastapi import WebSocket
|
||||
from typing import Any, Dict, List, Optional
|
||||
from langflow.services.getters import get_chat_service
|
||||
|
||||
|
||||
from langchain.schema import AgentAction, LLMResult, AgentFinish
|
||||
from langflow.utils.util import remove_ansi_escape_codes
|
||||
from langchain.schema import AgentAction, AgentFinish
|
||||
from loguru import logger
|
||||
|
||||
|
||||
|
|
@ -17,39 +19,15 @@ from loguru import logger
|
|||
class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
||||
"""Callback handler for streaming LLM responses."""
|
||||
|
||||
def __init__(self, websocket: WebSocket):
|
||||
self.websocket = websocket
|
||||
def __init__(self, client_id: str):
|
||||
self.chat_service = get_chat_service()
|
||||
self.client_id = client_id
|
||||
self.websocket = self.chat_service.active_connections[self.client_id]
|
||||
|
||||
async def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
|
||||
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
|
||||
await self.websocket.send_json(resp.dict())
|
||||
|
||||
async def on_llm_start(
|
||||
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
|
||||
) -> Any:
|
||||
"""Run when LLM starts running."""
|
||||
|
||||
async def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
|
||||
"""Run when LLM ends running."""
|
||||
|
||||
async def on_llm_error(
|
||||
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
|
||||
) -> Any:
|
||||
"""Run when LLM errors."""
|
||||
|
||||
async def on_chain_start(
|
||||
self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
|
||||
) -> Any:
|
||||
"""Run when chain starts running."""
|
||||
|
||||
async def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any:
|
||||
"""Run when chain ends running."""
|
||||
|
||||
async def on_chain_error(
|
||||
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
|
||||
) -> Any:
|
||||
"""Run when chain errors."""
|
||||
|
||||
async def on_tool_start(
|
||||
self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
|
||||
) -> Any:
|
||||
|
|
@ -95,8 +73,14 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
logger.error(f"Error sending response: {exc}")
|
||||
|
||||
async def on_tool_error(
|
||||
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
|
||||
) -> Any:
|
||||
self,
|
||||
error: BaseException,
|
||||
*,
|
||||
run_id: UUID,
|
||||
parent_run_id: Optional[UUID] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Run when tool errors."""
|
||||
|
||||
async def on_text(self, text: str, **kwargs: Any) -> Any:
|
||||
|
|
@ -104,6 +88,14 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
# This runs when first sending the prompt
|
||||
# to the LLM, adding it will send the final prompt
|
||||
# to the frontend
|
||||
if "Prompt after formatting" in text:
|
||||
text = text.replace("Prompt after formatting:\n", "")
|
||||
text = remove_ansi_escape_codes(text)
|
||||
resp = PromptResponse(
|
||||
prompt=text,
|
||||
)
|
||||
await self.websocket.send_json(resp.dict())
|
||||
self.chat_service.chat_history.add_message(self.client_id, resp)
|
||||
|
||||
async def on_agent_action(self, action: AgentAction, **kwargs: Any):
|
||||
log = f"Thought: {action.log}"
|
||||
|
|
@ -131,8 +123,10 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
class StreamingLLMCallbackHandler(BaseCallbackHandler):
|
||||
"""Callback handler for streaming LLM responses."""
|
||||
|
||||
def __init__(self, websocket):
|
||||
self.websocket = websocket
|
||||
def __init__(self, client_id: str):
|
||||
self.chat_service = get_chat_service()
|
||||
self.client_id = client_id
|
||||
self.websocket = self.chat_service.active_connections[self.client_id]
|
||||
|
||||
def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
|
||||
resp = ChatResponse(message=token, type="stream", intermediate_steps="")
|
||||
|
|
|
|||
|
|
@ -13,17 +13,16 @@ from langflow.api.v1.schemas import BuildStatus, BuiltResponse, InitResponse, St
|
|||
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.services.auth.utils import get_current_active_user, get_current_user
|
||||
from langflow.services.cache.utils import update_build_status
|
||||
from loguru import logger
|
||||
from langflow.services.utils import get_chat_manager, get_session
|
||||
from cachetools import LRUCache
|
||||
from langflow.services.getters import get_chat_service, get_session, get_cache_service
|
||||
from sqlmodel import Session
|
||||
from langflow.services.chat.manager import ChatManager
|
||||
from langflow.services.chat.manager import ChatService
|
||||
from langflow.services.cache.manager import BaseCacheService
|
||||
|
||||
|
||||
router = APIRouter(tags=["Chat"])
|
||||
|
||||
flow_data_store: LRUCache = LRUCache(maxsize=10)
|
||||
|
||||
|
||||
@router.websocket("/chat/{client_id}")
|
||||
async def chat(
|
||||
|
|
@ -31,7 +30,7 @@ async def chat(
|
|||
websocket: WebSocket,
|
||||
token: str = Query(...),
|
||||
db: Session = Depends(get_session),
|
||||
chat_manager: "ChatManager" = Depends(get_chat_manager),
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
):
|
||||
"""Websocket endpoint for chat."""
|
||||
try:
|
||||
|
|
@ -46,15 +45,15 @@ async def chat(
|
|||
code=status.WS_1008_POLICY_VIOLATION, reason="Unauthorized"
|
||||
)
|
||||
|
||||
if client_id in chat_manager.in_memory_cache:
|
||||
await chat_manager.handle_websocket(client_id, websocket)
|
||||
if client_id in chat_service.cache_service:
|
||||
await chat_service.handle_websocket(client_id, websocket)
|
||||
else:
|
||||
# We accept the connection but close it immediately
|
||||
# if the flow is not built yet
|
||||
message = "Please, build the flow before sending messages"
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=message)
|
||||
except WebSocketException as exc:
|
||||
logger.error(f"Websocket error: {exc}")
|
||||
logger.error(f"Websocket exrror: {exc}")
|
||||
await websocket.close(code=status.WS_1011_INTERNAL_ERROR, reason=str(exc))
|
||||
except Exception as exc:
|
||||
logger.error(f"Error in chat websocket: {exc}")
|
||||
|
|
@ -72,26 +71,26 @@ async def init_build(
|
|||
graph_data: dict,
|
||||
flow_id: str,
|
||||
current_user=Depends(get_current_active_user),
|
||||
chat_manager: "ChatManager" = Depends(get_chat_manager),
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
cache_service: "BaseCacheService" = Depends(get_cache_service),
|
||||
):
|
||||
"""Initialize the build by storing graph data and returning a unique session ID."""
|
||||
|
||||
try:
|
||||
if flow_id is None:
|
||||
raise ValueError("No ID provided")
|
||||
# Check if already building
|
||||
if (
|
||||
flow_id in flow_data_store
|
||||
and flow_data_store[flow_id]["status"] == BuildStatus.IN_PROGRESS
|
||||
flow_id in cache_service
|
||||
and isinstance(cache_service[flow_id], dict)
|
||||
and cache_service[flow_id].get("status") == BuildStatus.IN_PROGRESS
|
||||
):
|
||||
return InitResponse(flowId=flow_id)
|
||||
|
||||
# Delete from cache if already exists
|
||||
if flow_id in chat_manager.in_memory_cache:
|
||||
with chat_manager.in_memory_cache._lock:
|
||||
chat_manager.in_memory_cache.delete(flow_id)
|
||||
logger.debug(f"Deleted flow {flow_id} from cache")
|
||||
flow_data_store[flow_id] = {
|
||||
if flow_id in chat_service.cache_service:
|
||||
chat_service.cache_service.delete(flow_id)
|
||||
logger.debug(f"Deleted flow {flow_id} from cache")
|
||||
cache_service[flow_id] = {
|
||||
"graph_data": graph_data,
|
||||
"status": BuildStatus.STARTED,
|
||||
"user_id": current_user.id,
|
||||
|
|
@ -104,12 +103,14 @@ async def init_build(
|
|||
|
||||
|
||||
@router.get("/build/{flow_id}/status", response_model=BuiltResponse)
|
||||
async def build_status(flow_id: str):
|
||||
"""Check the flow_id is in the flow_data_store."""
|
||||
async def build_status(
|
||||
flow_id: str, cache_service: "BaseCacheService" = Depends(get_cache_service)
|
||||
):
|
||||
"""Check the flow_id is in the cache_service."""
|
||||
try:
|
||||
built = (
|
||||
flow_id in flow_data_store
|
||||
and flow_data_store[flow_id]["status"] == BuildStatus.SUCCESS
|
||||
flow_id in cache_service
|
||||
and cache_service[flow_id]["status"] == BuildStatus.SUCCESS
|
||||
)
|
||||
|
||||
return BuiltResponse(
|
||||
|
|
@ -123,7 +124,9 @@ async def build_status(flow_id: str):
|
|||
|
||||
@router.get("/build/stream/{flow_id}", response_class=StreamingResponse)
|
||||
async def stream_build(
|
||||
flow_id: str, chat_manager: "ChatManager" = Depends(get_chat_manager)
|
||||
flow_id: str,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
cache_service: "BaseCacheService" = Depends(get_cache_service),
|
||||
):
|
||||
"""Stream the build process based on stored flow data."""
|
||||
|
||||
|
|
@ -131,18 +134,18 @@ async def stream_build(
|
|||
final_response = {"end_of_stream": True}
|
||||
artifacts = {}
|
||||
try:
|
||||
if flow_id not in flow_data_store:
|
||||
if flow_id not in cache_service:
|
||||
error_message = "Invalid session ID"
|
||||
yield str(StreamData(event="error", data={"error": error_message}))
|
||||
return
|
||||
|
||||
if flow_data_store[flow_id].get("status") == BuildStatus.IN_PROGRESS:
|
||||
if cache_service[flow_id].get("status") == BuildStatus.IN_PROGRESS:
|
||||
error_message = "Already building"
|
||||
yield str(StreamData(event="error", data={"error": error_message}))
|
||||
return
|
||||
|
||||
graph_data = flow_data_store[flow_id].get("graph_data")
|
||||
user_id = flow_data_store[flow_id]["user_id"]
|
||||
graph_data = cache_service[flow_id].get("graph_data")
|
||||
cache_service[flow_id]["user_id"]
|
||||
|
||||
if not graph_data:
|
||||
error_message = "No data provided"
|
||||
|
|
@ -155,7 +158,7 @@ async def stream_build(
|
|||
graph = Graph.from_payload(graph_data)
|
||||
|
||||
number_of_nodes = len(graph.nodes)
|
||||
flow_data_store[flow_id]["status"] = BuildStatus.IN_PROGRESS
|
||||
update_build_status(cache_service, flow_id, BuildStatus.IN_PROGRESS)
|
||||
|
||||
for i, vertex in enumerate(graph.generator_build(), 1):
|
||||
try:
|
||||
|
|
@ -163,8 +166,10 @@ async def stream_build(
|
|||
"log": f"Building node {vertex.vertex_type}",
|
||||
}
|
||||
yield str(StreamData(event="log", data=log_dict))
|
||||
vertex.build(user_id)
|
||||
|
||||
if vertex.is_task:
|
||||
vertex = try_running_celery_task(vertex)
|
||||
else:
|
||||
vertex.build()
|
||||
params = vertex._built_object_repr()
|
||||
valid = True
|
||||
logger.debug(f"Building node {str(vertex.vertex_type)}")
|
||||
|
|
@ -180,7 +185,7 @@ async def stream_build(
|
|||
logger.exception(exc)
|
||||
params = str(exc)
|
||||
valid = False
|
||||
flow_data_store[flow_id]["status"] = BuildStatus.FAILURE
|
||||
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||
|
||||
vertex_id = (
|
||||
vertex.parent_node_id if vertex.parent_is_top_level else vertex.id
|
||||
|
|
@ -208,14 +213,15 @@ async def stream_build(
|
|||
"handle_keys": [],
|
||||
}
|
||||
yield str(StreamData(event="message", data=input_keys_response))
|
||||
chat_manager.set_cache(flow_id, langchain_object)
|
||||
chat_service.set_cache(flow_id, langchain_object)
|
||||
# We need to reset the chat history
|
||||
chat_manager.chat_history.empty_history(flow_id)
|
||||
flow_data_store[flow_id]["status"] = BuildStatus.SUCCESS
|
||||
chat_service.chat_history.empty_history(flow_id)
|
||||
update_build_status(cache_service, flow_id, BuildStatus.SUCCESS)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
logger.error("Error while building the flow: %s", exc)
|
||||
flow_data_store[flow_id]["status"] = BuildStatus.FAILURE
|
||||
|
||||
update_build_status(cache_service, flow_id, BuildStatus.FAILURE)
|
||||
yield str(StreamData(event="error", data={"error": str(exc)}))
|
||||
finally:
|
||||
yield str(StreamData(event="message", data=final_response))
|
||||
|
|
@ -225,3 +231,19 @@ async def stream_build(
|
|||
except Exception as exc:
|
||||
logger.error(f"Error streaming build: {exc}")
|
||||
raise HTTPException(status_code=500, detail=str(exc))
|
||||
|
||||
|
||||
def try_running_celery_task(vertex):
|
||||
# Try running the task in celery
|
||||
# and set the task_id to the local vertex
|
||||
# if it fails, run the task locally
|
||||
try:
|
||||
from langflow.worker import build_vertex
|
||||
|
||||
task = build_vertex.delay(vertex)
|
||||
vertex.task_id = task.id
|
||||
except Exception as exc:
|
||||
logger.debug(f"Error running task in celery: {exc}")
|
||||
vertex.task_id = None
|
||||
vertex.build()
|
||||
return vertex
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from datetime import timezone
|
|||
from typing import List
|
||||
from uuid import UUID
|
||||
from langflow.services.database.models.component import Component, ComponentModel
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.getters import get_session
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Annotated, Any, Optional, Union
|
||||
from typing import Annotated, Optional, Union
|
||||
from langflow.services.auth.utils import api_key_security, get_current_active_user
|
||||
|
||||
|
||||
from langflow.services.cache.utils import save_uploaded_file
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.processing.process import process_graph_cached, process_tweaks
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import (
|
||||
get_session_service,
|
||||
get_settings_service,
|
||||
get_task_service,
|
||||
)
|
||||
from loguru import logger
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, Body, status
|
||||
import sqlalchemy as sa
|
||||
|
|
@ -15,66 +20,43 @@ from langflow.interface.custom.custom_component import CustomComponent
|
|||
|
||||
from langflow.api.v1.schemas import (
|
||||
ProcessResponse,
|
||||
TaskResponse,
|
||||
TaskStatusResponse,
|
||||
UploadFileResponse,
|
||||
CustomComponentCode,
|
||||
)
|
||||
|
||||
from langflow.api.utils import merge_nested_dicts_with_renaming
|
||||
|
||||
from langflow.interface.types import (
|
||||
build_langchain_types_dict,
|
||||
build_langchain_template_custom_component,
|
||||
build_langchain_custom_component_list_from_path,
|
||||
)
|
||||
from langflow.services.getters import get_session
|
||||
|
||||
try:
|
||||
from langflow.worker import process_graph_cached_task
|
||||
except ImportError:
|
||||
|
||||
def process_graph_cached_task(*args, **kwargs):
|
||||
raise NotImplementedError("Celery is not installed")
|
||||
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
from langflow.services.task.manager import TaskService
|
||||
|
||||
# build router
|
||||
router = APIRouter(tags=["Base"])
|
||||
|
||||
|
||||
@router.get("/all", dependencies=[Depends(get_current_active_user)])
|
||||
def get_all(
|
||||
settings_manager=Depends(get_settings_manager),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
from langflow.interface.types import get_all_types_dict
|
||||
|
||||
logger.debug("Building langchain types dict")
|
||||
native_components = build_langchain_types_dict()
|
||||
# custom_components is a list of dicts
|
||||
# need to merge all the keys into one dict
|
||||
custom_components_from_file: dict[str, Any] = {}
|
||||
if settings_manager.settings.COMPONENTS_PATH:
|
||||
logger.info(
|
||||
f"Building custom components from {settings_manager.settings.COMPONENTS_PATH}"
|
||||
)
|
||||
|
||||
custom_component_dicts = []
|
||||
processed_paths = []
|
||||
for path in settings_manager.settings.COMPONENTS_PATH:
|
||||
if str(path) in processed_paths:
|
||||
continue
|
||||
custom_component_dict = build_langchain_custom_component_list_from_path(
|
||||
str(path)
|
||||
)
|
||||
custom_component_dicts.append(custom_component_dict)
|
||||
processed_paths.append(str(path))
|
||||
|
||||
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
|
||||
for custom_component_dict in custom_component_dicts:
|
||||
# custom_component_dict is a dict of dicts
|
||||
if not custom_component_dict:
|
||||
continue
|
||||
category = list(custom_component_dict.keys())[0]
|
||||
logger.info(
|
||||
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
|
||||
)
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
)
|
||||
|
||||
return merge_nested_dicts_with_renaming(
|
||||
native_components, custom_components_from_file
|
||||
)
|
||||
try:
|
||||
return get_all_types_dict(settings_service)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
# For backwards compatibility we will keep the old endpoint
|
||||
|
|
@ -94,7 +76,9 @@ async def process(
|
|||
tweaks: Optional[dict] = None,
|
||||
clear_cache: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
task_service: "TaskService" = Depends(get_task_service),
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
sync: Annotated[bool, Body(embed=True)] = True, # noqa: F821
|
||||
):
|
||||
"""
|
||||
Endpoint to process an input with a given flow_id.
|
||||
|
|
@ -125,10 +109,55 @@ async def process(
|
|||
graph_data = process_tweaks(graph_data, tweaks)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error processing tweaks: {exc}")
|
||||
response, session_id = process_graph_cached(
|
||||
graph_data, inputs, clear_cache, session_id
|
||||
if sync:
|
||||
task_id, result = await task_service.launch_and_await_task(
|
||||
process_graph_cached_task
|
||||
if task_service.use_celery
|
||||
else process_graph_cached,
|
||||
graph_data,
|
||||
inputs,
|
||||
clear_cache,
|
||||
session_id,
|
||||
)
|
||||
if isinstance(result, dict) and "result" in result:
|
||||
task_result = result["result"]
|
||||
session_id = result["session_id"]
|
||||
elif hasattr(result, "result") and hasattr(result, "session_id"):
|
||||
task_result = result.result
|
||||
|
||||
session_id = result.session_id
|
||||
else:
|
||||
logger.warning(
|
||||
"This is an experimental feature and may not work as expected."
|
||||
"Please report any issues to our GitHub repository."
|
||||
)
|
||||
if session_id is None:
|
||||
# Generate a session ID
|
||||
session_id = get_session_service().generate_key(
|
||||
session_id=session_id, data_graph=graph_data
|
||||
)
|
||||
task_id, task = await task_service.launch_task(
|
||||
process_graph_cached_task
|
||||
if task_service.use_celery
|
||||
else process_graph_cached,
|
||||
graph_data,
|
||||
inputs,
|
||||
clear_cache,
|
||||
session_id,
|
||||
)
|
||||
task_result = task.status
|
||||
|
||||
if task_id:
|
||||
task_response = TaskResponse(id=task_id, href=f"api/v1/task/{task_id}")
|
||||
else:
|
||||
task_response = None
|
||||
|
||||
return ProcessResponse(
|
||||
result=task_result,
|
||||
task=task_response,
|
||||
session_id=session_id,
|
||||
backend=task_service.backend_name,
|
||||
)
|
||||
return ProcessResponse(result=response, session_id=session_id)
|
||||
except sa.exc.StatementError as exc:
|
||||
# StatementError('(builtins.ValueError) badly formed hexadecimal UUID string')
|
||||
if "badly formed hexadecimal UUID string" in str(exc):
|
||||
|
|
@ -151,6 +180,23 @@ async def process(
|
|||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/task/{task_id}", response_model=TaskStatusResponse)
|
||||
async def get_task_status(task_id: str):
|
||||
task_service = get_task_service()
|
||||
task = task_service.get_task(task_id)
|
||||
result = None
|
||||
if task.ready():
|
||||
result = task.result
|
||||
if isinstance(result, dict) and "result" in result:
|
||||
result = result["result"]
|
||||
elif hasattr(result, "result"):
|
||||
result = result.result
|
||||
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return TaskStatusResponse(status=task.status, result=result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/upload/{flow_id}",
|
||||
response_model=UploadFileResponse,
|
||||
|
|
@ -159,7 +205,7 @@ async def process(
|
|||
async def create_upload_file(file: UploadFile, flow_id: str):
|
||||
# Cache file
|
||||
try:
|
||||
file_path = save_uploaded_file(file.file, folder_name=flow_id)
|
||||
file_path = save_uploaded_file(file, folder_name=flow_id)
|
||||
|
||||
return UploadFileResponse(
|
||||
flowId=flow_id,
|
||||
|
|
@ -182,6 +228,10 @@ def get_version():
|
|||
async def custom_component(
|
||||
raw_code: CustomComponentCode,
|
||||
):
|
||||
from langflow.interface.types import (
|
||||
build_langchain_template_custom_component,
|
||||
)
|
||||
|
||||
extractor = CustomComponent(code=raw_code.code)
|
||||
extractor.is_check_valid()
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ from langflow.services.database.models.flow import (
|
|||
FlowUpdate,
|
||||
)
|
||||
from langflow.services.database.models.user.user import User
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_session
|
||||
from langflow.services.getters import get_settings_service
|
||||
import orjson
|
||||
from sqlmodel import Session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
|
@ -83,7 +83,7 @@ def update_flow(
|
|||
flow_id: UUID,
|
||||
flow: FlowUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_manager=Depends(get_settings_manager),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Update a flow."""
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ def update_flow(
|
|||
if not db_flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
flow_data = flow.dict(exclude_unset=True)
|
||||
if settings_manager.settings.REMOVE_API_KEYS:
|
||||
if settings_service.settings.REMOVE_API_KEYS:
|
||||
flow_data = remove_api_keys(flow_data)
|
||||
for key, value in flow_data.items():
|
||||
if value is not None:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from sqlmodel import Session
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.getters import get_session
|
||||
from langflow.api.v1.schemas import Token
|
||||
from langflow.services.auth.utils import (
|
||||
authenticate_user,
|
||||
|
|
@ -12,7 +12,7 @@ from langflow.services.auth.utils import (
|
|||
get_current_active_user,
|
||||
)
|
||||
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
router = APIRouter(tags=["Login"])
|
||||
|
||||
|
|
@ -23,7 +23,17 @@ async def login_to_get_access_token(
|
|||
db: Session = Depends(get_session),
|
||||
# _: Session = Depends(get_current_active_user)
|
||||
):
|
||||
if user := authenticate_user(form_data.username, form_data.password, db):
|
||||
try:
|
||||
user = authenticate_user(form_data.username, form_data.password, db)
|
||||
except Exception as exc:
|
||||
if isinstance(exc, HTTPException):
|
||||
raise exc
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
if user:
|
||||
return create_user_tokens(user_id=user.id, db=db, update_last_login=True)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
@ -35,9 +45,9 @@ async def login_to_get_access_token(
|
|||
|
||||
@router.get("/auto_login")
|
||||
async def auto_login(
|
||||
db: Session = Depends(get_session), settings_manager=Depends(get_settings_manager)
|
||||
db: Session = Depends(get_session), settings_service=Depends(get_settings_service)
|
||||
):
|
||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||
if settings_service.auth_settings.AUTO_LOGIN:
|
||||
return create_user_longterm_token(db)
|
||||
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -47,11 +47,30 @@ class UpdateTemplateRequest(BaseModel):
|
|||
template: dict
|
||||
|
||||
|
||||
class TaskResponse(BaseModel):
|
||||
"""Task response schema."""
|
||||
|
||||
id: Optional[str] = Field(None)
|
||||
href: Optional[str] = Field(None)
|
||||
|
||||
|
||||
class ProcessResponse(BaseModel):
|
||||
"""Process response schema."""
|
||||
|
||||
result: dict
|
||||
result: Any
|
||||
task: Optional[TaskResponse] = None
|
||||
session_id: Optional[str] = None
|
||||
backend: Optional[str] = None
|
||||
|
||||
|
||||
# TaskStatusResponse(
|
||||
# status=task.status, result=task.result if task.ready() else None
|
||||
# )
|
||||
class TaskStatusResponse(BaseModel):
|
||||
"""Task status response schema."""
|
||||
|
||||
status: str
|
||||
result: Optional[Any] = None
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
|
|
@ -59,6 +78,7 @@ class ChatMessage(BaseModel):
|
|||
|
||||
is_bot: bool = False
|
||||
message: Union[str, None, dict] = None
|
||||
chatKey: Optional[str] = None
|
||||
type: str = "human"
|
||||
|
||||
|
||||
|
|
@ -66,6 +86,7 @@ class ChatResponse(ChatMessage):
|
|||
"""Chat response schema."""
|
||||
|
||||
intermediate_steps: str
|
||||
|
||||
type: str
|
||||
is_bot: bool = True
|
||||
files: list = []
|
||||
|
|
@ -77,6 +98,14 @@ class ChatResponse(ChatMessage):
|
|||
return v
|
||||
|
||||
|
||||
class PromptResponse(ChatMessage):
|
||||
"""Prompt response schema."""
|
||||
|
||||
prompt: str
|
||||
type: str = "prompt"
|
||||
is_bot: bool = True
|
||||
|
||||
|
||||
class FileResponse(ChatMessage):
|
||||
"""File response schema."""
|
||||
|
||||
|
|
|
|||
|
|
@ -13,23 +13,26 @@ from sqlalchemy.exc import IntegrityError
|
|||
from sqlmodel import Session, select
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.getters import get_session, get_settings_service
|
||||
from langflow.services.auth.utils import (
|
||||
get_current_active_superuser,
|
||||
get_current_active_user,
|
||||
get_password_hash,
|
||||
verify_password,
|
||||
)
|
||||
from langflow.services.database.models.user.crud import (
|
||||
get_user_by_id,
|
||||
update_user,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["Users"])
|
||||
router = APIRouter(tags=["Users"], prefix="/users")
|
||||
|
||||
|
||||
@router.post("/user", response_model=UserRead, status_code=201)
|
||||
@router.post("/", response_model=UserRead, status_code=201)
|
||||
def add_user(
|
||||
user: UserCreate,
|
||||
session: Session = Depends(get_session),
|
||||
settings_service=Depends(get_settings_service),
|
||||
) -> User:
|
||||
"""
|
||||
Add a new user to the database.
|
||||
|
|
@ -37,7 +40,7 @@ def add_user(
|
|||
new_user = User.from_orm(user)
|
||||
try:
|
||||
new_user.password = get_password_hash(user.password)
|
||||
|
||||
new_user.is_active = settings_service.auth_settings.NEW_USER_IS_ACTIVE
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
|
|
@ -50,7 +53,7 @@ def add_user(
|
|||
return new_user
|
||||
|
||||
|
||||
@router.get("/user", response_model=UserRead)
|
||||
@router.get("/whoami", response_model=UserRead)
|
||||
def read_current_user(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
) -> User:
|
||||
|
|
@ -60,11 +63,11 @@ def read_current_user(
|
|||
return current_user
|
||||
|
||||
|
||||
@router.get("/users", response_model=UsersResponse)
|
||||
@router.get("/", response_model=UsersResponse)
|
||||
def read_all_users(
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
current_user: Session = Depends(get_current_active_superuser),
|
||||
_: Session = Depends(get_current_active_superuser),
|
||||
session: Session = Depends(get_session),
|
||||
) -> UsersResponse:
|
||||
"""
|
||||
|
|
@ -82,20 +85,63 @@ def read_all_users(
|
|||
)
|
||||
|
||||
|
||||
@router.patch("/user/{user_id}", response_model=UserRead)
|
||||
@router.patch("/{user_id}", response_model=UserRead)
|
||||
def patch_user(
|
||||
user_id: UUID,
|
||||
user: UserUpdate,
|
||||
_: Session = Depends(get_current_active_user),
|
||||
user_update: UserUpdate,
|
||||
user: User = Depends(get_current_active_user),
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Update an existing user's data.
|
||||
"""
|
||||
return update_user(user_id, user, session)
|
||||
if not user.is_superuser and user.id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="You don't have the permission to update this user"
|
||||
)
|
||||
if user_update.password:
|
||||
if not user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't change your password here"
|
||||
)
|
||||
user_update.password = get_password_hash(user_update.password)
|
||||
|
||||
if user_db := get_user_by_id(session, user_id):
|
||||
return update_user(user_db, user_update, session)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
|
||||
@router.delete("/user/{user_id}")
|
||||
@router.patch("/{user_id}/reset-password", response_model=UserRead)
|
||||
def reset_password(
|
||||
user_id: UUID,
|
||||
user_update: UserUpdate,
|
||||
user: User = Depends(get_current_active_user),
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Reset a user's password.
|
||||
"""
|
||||
if user_id != user.id:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't change another user's password"
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if verify_password(user_update.password, user.password):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="You can't use your current password"
|
||||
)
|
||||
new_password = get_password_hash(user_update.password)
|
||||
user.password = new_password
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
@router.delete("/{user_id}", response_model=dict)
|
||||
def delete_user(
|
||||
user_id: UUID,
|
||||
current_user: User = Depends(get_current_active_superuser),
|
||||
|
|
@ -121,31 +167,3 @@ def delete_user(
|
|||
session.commit()
|
||||
|
||||
return {"detail": "User deleted"}
|
||||
|
||||
|
||||
# TODO: REMOVE - Just for testing purposes
|
||||
@router.post("/super_user", response_model=User)
|
||||
def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
|
||||
session: Session = Depends(get_session),
|
||||
) -> User:
|
||||
"""
|
||||
Add a superuser for testing purposes.
|
||||
(This should be removed in production)
|
||||
"""
|
||||
new_user = User(
|
||||
username="superuser",
|
||||
password=get_password_hash("12345"),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
last_login_at=None,
|
||||
)
|
||||
|
||||
try:
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
except IntegrityError as e:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=400, detail="User exists") from e
|
||||
|
||||
return new_user
|
||||
|
|
|
|||
|
|
@ -58,6 +58,16 @@ def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
|||
|
||||
def get_old_custom_fields(prompt_request):
|
||||
try:
|
||||
if (
|
||||
len(prompt_request.frontend_node.custom_fields) == 1
|
||||
and prompt_request.name == ""
|
||||
):
|
||||
# If there is only one custom field and the name is empty string
|
||||
# then we are dealing with the first prompt request after the node was created
|
||||
prompt_request.name = list(
|
||||
prompt_request.frontend_node.custom_fields.keys()
|
||||
)[0]
|
||||
|
||||
old_custom_fields = prompt_request.frontend_node.custom_fields[
|
||||
prompt_request.name
|
||||
].copy()
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ class ConversationalAgent(CustomComponent):
|
|||
self,
|
||||
model_name: str,
|
||||
openai_api_key: str,
|
||||
openai_api_base: str,
|
||||
tools: Tool,
|
||||
openai_api_base: Optional[str] = None,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
system_message: Optional[SystemMessagePromptTemplate] = None,
|
||||
max_token_limit: int = 2000,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from langflow import CustomComponent
|
||||
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain import PromptTemplate
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.schema import Document
|
||||
|
||||
|
||||
|
|
@ -16,17 +16,14 @@ class PromptRunner(CustomComponent):
|
|||
"info": "Make sure the prompt has all variables filled.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
"inputs": {"field_type": "code"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
llm: BaseLLM,
|
||||
prompt: PromptTemplate,
|
||||
self, llm: BaseLLM, prompt: PromptTemplate, inputs: dict = {}
|
||||
) -> Document:
|
||||
chain = prompt | llm
|
||||
# The input is an empty dict because the prompt is already filled
|
||||
result = chain.invoke({})
|
||||
result = chain.invoke(input=inputs)
|
||||
if hasattr(result, "content"):
|
||||
result = result.content
|
||||
self.repr_value = result
|
||||
|
|
|
|||
42
src/backend/langflow/components/llms/HuggingFaceEndpoints.py
Normal file
42
src/backend/langflow/components/llms/HuggingFaceEndpoints.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.llms import HuggingFaceEndpoint
|
||||
from langchain.llms.base import BaseLLM
|
||||
|
||||
|
||||
class HuggingFaceEndpointsComponent(CustomComponent):
|
||||
display_name: str = "Hugging Face Inference API"
|
||||
description: str = "LLM model from Hugging Face Inference API."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"endpoint_url": {"display_name": "Endpoint URL", "password": True},
|
||||
"task": {
|
||||
"display_name": "Task",
|
||||
"type": "select",
|
||||
"options": ["text2text-generation", "text-generation", "summarization"],
|
||||
},
|
||||
"huggingfacehub_api_token": {"display_name": "API token", "password": True},
|
||||
"model_kwargs": {
|
||||
"display_name": "Model Keyword Arguments",
|
||||
"field_type": "code",
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
endpoint_url: str,
|
||||
task="text2text-generation",
|
||||
huggingfacehub_api_token: Optional[str] = None,
|
||||
model_kwargs: Optional[dict] = None,
|
||||
) -> BaseLLM:
|
||||
try:
|
||||
output = HuggingFaceEndpoint(
|
||||
endpoint_url=endpoint_url,
|
||||
task=task,
|
||||
huggingfacehub_api_token=huggingfacehub_api_token,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError("Could not connect to HuggingFace Endpoints API.") from e
|
||||
return output
|
||||
0
src/backend/langflow/components/llms/__init__.py
Normal file
0
src/backend/langflow/components/llms/__init__.py
Normal file
28
src/backend/langflow/components/retrievers/MetalRetriever.py
Normal file
28
src/backend/langflow/components/retrievers/MetalRetriever.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.retrievers import MetalRetriever
|
||||
from langchain.schema import BaseRetriever
|
||||
from metal_sdk.metal import Metal # type: ignore
|
||||
|
||||
|
||||
class MetalRetrieverComponent(CustomComponent):
|
||||
display_name: str = "Metal Retriever"
|
||||
description: str = "Retriever that uses the Metal API."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"api_key": {"display_name": "API Key", "password": True},
|
||||
"client_id": {"display_name": "Client ID", "password": True},
|
||||
"index_id": {"display_name": "Index ID"},
|
||||
"params": {"display_name": "Parameters"},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self, api_key: str, client_id: str, index_id: str, params: Optional[dict] = None
|
||||
) -> BaseRetriever:
|
||||
try:
|
||||
metal = Metal(api_key=api_key, client_id=client_id, index_id=index_id)
|
||||
except Exception as e:
|
||||
raise ValueError("Could not connect to Metal API.") from e
|
||||
return MetalRetriever(client=metal, params=params or {})
|
||||
0
src/backend/langflow/components/retrievers/__init__.py
Normal file
0
src/backend/langflow/components/retrievers/__init__.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.text_splitter import Language
|
||||
from langchain.schema import Document
|
||||
|
||||
|
||||
class LanguageRecursiveTextSplitterComponent(CustomComponent):
|
||||
display_name: str = "Language Recursive Text Splitter"
|
||||
description: str = "Split text into chunks of a specified length based on language."
|
||||
documentation: str = "https://docs.langflow.org/components/text-splitters#languagerecursivetextsplitter"
|
||||
|
||||
def build_config(self):
|
||||
options = [x.value for x in Language]
|
||||
return {
|
||||
"documents": {
|
||||
"display_name": "Documents",
|
||||
"info": "The documents to split.",
|
||||
},
|
||||
"separator_type": {
|
||||
"display_name": "Separator Type",
|
||||
"info": "The type of separator to use.",
|
||||
"field_type": "str",
|
||||
"options": options,
|
||||
"value": "Python",
|
||||
},
|
||||
"separators": {
|
||||
"display_name": "Separators",
|
||||
"info": "The characters to split on.",
|
||||
"is_list": True,
|
||||
},
|
||||
"chunk_size": {
|
||||
"display_name": "Chunk Size",
|
||||
"info": "The maximum length of each chunk.",
|
||||
"field_type": "int",
|
||||
"value": 1000,
|
||||
},
|
||||
"chunk_overlap": {
|
||||
"display_name": "Chunk Overlap",
|
||||
"info": "The amount of overlap between chunks.",
|
||||
"field_type": "int",
|
||||
"value": 200,
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
documents: list[Document],
|
||||
chunk_size: Optional[int] = 1000,
|
||||
chunk_overlap: Optional[int] = 200,
|
||||
separator_type: Optional[str] = "Python",
|
||||
) -> list[Document]:
|
||||
"""
|
||||
Split text into chunks of a specified length.
|
||||
|
||||
Args:
|
||||
separators (list[str]): The characters to split on.
|
||||
chunk_size (int): The maximum length of each chunk.
|
||||
chunk_overlap (int): The amount of overlap between chunks.
|
||||
length_function (function): The function to use to calculate the length of the text.
|
||||
|
||||
Returns:
|
||||
list[str]: The chunks of text.
|
||||
"""
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
# Make sure chunk_size and chunk_overlap are ints
|
||||
if isinstance(chunk_size, str):
|
||||
chunk_size = int(chunk_size)
|
||||
if isinstance(chunk_overlap, str):
|
||||
chunk_overlap = int(chunk_overlap)
|
||||
|
||||
splitter = RecursiveCharacterTextSplitter.from_language(
|
||||
language=Language(separator_type),
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
)
|
||||
|
||||
docs = splitter.split_documents(documents)
|
||||
return docs
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
from typing import Optional
|
||||
from langflow import CustomComponent
|
||||
from langchain.schema import Document
|
||||
from langflow.utils.util import build_loader_repr_from_documents
|
||||
|
||||
|
||||
class RecursiveCharacterTextSplitterComponent(CustomComponent):
|
||||
display_name: str = "Recursive Character Text Splitter"
|
||||
description: str = "Split text into chunks of a specified length."
|
||||
documentation: str = "https://docs.langflow.org/components/text-splitters#recursivecharactertextsplitter"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"documents": {
|
||||
"display_name": "Documents",
|
||||
"info": "The documents to split.",
|
||||
},
|
||||
"separators": {
|
||||
"display_name": "Separators",
|
||||
"info": 'The characters to split on.\nIf left empty defaults to ["\\n\\n", "\\n", " ", ""].',
|
||||
"is_list": True,
|
||||
},
|
||||
"chunk_size": {
|
||||
"display_name": "Chunk Size",
|
||||
"info": "The maximum length of each chunk.",
|
||||
"field_type": "int",
|
||||
"value": 1000,
|
||||
},
|
||||
"chunk_overlap": {
|
||||
"display_name": "Chunk Overlap",
|
||||
"info": "The amount of overlap between chunks.",
|
||||
"field_type": "int",
|
||||
"value": 200,
|
||||
},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
documents: list[Document],
|
||||
separators: Optional[list[str]] = None,
|
||||
chunk_size: Optional[int] = 1000,
|
||||
chunk_overlap: Optional[int] = 200,
|
||||
) -> list[Document]:
|
||||
"""
|
||||
Split text into chunks of a specified length.
|
||||
|
||||
Args:
|
||||
separators (list[str]): The characters to split on.
|
||||
chunk_size (int): The maximum length of each chunk.
|
||||
chunk_overlap (int): The amount of overlap between chunks.
|
||||
length_function (function): The function to use to calculate the length of the text.
|
||||
|
||||
Returns:
|
||||
list[str]: The chunks of text.
|
||||
"""
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
if separators == "":
|
||||
separators = None
|
||||
elif separators:
|
||||
# check if the separators list has escaped characters
|
||||
# if there are escaped characters, unescape them
|
||||
separators = [x.encode().decode("unicode-escape") for x in separators]
|
||||
|
||||
# Make sure chunk_size and chunk_overlap are ints
|
||||
if isinstance(chunk_size, str):
|
||||
chunk_size = int(chunk_size)
|
||||
if isinstance(chunk_overlap, str):
|
||||
chunk_overlap = int(chunk_overlap)
|
||||
splitter = RecursiveCharacterTextSplitter(
|
||||
separators=separators,
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
)
|
||||
|
||||
docs = splitter.split_documents(documents)
|
||||
self.repr_value = build_loader_repr_from_documents(docs)
|
||||
return docs
|
||||
|
|
@ -19,7 +19,6 @@ class GetRequest(CustomComponent):
|
|||
},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"field_type": "code",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class PostRequest(CustomComponent):
|
|||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"field_type": "code",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class UpdateRequest(CustomComponent):
|
|||
"url": {"display_name": "URL", "info": "The URL to make the request to."},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"field_type": "code",
|
||||
"field_type": "NestedDict",
|
||||
"info": "The headers to send with the request.",
|
||||
},
|
||||
"code": {"show": False},
|
||||
|
|
|
|||
|
|
@ -171,8 +171,6 @@ prompts:
|
|||
textsplitters:
|
||||
CharacterTextSplitter:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/character_text_splitter"
|
||||
RecursiveCharacterTextSplitter:
|
||||
documentation: "https://python.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/recursive_text_splitter"
|
||||
toolkits:
|
||||
OpenAPIToolkit:
|
||||
documentation: ""
|
||||
|
|
|
|||
0
src/backend/langflow/core/__init__.py
Normal file
0
src/backend/langflow/core/__init__.py
Normal file
11
src/backend/langflow/core/celery_app.py
Normal file
11
src/backend/langflow/core/celery_app.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from celery import Celery # type: ignore
|
||||
|
||||
|
||||
def make_celery(app_name: str, config: str) -> Celery:
|
||||
celery_app = Celery(app_name)
|
||||
celery_app.config_from_object(config)
|
||||
celery_app.conf.task_routes = {"langflow.worker.tasks.*": {"queue": "langflow"}}
|
||||
return celery_app
|
||||
|
||||
|
||||
celery_app = make_celery("langflow", "langflow.core.celeryconfig")
|
||||
14
src/backend/langflow/core/celeryconfig.py
Normal file
14
src/backend/langflow/core/celeryconfig.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# celeryconfig.py
|
||||
import os
|
||||
|
||||
langflow_redis_host = os.environ.get("LANGFLOW_REDIS_HOST")
|
||||
langflow_redis_port = os.environ.get("LANGFLOW_REDIS_PORT")
|
||||
if "BROKER_URL" in os.environ and "RESULT_BACKEND" in os.environ:
|
||||
# RabbitMQ
|
||||
broker_url = os.environ.get("BROKER_URL", "amqp://localhost")
|
||||
result_backend = os.environ.get("RESULT_BACKEND", "redis://localhost:6379/0")
|
||||
elif langflow_redis_host and langflow_redis_port:
|
||||
broker_url = f"redis://{langflow_redis_host}:{langflow_redis_port}/0"
|
||||
result_backend = f"redis://{langflow_redis_host}:{langflow_redis_port}/0"
|
||||
# tasks should be json or pickle
|
||||
accept_content = ["json", "pickle"]
|
||||
3
src/backend/langflow/field_typing/__init__.py
Normal file
3
src/backend/langflow/field_typing/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .base import NestedDict
|
||||
|
||||
__all__ = ["NestedDict"]
|
||||
4
src/backend/langflow/field_typing/base.py
Normal file
4
src/backend/langflow/field_typing/base.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from typing import Union, Dict
|
||||
|
||||
# Type alias for more complex dicts
|
||||
NestedDict = Dict[str, Union[str, Dict]]
|
||||
|
|
@ -68,6 +68,17 @@ class Edge:
|
|||
f"has invalid handles"
|
||||
)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.source = state["source"]
|
||||
self.target = state["target"]
|
||||
self.target_param = state["target_param"]
|
||||
self.source_handle = state["source_handle"]
|
||||
self.target_handle = state["target_handle"]
|
||||
|
||||
def reset(self) -> None:
|
||||
self.source._build_params()
|
||||
self.target._build_params()
|
||||
|
||||
def validate_edge(self) -> None:
|
||||
# Validate that the outputs of the source node are valid inputs
|
||||
# for the target node
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ class Graph:
|
|||
self._edges = self._graph_data["edges"]
|
||||
self._build_graph()
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
for edge in self.edges:
|
||||
edge.reset()
|
||||
edge.validate_edge()
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: Dict) -> "Graph":
|
||||
"""
|
||||
|
|
@ -55,6 +61,11 @@ class Graph:
|
|||
f"Invalid payload. Expected keys 'nodes' and 'edges'. Found {list(payload.keys())}"
|
||||
) from exc
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Graph):
|
||||
return False
|
||||
return self.__repr__() == other.__repr__()
|
||||
|
||||
def _build_graph(self) -> None:
|
||||
"""Builds the graph from the nodes and edges."""
|
||||
self.nodes = self._build_vertices()
|
||||
|
|
@ -154,7 +165,7 @@ class Graph:
|
|||
def generator_build(self) -> Generator[Vertex, None, None]:
|
||||
"""Builds each vertex in the graph and yields it."""
|
||||
sorted_vertices = self.topological_sort()
|
||||
logger.debug("Sorted vertices: %s", sorted_vertices)
|
||||
logger.debug("There are %s vertices in the graph", len(sorted_vertices))
|
||||
yield from sorted_vertices
|
||||
|
||||
def get_node_neighbors(self, node: Vertex) -> Dict[Vertex, int]:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import ast
|
||||
import pickle
|
||||
from langflow.graph.utils import UnbuiltObject
|
||||
from langflow.graph.vertex.utils import is_basic_type
|
||||
from langflow.interface.initialize import loading
|
||||
from langflow.interface.listing import lazy_load_dict
|
||||
from langflow.utils.constants import DIRECT_TYPES
|
||||
|
|
@ -12,12 +14,19 @@ import types
|
|||
from typing import Any, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.edge.base import Edge
|
||||
|
||||
|
||||
class Vertex:
|
||||
def __init__(self, data: Dict, base_type: Optional[str] = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
data: Dict,
|
||||
base_type: Optional[str] = None,
|
||||
is_task: bool = False,
|
||||
params: Optional[Dict] = None,
|
||||
) -> None:
|
||||
self.id: str = data["id"]
|
||||
self._data = data
|
||||
self.edges: List["Edge"] = []
|
||||
|
|
@ -26,6 +35,59 @@ class Vertex:
|
|||
self._built_object = UnbuiltObject()
|
||||
self._built = False
|
||||
self.artifacts: Dict[str, Any] = {}
|
||||
self.task_id: Optional[str] = None
|
||||
self.is_task = is_task
|
||||
self.params = params or {}
|
||||
|
||||
def reset_params(self):
|
||||
for edge in self.edges:
|
||||
if edge.source != self:
|
||||
target_param = edge.target_param
|
||||
if target_param in ["document", "texts"]:
|
||||
# this means they got data and have already ingested it
|
||||
# so we continue after removing the param
|
||||
self.params.pop(target_param, None)
|
||||
continue
|
||||
|
||||
if target_param in self.params and not is_basic_type(
|
||||
self.params[target_param]
|
||||
):
|
||||
# edge.source.params = {}
|
||||
edge.source._build_params()
|
||||
edge.source._built_object = UnbuiltObject()
|
||||
edge.source._built = False
|
||||
|
||||
self.params[target_param] = edge.source
|
||||
|
||||
def __getstate__(self):
|
||||
state_dict = self.__dict__.copy()
|
||||
try:
|
||||
# try pickling the built object
|
||||
# if it fails, then we need to delete it
|
||||
# and build it again
|
||||
pickle.dumps(state_dict["_built_object"])
|
||||
except Exception:
|
||||
self.reset_params()
|
||||
del state_dict["_built_object"]
|
||||
del state_dict["_built"]
|
||||
return state_dict
|
||||
|
||||
def __setstate__(self, state):
|
||||
self._data = state["_data"]
|
||||
self.params = state["params"]
|
||||
self.base_type = state["base_type"]
|
||||
self.is_task = state["is_task"]
|
||||
self.edges = state["edges"]
|
||||
self.id = state["id"]
|
||||
self._parse_data()
|
||||
if "_built_object" in state:
|
||||
self._built_object = state["_built_object"]
|
||||
self._built = state["_built"]
|
||||
else:
|
||||
self._built_object = UnbuiltObject()
|
||||
self._built = False
|
||||
self.artifacts: Dict[str, Any] = {}
|
||||
self.task_id: Optional[str] = None
|
||||
self.parent_node_id: Optional[str] = self._data.get("parent_node_id")
|
||||
self.parent_is_top_level = False
|
||||
|
||||
|
|
@ -73,6 +135,13 @@ class Vertex:
|
|||
self.base_type = base_type
|
||||
break
|
||||
|
||||
def get_task(self):
|
||||
# using the task_id, get the task from celery
|
||||
# and return it
|
||||
from celery.result import AsyncResult # type: ignore
|
||||
|
||||
return AsyncResult(self.task_id)
|
||||
|
||||
def _build_params(self):
|
||||
# sourcery skip: merge-list-append, remove-redundant-if
|
||||
# Some params are required, some are optional
|
||||
|
|
@ -94,9 +163,11 @@ class Vertex:
|
|||
for key, value in self.data["node"]["template"].items()
|
||||
if isinstance(value, dict)
|
||||
}
|
||||
params = {}
|
||||
params = self.params.copy() if self.params else {}
|
||||
|
||||
for edge in self.edges:
|
||||
if not hasattr(edge, "target_param"):
|
||||
continue
|
||||
param_key = edge.target_param
|
||||
if param_key in template_dict:
|
||||
if template_dict[param_key]["list"]:
|
||||
|
|
@ -107,6 +178,8 @@ class Vertex:
|
|||
params[param_key] = edge.source
|
||||
|
||||
for key, value in template_dict.items():
|
||||
if key in params:
|
||||
continue
|
||||
# Skip _type and any value that has show == False and is not code
|
||||
# If we don't want to show code but we want to use it
|
||||
if key == "_type" or (not value.get("show") and key != "code"):
|
||||
|
|
@ -117,9 +190,10 @@ class Vertex:
|
|||
# Load the type in value.get('suffixes') using
|
||||
# what is inside value.get('content')
|
||||
# value.get('value') is the file name
|
||||
file_path = value.get("file_path")
|
||||
|
||||
params[key] = file_path
|
||||
if file_path := value.get("file_path"):
|
||||
params[key] = file_path
|
||||
else:
|
||||
raise ValueError(f"File path not found for {self.vertex_type}")
|
||||
elif value.get("type") in DIRECT_TYPES and params.get(key) is None:
|
||||
if value.get("type") == "code":
|
||||
try:
|
||||
|
|
@ -127,6 +201,19 @@ class Vertex:
|
|||
except Exception as exc:
|
||||
logger.debug(f"Error parsing code: {exc}")
|
||||
params[key] = value.get("value")
|
||||
elif value.get("type") in ["dict", "NestedDict"]:
|
||||
# When dict comes from the frontend it comes as a
|
||||
# list of dicts, so we need to convert it to a dict
|
||||
# before passing it to the build method
|
||||
_value = value.get("value")
|
||||
if isinstance(_value, list):
|
||||
params[key] = {
|
||||
k: v
|
||||
for item in value.get("value", [])
|
||||
for k, v in item.items()
|
||||
}
|
||||
elif isinstance(_value, dict):
|
||||
params[key] = _value
|
||||
else:
|
||||
params[key] = value.get("value")
|
||||
|
||||
|
|
@ -136,6 +223,7 @@ class Vertex:
|
|||
else:
|
||||
params.pop(key, None)
|
||||
# Add _type to params
|
||||
self._raw_params = params
|
||||
self.params = params
|
||||
|
||||
def _build(self, user_id=None):
|
||||
|
|
@ -143,13 +231,13 @@ class Vertex:
|
|||
Initiate the build process.
|
||||
"""
|
||||
logger.debug(f"Building {self.vertex_type}")
|
||||
self._build_each_node_in_params_dict()
|
||||
self._build_each_node_in_params_dict(user_id)
|
||||
self._get_and_instantiate_class(user_id)
|
||||
self._validate_built_object()
|
||||
|
||||
self._built = True
|
||||
|
||||
def _build_each_node_in_params_dict(self):
|
||||
def _build_each_node_in_params_dict(self, user_id=None):
|
||||
"""
|
||||
Iterates over each node in the params dictionary and builds it.
|
||||
"""
|
||||
|
|
@ -158,9 +246,9 @@ class Vertex:
|
|||
if value == self:
|
||||
del self.params[key]
|
||||
continue
|
||||
self._build_node_and_update_params(key, value)
|
||||
self._build_node_and_update_params(key, value, user_id)
|
||||
elif isinstance(value, list) and self._is_list_of_nodes(value):
|
||||
self._build_list_of_nodes_and_update_params(key, value)
|
||||
self._build_list_of_nodes_and_update_params(key, value, user_id)
|
||||
|
||||
def _is_node(self, value):
|
||||
"""
|
||||
|
|
@ -174,11 +262,31 @@ class Vertex:
|
|||
"""
|
||||
return all(self._is_node(node) for node in value)
|
||||
|
||||
def get_result(self, user_id=None, timeout=None) -> Any:
|
||||
# Check if the Vertex was built already
|
||||
if self._built:
|
||||
return self._built_object
|
||||
|
||||
if self.is_task and self.task_id is not None:
|
||||
task = self.get_task()
|
||||
result = task.get(timeout=timeout)
|
||||
if result is not None: # If result is ready
|
||||
self._update_built_object_and_artifacts(result)
|
||||
return self._built_object
|
||||
else:
|
||||
# Handle the case when the result is not ready (retry, throw exception, etc.)
|
||||
pass
|
||||
|
||||
# If there's no task_id, build the vertex locally
|
||||
self.build(user_id)
|
||||
return self._built_object
|
||||
|
||||
def _build_node_and_update_params(self, key, node, user_id=None):
|
||||
"""
|
||||
Builds a given node and updates the params dictionary accordingly.
|
||||
"""
|
||||
result = node.build(user_id)
|
||||
|
||||
result = node.get_result(user_id)
|
||||
self._handle_func(key, result)
|
||||
if isinstance(result, list):
|
||||
self._extend_params_list_with_result(key, result)
|
||||
|
|
@ -192,7 +300,7 @@ class Vertex:
|
|||
"""
|
||||
self.params[key] = []
|
||||
for node in nodes:
|
||||
built = node.build(user_id)
|
||||
built = node.get_result(user_id)
|
||||
if isinstance(built, list):
|
||||
if key not in self.params:
|
||||
self.params[key] = []
|
||||
|
|
@ -237,6 +345,7 @@ class Vertex:
|
|||
)
|
||||
self._update_built_object_and_artifacts(result)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise ValueError(
|
||||
f"Error building node {self.vertex_type}: {str(exc)}"
|
||||
) from exc
|
||||
|
|
@ -277,7 +386,10 @@ class Vertex:
|
|||
return f"Vertex(id={self.id}, data={self.data})"
|
||||
|
||||
def __eq__(self, __o: object) -> bool:
|
||||
return self.id == __o.id if isinstance(__o, Vertex) else False
|
||||
try:
|
||||
return self.id == __o.id if isinstance(__o, Vertex) else False
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return id(self)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,27 @@ from langflow.interface.utils import extract_input_variables_from_prompt
|
|||
|
||||
|
||||
class AgentVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="agents")
|
||||
def __init__(self, data: Dict, params: Optional[Dict] = None):
|
||||
super().__init__(data, base_type="agents", params=params)
|
||||
|
||||
self.tools: List[Union[ToolkitVertex, ToolVertex]] = []
|
||||
self.chains: List[ChainVertex] = []
|
||||
|
||||
def __getstate__(self):
|
||||
state = super().__getstate__()
|
||||
state["tools"] = self.tools
|
||||
state["chains"] = self.chains
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.tools = state["tools"]
|
||||
self.chains = state["chains"]
|
||||
super().__setstate__(state)
|
||||
|
||||
def _set_tools_and_chains(self) -> None:
|
||||
for edge in self.edges:
|
||||
if not hasattr(edge, "source"):
|
||||
continue
|
||||
source_node = edge.source
|
||||
if isinstance(source_node, (ToolVertex, ToolkitVertex)):
|
||||
self.tools.append(source_node)
|
||||
|
|
@ -38,16 +51,16 @@ class AgentVertex(Vertex):
|
|||
|
||||
|
||||
class ToolVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="tools")
|
||||
def __init__(self, data: Dict, params: Optional[Dict] = None):
|
||||
super().__init__(data, base_type="tools", params=params)
|
||||
|
||||
|
||||
class LLMVertex(Vertex):
|
||||
built_node_type = None
|
||||
class_built_object = None
|
||||
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="llms")
|
||||
def __init__(self, data: Dict, params: Optional[Dict] = None):
|
||||
super().__init__(data, base_type="llms", params=params)
|
||||
|
||||
def build(self, force: bool = False, user_id=None, *args, **kwargs) -> Any:
|
||||
# LLM is different because some models might take up too much memory
|
||||
|
|
@ -64,13 +77,13 @@ class LLMVertex(Vertex):
|
|||
|
||||
|
||||
class ToolkitVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="toolkits")
|
||||
def __init__(self, data: Dict, params=None):
|
||||
super().__init__(data, base_type="toolkits", params=params)
|
||||
|
||||
|
||||
class FileToolVertex(ToolVertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data)
|
||||
def __init__(self, data: Dict, params=None):
|
||||
super().__init__(data, params=params)
|
||||
|
||||
|
||||
class WrapperVertex(Vertex):
|
||||
|
|
@ -86,17 +99,19 @@ class WrapperVertex(Vertex):
|
|||
|
||||
|
||||
class DocumentLoaderVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="documentloaders")
|
||||
def __init__(self, data: Dict, params: Optional[Dict] = None):
|
||||
super().__init__(data, base_type="documentloaders", params=params)
|
||||
|
||||
def _built_object_repr(self):
|
||||
# This built_object is a list of documents. Maybe we should
|
||||
# show how many documents are in the list?
|
||||
|
||||
if self._built_object:
|
||||
avg_length = sum(len(doc.page_content) for doc in self._built_object) / len(
|
||||
self._built_object
|
||||
)
|
||||
avg_length = sum(
|
||||
len(doc.page_content)
|
||||
for doc in self._built_object
|
||||
if hasattr(doc, "page_content")
|
||||
) / len(self._built_object)
|
||||
return f"""{self.vertex_type}({len(self._built_object)} documents)
|
||||
\nAvg. Document Length (characters): {int(avg_length)}
|
||||
Documents: {self._built_object[:3]}..."""
|
||||
|
|
@ -104,14 +119,51 @@ class DocumentLoaderVertex(Vertex):
|
|||
|
||||
|
||||
class EmbeddingVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="embeddings")
|
||||
def __init__(self, data: Dict, params: Optional[Dict] = None):
|
||||
super().__init__(data, base_type="embeddings", params=params)
|
||||
|
||||
|
||||
class VectorStoreVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
def __init__(self, data: Dict, params=None):
|
||||
super().__init__(data, base_type="vectorstores")
|
||||
|
||||
self.params = params or {}
|
||||
|
||||
# VectorStores may contain databse connections
|
||||
# so we need to define the __reduce__ method and the __setstate__ method
|
||||
# to avoid pickling errors
|
||||
def clean_edges_for_pickling(self):
|
||||
# for each edge that has self as source
|
||||
# we need to clear the _built_object of the target
|
||||
# so that we don't try to pickle a database connection
|
||||
for edge in self.edges:
|
||||
if edge.source == self:
|
||||
edge.target._built_object = None
|
||||
edge.target._built = False
|
||||
edge.target.params[edge.target_param] = self
|
||||
|
||||
def remove_docs_and_texts_from_params(self):
|
||||
# remove documents and texts from params
|
||||
# so that we don't try to pickle a database connection
|
||||
self.params.pop("documents", None)
|
||||
self.params.pop("texts", None)
|
||||
|
||||
def __getstate__(self):
|
||||
# We want to save the params attribute
|
||||
# and if "documents" or "texts" are in the params
|
||||
# we want to remove them because they have already
|
||||
# been processed.
|
||||
params = self.params.copy()
|
||||
params.pop("documents", None)
|
||||
params.pop("texts", None)
|
||||
self.clean_edges_for_pickling()
|
||||
|
||||
return super().__getstate__()
|
||||
|
||||
def __setstate__(self, state):
|
||||
super().__setstate__(state)
|
||||
self.remove_docs_and_texts_from_params()
|
||||
|
||||
|
||||
class MemoryVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
|
|
@ -124,8 +176,8 @@ class RetrieverVertex(Vertex):
|
|||
|
||||
|
||||
class TextSplitterVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="textsplitters")
|
||||
def __init__(self, data: Dict, params: Optional[Dict] = None):
|
||||
super().__init__(data, base_type="textsplitters", params=params)
|
||||
|
||||
def _built_object_repr(self):
|
||||
# This built_object is a list of documents. Maybe we should
|
||||
|
|
@ -211,7 +263,7 @@ class PromptVertex(Vertex):
|
|||
self.params["input_variables"] = list(
|
||||
set(self.params["input_variables"])
|
||||
)
|
||||
else:
|
||||
elif isinstance(self.params, dict):
|
||||
self.params.pop("input_variables", None)
|
||||
|
||||
self._build(user_id=user_id)
|
||||
|
|
@ -258,8 +310,13 @@ class OutputParserVertex(Vertex):
|
|||
|
||||
class CustomComponentVertex(Vertex):
|
||||
def __init__(self, data: Dict):
|
||||
super().__init__(data, base_type="custom_components")
|
||||
super().__init__(data, base_type="custom_components", is_task=True)
|
||||
|
||||
def _built_object_repr(self):
|
||||
if self.task_id and self.is_task:
|
||||
if task := self.get_task():
|
||||
return str(task.info)
|
||||
else:
|
||||
return f"Task {self.task_id} is not running"
|
||||
if self.artifacts and "repr" in self.artifacts:
|
||||
return self.artifacts["repr"] or super()._built_object_repr()
|
||||
|
|
|
|||
5
src/backend/langflow/graph/vertex/utils.py
Normal file
5
src/backend/langflow/graph/vertex/utils.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from langflow.utils.constants import PYTHON_BASIC_TYPES
|
||||
|
||||
|
||||
def is_basic_type(obj):
|
||||
return type(obj) in PYTHON_BASIC_TYPES
|
||||
|
|
@ -5,7 +5,7 @@ from langchain.agents import types
|
|||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.agents.custom import CUSTOM_AGENTS
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.agents import AgentFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -54,7 +54,7 @@ class AgentCreator(LangChainTypeCreator):
|
|||
# Now this is a generator
|
||||
def to_list(self) -> List[str]:
|
||||
names = []
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
for _, agent in self.type_to_loader_dict.items():
|
||||
agent_name = (
|
||||
agent.function_name()
|
||||
|
|
@ -62,8 +62,8 @@ class AgentCreator(LangChainTypeCreator):
|
|||
else agent.__name__
|
||||
)
|
||||
if (
|
||||
agent_name in settings_manager.settings.AGENTS
|
||||
or settings_manager.settings.DEV
|
||||
agent_name in settings_service.settings.AGENTS
|
||||
or settings_service.settings.DEV
|
||||
):
|
||||
names.append(agent_name)
|
||||
return names
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Any, List, Optional
|
||||
|
||||
from langchain import LLMChain
|
||||
from langchain.chains.llm import LLMChain
|
||||
from langchain.agents import (
|
||||
AgentExecutor,
|
||||
Tool,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from langchain import LLMChain
|
||||
from langchain.chains.llm import LLMChain
|
||||
from langchain.agents import AgentExecutor, ZeroShotAgent
|
||||
from langchain.agents.agent_toolkits.json.prompt import JSON_PREFIX, JSON_SUFFIX
|
||||
from langchain.agents.agent_toolkits.json.toolkit import JsonToolkit
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
|
|||
from typing import Any, Dict, List, Optional, Type, Union
|
||||
from langchain.chains.base import Chain
|
||||
from langchain.agents import AgentExecutor
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langflow.template.field.base import TemplateField
|
||||
|
|
@ -27,11 +27,11 @@ class LangChainTypeCreator(BaseModel, ABC):
|
|||
@property
|
||||
def docs_map(self) -> Dict[str, str]:
|
||||
"""A dict with the name of the component as key and the documentation link as value."""
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
if self.name_docs_dict is None:
|
||||
try:
|
||||
type_settings = getattr(
|
||||
settings_manager.settings, self.type_name.upper()
|
||||
settings_service.settings, self.type_name.upper()
|
||||
)
|
||||
self.name_docs_dict = {
|
||||
name: value_dict["documentation"]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Type
|
|||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.chains import ChainFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -31,7 +31,7 @@ class ChainCreator(LangChainTypeCreator):
|
|||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
self.type_dict: dict[str, Any] = {
|
||||
chain_name: import_class(f"langchain.chains.{chain_name}")
|
||||
for chain_name in chains.__all__
|
||||
|
|
@ -45,8 +45,8 @@ class ChainCreator(LangChainTypeCreator):
|
|||
self.type_dict = {
|
||||
name: chain
|
||||
for name, chain in self.type_dict.items()
|
||||
if name in settings_manager.settings.CHAINS
|
||||
or settings_manager.settings.DEV
|
||||
if name in settings_service.settings.CHAINS
|
||||
or settings_service.settings.DEV
|
||||
}
|
||||
return self.type_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from langchain import PromptTemplate
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.chains.base import Chain
|
||||
from langchain.document_loaders.base import BaseLoader
|
||||
from langchain.embeddings.base import Embeddings
|
||||
from langchain.schema.embeddings import Embeddings
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain.schema import BaseRetriever, Document
|
||||
from langchain.text_splitter import TextSplitter
|
||||
|
|
@ -45,7 +45,7 @@ DEFAULT_CUSTOM_COMPONENT_CODE = """from langflow import CustomComponent
|
|||
|
||||
from langchain.llms.base import BaseLLM
|
||||
from langchain.chains import LLMChain
|
||||
from langchain import PromptTemplate
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.schema import Document
|
||||
|
||||
import requests
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from fastapi import HTTPException
|
|||
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
from langflow.interface.custom.component import Component
|
||||
from langflow.interface.custom.directory_reader import DirectoryReader
|
||||
from langflow.services.utils import get_db_manager
|
||||
from langflow.services.getters import get_db_service
|
||||
from langflow.interface.custom.utils import extract_inner_type
|
||||
|
||||
from langflow.utils import validate
|
||||
|
|
@ -95,7 +95,20 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
|
||||
build_method = build_methods[0]
|
||||
|
||||
return build_method["args"]
|
||||
args = build_method["args"]
|
||||
for arg in args:
|
||||
if arg.get("type") == "prompt":
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Type hint Error",
|
||||
"traceback": (
|
||||
"Prompt type is not supported in the build method."
|
||||
" Try using PromptTemplate instead."
|
||||
),
|
||||
},
|
||||
)
|
||||
return args
|
||||
|
||||
@property
|
||||
def get_function_entrypoint_return_type(self) -> List[str]:
|
||||
|
|
@ -176,25 +189,25 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
return validate.create_function(self.code, self.function_entrypoint_name)
|
||||
|
||||
def load_flow(self, flow_id: str, tweaks: Optional[dict] = None) -> Any:
|
||||
from langflow.processing.process import build_sorted_vertices_with_caching
|
||||
from langflow.processing.process import build_sorted_vertices
|
||||
from langflow.processing.process import process_tweaks
|
||||
|
||||
db_manager = get_db_manager()
|
||||
with session_getter(db_manager) as session:
|
||||
db_service = get_db_service()
|
||||
with session_getter(db_service) as session:
|
||||
graph_data = flow.data if (flow := session.get(Flow, flow_id)) else None
|
||||
if not graph_data:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
if tweaks:
|
||||
graph_data = process_tweaks(graph_data=graph_data, tweaks=tweaks)
|
||||
return build_sorted_vertices_with_caching(graph_data)
|
||||
return build_sorted_vertices(graph_data)
|
||||
|
||||
def list_flows(self, *, get_session: Optional[Callable] = None) -> List[Flow]:
|
||||
if not self.user_id:
|
||||
raise ValueError("Session is invalid")
|
||||
try:
|
||||
get_session = get_session or session_getter
|
||||
db_manager = get_db_manager()
|
||||
with get_session(db_manager) as session:
|
||||
db_service = get_db_service()
|
||||
with get_session(db_service) as session:
|
||||
flows = session.query(Flow).filter(Flow.user_id == self.user_id).all()
|
||||
return flows
|
||||
except Exception as e:
|
||||
|
|
@ -209,8 +222,8 @@ class CustomComponent(Component, extra=Extra.allow):
|
|||
get_session: Optional[Callable] = None,
|
||||
) -> Flow:
|
||||
get_session = get_session or session_getter
|
||||
db_manager = get_db_manager()
|
||||
with get_session(db_manager) as session:
|
||||
db_service = get_db_service()
|
||||
with get_session(db_service) as session:
|
||||
if flow_id:
|
||||
flow = session.query(Flow).get(flow_id)
|
||||
elif flow_name:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
from langflow.template.frontend_node.documentloaders import DocumentLoaderFrontNode
|
||||
from langflow.interface.custom_lists import documentloaders_type_to_cls_dict
|
||||
|
||||
|
|
@ -31,12 +31,12 @@ class DocumentLoaderCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
return [
|
||||
documentloader.__name__
|
||||
for documentloader in self.type_to_loader_dict.values()
|
||||
if documentloader.__name__ in settings_manager.settings.DOCUMENTLOADERS
|
||||
or settings_manager.settings.DEV
|
||||
if documentloader.__name__ in settings_service.settings.DOCUMENTLOADERS
|
||||
or settings_service.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from typing import Dict, List, Optional, Type
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import embedding_type_to_cls_dict
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from langflow.template.frontend_node.embeddings import EmbeddingFrontendNode
|
||||
|
|
@ -33,12 +33,12 @@ class EmbeddingCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
return [
|
||||
embedding.__name__
|
||||
for embedding in self.type_to_loader_dict.values()
|
||||
if embedding.__name__ in settings_manager.settings.EMBEDDINGS
|
||||
or settings_manager.settings.DEV
|
||||
if embedding.__name__ in settings_service.settings.EMBEDDINGS
|
||||
or settings_service.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import importlib
|
||||
from typing import Any, Type
|
||||
|
||||
from langchain import PromptTemplate
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.agents import Agent
|
||||
from langchain.base_language import BaseLanguageModel
|
||||
from langchain.chains.base import Chain
|
||||
|
|
@ -144,6 +144,8 @@ def import_chain(chain: str) -> Type[Chain]:
|
|||
|
||||
if chain in CUSTOM_CHAINS:
|
||||
return CUSTOM_CHAINS[chain]
|
||||
if chain == "SQLDatabaseChain":
|
||||
return import_class("langchain_experimental.sql.SQLDatabaseChain")
|
||||
return import_class(f"langchain.chains.{chain}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import json
|
||||
import orjson
|
||||
from typing import Any, Callable, Dict, Sequence, Type, TYPE_CHECKING
|
||||
|
||||
from langchain.schema import Document
|
||||
from langchain.agents import agent as agent_module
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
from langchain.agents.agent_toolkits.base import BaseToolkit
|
||||
|
|
@ -40,12 +40,23 @@ if TYPE_CHECKING:
|
|||
from langflow import CustomComponent
|
||||
|
||||
|
||||
def build_vertex_in_params(params: Dict) -> Dict:
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
|
||||
# If any of the values in params is a Vertex, we will build it
|
||||
return {
|
||||
key: value.build() if isinstance(value, Vertex) else value
|
||||
for key, value in params.items()
|
||||
}
|
||||
|
||||
|
||||
def instantiate_class(
|
||||
node_type: str, base_type: str, params: Dict, user_id=None
|
||||
) -> Any:
|
||||
"""Instantiate class from module type and key, and params"""
|
||||
params = convert_params_to_sets(params)
|
||||
params = convert_kwargs(params)
|
||||
|
||||
if node_type in CUSTOM_NODES:
|
||||
if custom_node := CUSTOM_NODES.get(node_type):
|
||||
if hasattr(custom_node, "initialize"):
|
||||
|
|
@ -100,7 +111,7 @@ def instantiate_based_on_type(class_object, base_type, node_type, params, user_i
|
|||
elif base_type == "vectorstores":
|
||||
return instantiate_vectorstore(class_object, params)
|
||||
elif base_type == "documentloaders":
|
||||
return instantiate_documentloader(class_object, params)
|
||||
return instantiate_documentloader(node_type, class_object, params)
|
||||
elif base_type == "textsplitters":
|
||||
return instantiate_textsplitter(class_object, params)
|
||||
elif base_type == "utilities":
|
||||
|
|
@ -289,6 +300,13 @@ def instantiate_embedding(node_type, class_object, params: Dict):
|
|||
|
||||
def instantiate_vectorstore(class_object: Type[VectorStore], params: Dict):
|
||||
search_kwargs = params.pop("search_kwargs", {})
|
||||
# clean up docs or texts to have only documents
|
||||
if "texts" in params:
|
||||
params["documents"] = params.pop("texts")
|
||||
if "documents" in params:
|
||||
params["documents"] = [
|
||||
doc for doc in params["documents"] if isinstance(doc, Document)
|
||||
]
|
||||
if initializer := vecstore_initializer.get(class_object.__name__):
|
||||
vecstore = initializer(class_object, params)
|
||||
else:
|
||||
|
|
@ -303,7 +321,9 @@ def instantiate_vectorstore(class_object: Type[VectorStore], params: Dict):
|
|||
return vecstore
|
||||
|
||||
|
||||
def instantiate_documentloader(class_object: Type[BaseLoader], params: Dict):
|
||||
def instantiate_documentloader(
|
||||
node_type: str, class_object: Type[BaseLoader], params: Dict
|
||||
):
|
||||
if "file_filter" in params:
|
||||
# file_filter will be a string but we need a function
|
||||
# that will be used to filter the files using file_filter
|
||||
|
|
@ -323,6 +343,11 @@ def instantiate_documentloader(class_object: Type[BaseLoader], params: Dict):
|
|||
raise ValueError(
|
||||
"The metadata you provided is not a valid JSON string."
|
||||
) from exc
|
||||
|
||||
if node_type == "WebBaseLoader":
|
||||
if web_path := params.pop("web_path", None):
|
||||
params["web_paths"] = [web_path]
|
||||
|
||||
docs = class_object(**params).load()
|
||||
# Now if metadata is an empty dict, we will not add it to the documents
|
||||
if metadata:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from langchain.vectorstores import (
|
|||
SupabaseVectorStore,
|
||||
MongoDBAtlasVectorSearch,
|
||||
)
|
||||
|
||||
from langchain.schema import Document
|
||||
import os
|
||||
|
||||
import orjson
|
||||
|
|
@ -201,11 +201,16 @@ def initialize_chroma(class_object: Type[Chroma], params: dict):
|
|||
if "texts" in params:
|
||||
params["documents"] = params.pop("texts")
|
||||
for doc in params["documents"]:
|
||||
if not isinstance(doc, Document):
|
||||
# remove any non-Document objects from the list
|
||||
params["documents"].remove(doc)
|
||||
continue
|
||||
if doc.metadata is None:
|
||||
doc.metadata = {}
|
||||
for key, value in doc.metadata.items():
|
||||
if value is None:
|
||||
doc.metadata[key] = ""
|
||||
|
||||
chromadb = class_object.from_documents(**params)
|
||||
if persist:
|
||||
chromadb.persist()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,4 @@
|
|||
from langflow.interface.agents.base import agent_creator
|
||||
from langflow.interface.chains.base import chain_creator
|
||||
from langflow.interface.document_loaders.base import documentloader_creator
|
||||
from langflow.interface.embeddings.base import embedding_creator
|
||||
from langflow.interface.llms.base import llm_creator
|
||||
from langflow.interface.memories.base import memory_creator
|
||||
from langflow.interface.prompts.base import prompt_creator
|
||||
from langflow.interface.text_splitters.base import textsplitter_creator
|
||||
from langflow.interface.toolkits.base import toolkits_creator
|
||||
from langflow.interface.tools.base import tool_creator
|
||||
from langflow.interface.utilities.base import utility_creator
|
||||
from langflow.interface.vector_store.base import vectorstore_creator
|
||||
from langflow.interface.wrappers.base import wrapper_creator
|
||||
from langflow.interface.output_parsers.base import output_parser_creator
|
||||
from langflow.interface.retrievers.base import retriever_creator
|
||||
from langflow.interface.custom.base import custom_component_creator
|
||||
from langflow.services.getters import get_settings_service
|
||||
from langflow.utils.lazy_load import LazyLoadDictBase
|
||||
|
||||
|
||||
|
|
@ -33,24 +18,10 @@ class AllTypesDict(LazyLoadDictBase):
|
|||
}
|
||||
|
||||
def get_type_dict(self):
|
||||
return {
|
||||
"agents": agent_creator.to_list(),
|
||||
"prompts": prompt_creator.to_list(),
|
||||
"llms": llm_creator.to_list(),
|
||||
"tools": tool_creator.to_list(),
|
||||
"chains": chain_creator.to_list(),
|
||||
"memory": memory_creator.to_list(),
|
||||
"toolkits": toolkits_creator.to_list(),
|
||||
"wrappers": wrapper_creator.to_list(),
|
||||
"documentLoaders": documentloader_creator.to_list(),
|
||||
"vectorStore": vectorstore_creator.to_list(),
|
||||
"embeddings": embedding_creator.to_list(),
|
||||
"textSplitters": textsplitter_creator.to_list(),
|
||||
"utilities": utility_creator.to_list(),
|
||||
"outputParsers": output_parser_creator.to_list(),
|
||||
"retrievers": retriever_creator.to_list(),
|
||||
"custom_components": custom_component_creator.to_list(),
|
||||
}
|
||||
from langflow.interface.types import get_all_types_dict
|
||||
|
||||
settings_service = get_settings_service()
|
||||
return get_all_types_dict(settings_service=settings_service)
|
||||
|
||||
|
||||
lazy_load_dict = AllTypesDict()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from typing import Dict, List, Optional, Type
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import llm_type_to_cls_dict
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.llms import LLMFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -34,12 +34,12 @@ class LLMCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
return [
|
||||
llm.__name__
|
||||
for llm in self.type_to_loader_dict.values()
|
||||
if llm.__name__ in settings_manager.settings.LLMS
|
||||
or settings_manager.settings.DEV
|
||||
if llm.__name__ in settings_service.settings.LLMS
|
||||
or settings_service.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from typing import Dict, List, Optional, Type
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.custom_lists import memory_type_to_cls_dict
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
from langflow.template.frontend_node.memories import MemoryFrontendNode
|
||||
|
|
@ -49,12 +49,12 @@ class MemoryCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
return [
|
||||
memory.__name__
|
||||
for memory in self.type_to_loader_dict.values()
|
||||
if memory.__name__ in settings_manager.settings.MEMORIES
|
||||
or settings_manager.settings.DEV
|
||||
if memory.__name__ in settings_service.settings.MEMORIES
|
||||
or settings_service.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from langchain import output_parsers
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.output_parsers import OutputParserFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -24,7 +24,7 @@ class OutputParserCreator(LangChainTypeCreator):
|
|||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
self.type_dict = {
|
||||
output_parser_name: import_class(
|
||||
f"langchain.output_parsers.{output_parser_name}"
|
||||
|
|
@ -35,8 +35,8 @@ class OutputParserCreator(LangChainTypeCreator):
|
|||
self.type_dict = {
|
||||
name: output_parser
|
||||
for name, output_parser in self.type_dict.items()
|
||||
if name in settings_manager.settings.OUTPUT_PARSERS
|
||||
or settings_manager.settings.DEV
|
||||
if name in settings_service.settings.OUTPUT_PARSERS
|
||||
or settings_service.settings.DEV
|
||||
}
|
||||
return self.type_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from langchain import prompts
|
|||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.prompts import PromptFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -21,7 +21,7 @@ class PromptCreator(LangChainTypeCreator):
|
|||
|
||||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
if self.type_dict is None:
|
||||
self.type_dict = {
|
||||
prompt_name: import_class(f"langchain.prompts.{prompt_name}")
|
||||
|
|
@ -36,8 +36,8 @@ class PromptCreator(LangChainTypeCreator):
|
|||
self.type_dict = {
|
||||
name: prompt
|
||||
for name, prompt in self.type_dict.items()
|
||||
if name in settings_manager.settings.PROMPTS
|
||||
or settings_manager.settings.DEV
|
||||
if name in settings_service.settings.PROMPTS
|
||||
or settings_service.settings.DEV
|
||||
}
|
||||
return self.type_dict
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from langchain import retrievers
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.retrievers import RetrieverFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -49,12 +49,12 @@ class RetrieverCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
return [
|
||||
retriever
|
||||
for retriever in self.type_to_loader_dict.keys()
|
||||
if retriever in settings_manager.settings.RETRIEVERS
|
||||
or settings_manager.settings.DEV
|
||||
if retriever in settings_service.settings.RETRIEVERS
|
||||
or settings_service.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,9 @@
|
|||
from typing import Any, Dict, Tuple
|
||||
from langflow.services.cache.utils import memoize_dict
|
||||
from typing import Dict, Tuple
|
||||
from langflow.graph import Graph
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@memoize_dict(maxsize=10)
|
||||
def build_langchain_object_with_caching(data_graph):
|
||||
"""
|
||||
Build langchain object from data_graph.
|
||||
"""
|
||||
|
||||
logger.debug("Building langchain object")
|
||||
graph = Graph.from_payload(data_graph)
|
||||
return graph.build()
|
||||
|
||||
|
||||
@memoize_dict(maxsize=10)
|
||||
def build_sorted_vertices_with_caching(data_graph) -> Tuple[Any, Dict]:
|
||||
def build_sorted_vertices(data_graph) -> Tuple[Graph, Dict]:
|
||||
"""
|
||||
Build langchain object from data_graph.
|
||||
"""
|
||||
|
|
@ -29,7 +16,7 @@ def build_sorted_vertices_with_caching(data_graph) -> Tuple[Any, Dict]:
|
|||
vertex.build()
|
||||
if vertex.artifacts:
|
||||
artifacts.update(vertex.artifacts)
|
||||
return graph.build(), artifacts
|
||||
return graph, artifacts
|
||||
|
||||
|
||||
def build_langchain_object(data_graph):
|
||||
|
|
@ -58,8 +45,12 @@ def get_memory_key(langchain_object):
|
|||
"chat_history": "history",
|
||||
"history": "chat_history",
|
||||
}
|
||||
memory_key = langchain_object.memory.memory_key
|
||||
return mem_key_dict.get(memory_key)
|
||||
# Check if memory_key attribute exists
|
||||
if hasattr(langchain_object.memory, "memory_key"):
|
||||
memory_key = langchain_object.memory.memory_key
|
||||
return mem_key_dict.get(memory_key)
|
||||
else:
|
||||
return None # or some other default value or action
|
||||
|
||||
|
||||
def update_memory_keys(langchain_object, possible_new_mem_key):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
from langflow.template.frontend_node.textsplitters import TextSplittersFrontendNode
|
||||
from langflow.interface.custom_lists import textsplitter_type_to_cls_dict
|
||||
|
||||
|
|
@ -31,12 +31,12 @@ class TextSplitterCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
return [
|
||||
textsplitter.__name__
|
||||
for textsplitter in self.type_to_loader_dict.values()
|
||||
if textsplitter.__name__ in settings_manager.settings.TEXTSPLITTERS
|
||||
or settings_manager.settings.DEV
|
||||
if textsplitter.__name__ in settings_service.settings.TEXTSPLITTERS
|
||||
or settings_service.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from langchain.agents import agent_toolkits
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class, import_module
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from loguru import logger
|
||||
from langflow.utils.util import build_template_from_class
|
||||
|
|
@ -30,7 +30,7 @@ class ToolkitCreator(LangChainTypeCreator):
|
|||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
self.type_dict = {
|
||||
toolkit_name: import_class(
|
||||
f"langchain.agents.agent_toolkits.{toolkit_name}"
|
||||
|
|
@ -38,7 +38,7 @@ class ToolkitCreator(LangChainTypeCreator):
|
|||
# if toolkit_name is not lower case it is a class
|
||||
for toolkit_name in agent_toolkits.__all__
|
||||
if not toolkit_name.islower()
|
||||
and toolkit_name in settings_manager.settings.TOOLKITS
|
||||
and toolkit_name in settings_service.settings.TOOLKITS
|
||||
}
|
||||
|
||||
return self.type_dict
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from langflow.interface.tools.constants import (
|
|||
OTHER_TOOLS,
|
||||
)
|
||||
from langflow.interface.tools.util import get_tool_params
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.field.base import TemplateField
|
||||
from langflow.template.template.base import Template
|
||||
|
|
@ -67,7 +67,7 @@ class ToolCreator(LangChainTypeCreator):
|
|||
|
||||
@property
|
||||
def type_to_loader_dict(self) -> Dict:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
if self.tools_dict is None:
|
||||
all_tools = {}
|
||||
|
||||
|
|
@ -77,8 +77,8 @@ class ToolCreator(LangChainTypeCreator):
|
|||
tool_name = tool_params.get("name") or tool
|
||||
|
||||
if (
|
||||
tool_name in settings_manager.settings.TOOLS
|
||||
or settings_manager.settings.DEV
|
||||
tool_name in settings_service.settings.TOOLS
|
||||
or settings_service.settings.DEV
|
||||
):
|
||||
if tool_name == "JsonSpec":
|
||||
tool_params["path"] = tool_params.pop("dict_") # type: ignore
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import ast
|
||||
import inspect
|
||||
import textwrap
|
||||
from typing import Dict, Union
|
||||
|
||||
from langchain.agents.tools import Tool
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
|
||||
tree = ast.parse(inspect.getsource(func))
|
||||
tree = ast.parse(textwrap.dedent(inspect.getsource(func)))
|
||||
|
||||
# Iterate over the statements in the abstract syntax tree
|
||||
for node in ast.walk(tree):
|
||||
|
|
@ -58,13 +58,7 @@ def get_func_tool_params(func, **kwargs) -> Union[Dict, None]:
|
|||
|
||||
|
||||
def get_class_tool_params(cls, **kwargs) -> Union[Dict, None]:
|
||||
try:
|
||||
tree = ast.parse(inspect.getsource(cls))
|
||||
except IndentationError:
|
||||
logger.error(
|
||||
f"Error parsing class {cls.__name__}. Make sure there are no tabs in the code."
|
||||
)
|
||||
return None
|
||||
tree = ast.parse(textwrap.dedent(inspect.getsource(cls)))
|
||||
|
||||
tool_params = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import ast
|
||||
import contextlib
|
||||
from typing import Any, List
|
||||
from langflow.api.utils import merge_nested_dicts_with_renaming
|
||||
from langflow.api.utils import get_new_key
|
||||
from langflow.interface.agents.base import agent_creator
|
||||
from langflow.interface.chains.base import chain_creator
|
||||
from langflow.interface.custom.constants import CUSTOM_COMPONENT_SUPPORTED_TYPES
|
||||
from langflow.interface.custom.utils import extract_inner_type
|
||||
from langflow.interface.document_loaders.base import documentloader_creator
|
||||
from langflow.interface.embeddings.base import embedding_creator
|
||||
from langflow.interface.importing.utils import get_function_custom
|
||||
|
|
@ -84,6 +85,8 @@ def build_langchain_types_dict(): # sourcery skip: dict-assign-update-to-union
|
|||
|
||||
|
||||
def process_type(field_type: str):
|
||||
if field_type.startswith("list") or field_type.startswith("List"):
|
||||
return extract_inner_type(field_type)
|
||||
return "prompt" if field_type == "Prompt" else field_type
|
||||
|
||||
|
||||
|
|
@ -100,6 +103,7 @@ def add_new_custom_field(
|
|||
# if it is, update the value
|
||||
display_name = field_config.pop("display_name", field_name)
|
||||
field_type = field_config.pop("field_type", field_type)
|
||||
field_contains_list = "list" in field_type.lower()
|
||||
field_type = process_type(field_type)
|
||||
field_value = field_config.pop("value", field_value)
|
||||
field_advanced = field_config.pop("advanced", False)
|
||||
|
|
@ -110,7 +114,9 @@ def add_new_custom_field(
|
|||
# If options is a list, then it's a dropdown
|
||||
# If options is None, then it's a list of strings
|
||||
is_list = isinstance(field_config.get("options"), list)
|
||||
field_config["is_list"] = is_list or field_config.get("is_list", False)
|
||||
field_config["is_list"] = (
|
||||
is_list or field_config.get("is_list", False) or field_contains_list
|
||||
)
|
||||
|
||||
if "name" in field_config:
|
||||
warnings.warn(
|
||||
|
|
@ -172,7 +178,7 @@ def extract_type_from_optional(field_type):
|
|||
Returns:
|
||||
str: The extracted type, or an empty string if no type was found.
|
||||
"""
|
||||
match = re.search(r"\[(.*?)\]", field_type)
|
||||
match = re.search(r"\[(.*?)\]$", field_type)
|
||||
return match[1] if match else None
|
||||
|
||||
|
||||
|
|
@ -284,31 +290,44 @@ def add_base_classes(frontend_node, return_types: List[str]):
|
|||
|
||||
def build_langchain_template_custom_component(custom_component: CustomComponent):
|
||||
"""Build a custom component template for the langchain"""
|
||||
logger.debug("Building custom component template")
|
||||
frontend_node = build_frontend_node(custom_component)
|
||||
try:
|
||||
logger.debug("Building custom component template")
|
||||
frontend_node = build_frontend_node(custom_component)
|
||||
|
||||
if frontend_node is None:
|
||||
return None
|
||||
logger.debug("Built base frontend node")
|
||||
template_config = custom_component.build_template_config
|
||||
if frontend_node is None:
|
||||
return None
|
||||
logger.debug("Built base frontend node")
|
||||
template_config = custom_component.build_template_config
|
||||
|
||||
update_attributes(frontend_node, template_config)
|
||||
logger.debug("Updated attributes")
|
||||
field_config = build_field_config(custom_component)
|
||||
logger.debug("Built field config")
|
||||
add_extra_fields(
|
||||
frontend_node, field_config, custom_component.get_function_entrypoint_args
|
||||
)
|
||||
logger.debug("Added extra fields")
|
||||
frontend_node = add_code_field(
|
||||
frontend_node, custom_component.code, field_config.get("code", {})
|
||||
)
|
||||
logger.debug("Added code field")
|
||||
add_base_classes(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
logger.debug("Added base classes")
|
||||
return frontend_node
|
||||
update_attributes(frontend_node, template_config)
|
||||
logger.debug("Updated attributes")
|
||||
field_config = build_field_config(custom_component)
|
||||
logger.debug("Built field config")
|
||||
entrypoint_args = custom_component.get_function_entrypoint_args
|
||||
|
||||
add_extra_fields(frontend_node, field_config, entrypoint_args)
|
||||
logger.debug("Added extra fields")
|
||||
frontend_node = add_code_field(
|
||||
frontend_node, custom_component.code, field_config.get("code", {})
|
||||
)
|
||||
logger.debug("Added code field")
|
||||
add_base_classes(
|
||||
frontend_node, custom_component.get_function_entrypoint_return_type
|
||||
)
|
||||
logger.debug("Added base classes")
|
||||
return frontend_node
|
||||
except Exception as exc:
|
||||
if isinstance(exc, HTTPException):
|
||||
raise exc
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": (
|
||||
"Invalid type convertion. Please check your code and try again."
|
||||
),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
) from exc
|
||||
|
||||
|
||||
def load_files_from_path(path: str):
|
||||
|
|
@ -416,6 +435,24 @@ def build_invalid_menu(invalid_components):
|
|||
return invalid_menu
|
||||
|
||||
|
||||
def merge_nested_dicts_with_renaming(dict1, dict2):
|
||||
for key, value in dict2.items():
|
||||
if (
|
||||
key in dict1
|
||||
and isinstance(value, dict)
|
||||
and isinstance(dict1.get(key), dict)
|
||||
):
|
||||
for sub_key, sub_value in value.items():
|
||||
if sub_key in dict1[key]:
|
||||
new_key = get_new_key(dict1[key], sub_key)
|
||||
dict1[key][new_key] = sub_value
|
||||
else:
|
||||
dict1[key][sub_key] = sub_value
|
||||
else:
|
||||
dict1[key] = value
|
||||
return dict1
|
||||
|
||||
|
||||
def build_langchain_custom_component_list_from_path(path: str):
|
||||
"""Build a list of custom components for the langchain from a given path"""
|
||||
file_list = load_files_from_path(path)
|
||||
|
|
@ -429,3 +466,51 @@ def build_langchain_custom_component_list_from_path(path: str):
|
|||
invalid_menu = build_invalid_menu(invalid_components)
|
||||
|
||||
return merge_nested_dicts_with_renaming(valid_menu, invalid_menu)
|
||||
|
||||
|
||||
def get_all_types_dict(settings_service):
|
||||
native_components = build_langchain_types_dict()
|
||||
# custom_components is a list of dicts
|
||||
# need to merge all the keys into one dict
|
||||
custom_components_from_file: dict[str, Any] = {}
|
||||
if settings_service.settings.COMPONENTS_PATH:
|
||||
logger.info(
|
||||
f"Building custom components from {settings_service.settings.COMPONENTS_PATH}"
|
||||
)
|
||||
|
||||
custom_component_dicts = []
|
||||
processed_paths = []
|
||||
for path in settings_service.settings.COMPONENTS_PATH:
|
||||
if str(path) in processed_paths:
|
||||
continue
|
||||
custom_component_dict = build_langchain_custom_component_list_from_path(
|
||||
str(path)
|
||||
)
|
||||
custom_component_dicts.append(custom_component_dict)
|
||||
processed_paths.append(str(path))
|
||||
|
||||
logger.info(f"Loading {len(custom_component_dicts)} category(ies)")
|
||||
for custom_component_dict in custom_component_dicts:
|
||||
# custom_component_dict is a dict of dicts
|
||||
if not custom_component_dict:
|
||||
continue
|
||||
category = list(custom_component_dict.keys())[0]
|
||||
logger.info(
|
||||
f"Loading {len(custom_component_dict[category])} component(s) from category {category}"
|
||||
)
|
||||
custom_components_from_file = merge_nested_dicts_with_renaming(
|
||||
custom_components_from_file, custom_component_dict
|
||||
)
|
||||
|
||||
return merge_nested_dicts_with_renaming(
|
||||
native_components, custom_components_from_file
|
||||
)
|
||||
|
||||
|
||||
def merge_nested_dicts(dict1, dict2):
|
||||
for key, value in dict2.items():
|
||||
if isinstance(value, dict) and isinstance(dict1.get(key), dict):
|
||||
dict1[key] = merge_nested_dicts(dict1[key], value)
|
||||
else:
|
||||
dict1[key] = value
|
||||
return dict1
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from langchain import SQLDatabase, utilities
|
||||
from langchain import utilities
|
||||
|
||||
from langflow.custom.customs import get_custom_nodes
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.utilities import UtilitiesFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -27,18 +27,18 @@ class UtilityCreator(LangChainTypeCreator):
|
|||
from the langchain.chains module and filtering them according to the settings.utilities list.
|
||||
"""
|
||||
if self.type_dict is None:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
self.type_dict = {
|
||||
utility_name: import_class(f"langchain.utilities.{utility_name}")
|
||||
for utility_name in utilities.__all__
|
||||
}
|
||||
self.type_dict["SQLDatabase"] = SQLDatabase
|
||||
self.type_dict["SQLDatabase"] = utilities.SQLDatabase
|
||||
# Filter according to settings.utilities
|
||||
self.type_dict = {
|
||||
name: utility
|
||||
for name, utility in self.type_dict.items()
|
||||
if name in settings_manager.settings.UTILITIES
|
||||
or settings_manager.settings.DEV
|
||||
if name in settings_service.settings.UTILITIES
|
||||
or settings_service.settings.DEV
|
||||
}
|
||||
|
||||
return self.type_dict
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from langchain.base_language import BaseLanguageModel
|
|||
from PIL.Image import Image
|
||||
from loguru import logger
|
||||
from langflow.services.chat.config import ChatConfig
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
|
||||
def load_file_into_dict(file_path: str) -> dict:
|
||||
|
|
@ -36,7 +36,7 @@ def pil_to_base64(image: Image) -> str:
|
|||
return img_str.decode("utf-8")
|
||||
|
||||
|
||||
def try_setting_streaming_options(langchain_object, websocket):
|
||||
def try_setting_streaming_options(langchain_object):
|
||||
# If the LLM type is OpenAI or ChatOpenAI,
|
||||
# set streaming to True
|
||||
# First we need to find the LLM
|
||||
|
|
@ -64,11 +64,11 @@ def extract_input_variables_from_prompt(prompt: str) -> list[str]:
|
|||
|
||||
def setup_llm_caching():
|
||||
"""Setup LLM caching."""
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
try:
|
||||
set_langchain_cache(settings_manager.settings)
|
||||
set_langchain_cache(settings_service.settings)
|
||||
except ImportError:
|
||||
logger.warning(f"Could not import {settings_manager.settings.CACHE}. ")
|
||||
logger.warning(f"Could not import {settings_service.settings.CACHE_TYPE}. ")
|
||||
except Exception as exc:
|
||||
logger.warning(f"Could not setup LLM caching. Error: {exc}")
|
||||
|
||||
|
|
@ -77,9 +77,16 @@ def set_langchain_cache(settings):
|
|||
import langchain
|
||||
from langflow.interface.importing.utils import import_class
|
||||
|
||||
cache_type = os.getenv("LANGFLOW_LANGCHAIN_CACHE")
|
||||
cache_class = import_class(f"langchain.cache.{cache_type or settings.CACHE}")
|
||||
if cache_type := os.getenv("LANGFLOW_LANGCHAIN_CACHE"):
|
||||
try:
|
||||
cache_class = import_class(
|
||||
f"langchain.cache.{cache_type or settings.LANGCHAIN_CACHE}"
|
||||
)
|
||||
|
||||
logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
|
||||
langchain.llm_cache = cache_class()
|
||||
logger.info(f"LLM caching setup with {cache_class.__name__}")
|
||||
logger.debug(f"Setting up LLM caching with {cache_class.__name__}")
|
||||
langchain.llm_cache = cache_class()
|
||||
logger.info(f"LLM caching setup with {cache_class.__name__}")
|
||||
except ImportError:
|
||||
logger.warning(f"Could not import {cache_type}. ")
|
||||
else:
|
||||
logger.info("No LLM cache set.")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from langchain import vectorstores
|
|||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from langflow.interface.importing.utils import import_class
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
|
||||
from langflow.template.frontend_node.vectorstores import VectorStoreFrontendNode
|
||||
from loguru import logger
|
||||
|
|
@ -44,12 +44,12 @@ class VectorstoreCreator(LangChainTypeCreator):
|
|||
return None
|
||||
|
||||
def to_list(self) -> List[str]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
return [
|
||||
vectorstore
|
||||
for vectorstore in self.type_to_loader_dict.keys()
|
||||
if vectorstore in settings_manager.settings.VECTORSTORES
|
||||
or settings_manager.settings.DEV
|
||||
if vectorstore in settings_service.settings.VECTORSTORES
|
||||
or settings_service.settings.DEV
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Dict, List, Optional
|
||||
|
||||
from langchain import requests, sql_database
|
||||
from langchain.utilities import requests, sql_database
|
||||
|
||||
from langflow.interface.base import LangChainTypeCreator
|
||||
from loguru import logger
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
instance: C4
|
||||
autoscale_min: 1
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# This file is used by lc-serve to load the mounted app and serve it.
|
||||
|
||||
import os
|
||||
|
||||
# Use the JCLOUD_WORKSPACE for db URL if it's provided by JCloud.
|
||||
if "JCLOUD_WORKSPACE" in os.environ:
|
||||
os.environ[
|
||||
"LANGFLOW_DATABASE_URL"
|
||||
] = f"sqlite:///{os.environ['JCLOUD_WORKSPACE']}/langflow.db"
|
||||
|
||||
from langflow.main import setup_app
|
||||
from langflow.utils.logger import configure
|
||||
|
||||
configure(log_level="DEBUG")
|
||||
app = setup_app()
|
||||
|
|
@ -9,8 +9,11 @@ from langflow.api import router
|
|||
|
||||
|
||||
from langflow.interface.utils import setup_llm_caching
|
||||
from langflow.services.database.utils import initialize_database
|
||||
from langflow.services.manager import initialize_services, teardown_services
|
||||
from langflow.services.utils import initialize_services
|
||||
from langflow.services.plugins.langfuse import LangfuseInstance
|
||||
from langflow.services.utils import (
|
||||
teardown_services,
|
||||
)
|
||||
from langflow.utils.logger import configure
|
||||
|
||||
|
||||
|
|
@ -38,9 +41,12 @@ def create_app():
|
|||
app.include_router(router)
|
||||
|
||||
app.on_event("startup")(initialize_services)
|
||||
app.on_event("startup")(initialize_database)
|
||||
app.on_event("startup")(setup_llm_caching)
|
||||
app.on_event("startup")(LangfuseInstance.update)
|
||||
|
||||
app.on_event("shutdown")(teardown_services)
|
||||
app.on_event("shutdown")(LangfuseInstance.teardown)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Union
|
||||
from typing import List, Union, TYPE_CHECKING
|
||||
from langflow.api.v1.callback import (
|
||||
AsyncStreamingLLMCallbackHandler,
|
||||
StreamingLLMCallbackHandler,
|
||||
|
|
@ -6,6 +6,52 @@ from langflow.api.v1.callback import (
|
|||
from langflow.processing.process import fix_memory_inputs, format_actions
|
||||
from loguru import logger
|
||||
from langchain.agents.agent import AgentExecutor
|
||||
from langchain.callbacks.base import BaseCallbackHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langfuse.callback import CallbackHandler # type: ignore
|
||||
|
||||
|
||||
def setup_callbacks(sync, trace_id, **kwargs):
|
||||
"""Setup callbacks for langchain object"""
|
||||
callbacks = []
|
||||
if sync:
|
||||
callbacks.append(StreamingLLMCallbackHandler(**kwargs))
|
||||
else:
|
||||
callbacks.append(AsyncStreamingLLMCallbackHandler(**kwargs))
|
||||
|
||||
if langfuse_callback := get_langfuse_callback(trace_id=trace_id):
|
||||
logger.debug("Langfuse callback loaded")
|
||||
callbacks.append(langfuse_callback)
|
||||
return callbacks
|
||||
|
||||
|
||||
def get_langfuse_callback(trace_id):
|
||||
from langflow.services.plugins.langfuse import LangfuseInstance
|
||||
from langfuse.callback import CreateTrace
|
||||
|
||||
logger.debug("Initializing langfuse callback")
|
||||
if langfuse := LangfuseInstance.get():
|
||||
logger.debug("Langfuse credentials found")
|
||||
try:
|
||||
trace = langfuse.trace(CreateTrace(id=trace_id))
|
||||
return trace.getNewHandler()
|
||||
except Exception as exc:
|
||||
logger.error(f"Error initializing langfuse callback: {exc}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def flush_langfuse_callback_if_present(
|
||||
callbacks: List[Union[BaseCallbackHandler, "CallbackHandler"]]
|
||||
):
|
||||
"""
|
||||
If langfuse callback is present, run callback.langfuse.flush()
|
||||
"""
|
||||
for callback in callbacks:
|
||||
if hasattr(callback, "langfuse"):
|
||||
callback.langfuse.flush()
|
||||
break
|
||||
|
||||
|
||||
async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwargs):
|
||||
|
|
@ -27,13 +73,18 @@ async def get_result_and_steps(langchain_object, inputs: Union[dict, str], **kwa
|
|||
logger.error(f"Error fixing memory inputs: {exc}")
|
||||
|
||||
try:
|
||||
async_callbacks = [AsyncStreamingLLMCallbackHandler(**kwargs)]
|
||||
output = await langchain_object.acall(inputs, callbacks=async_callbacks)
|
||||
trace_id = kwargs.pop("session_id", None)
|
||||
callbacks = setup_callbacks(sync=False, trace_id=trace_id, **kwargs)
|
||||
output = await langchain_object.acall(inputs, callbacks=callbacks)
|
||||
except Exception as exc:
|
||||
# make the error message more informative
|
||||
logger.debug(f"Error: {str(exc)}")
|
||||
sync_callbacks = [StreamingLLMCallbackHandler(**kwargs)]
|
||||
output = langchain_object(inputs, callbacks=sync_callbacks)
|
||||
trace_id = kwargs.pop("session_id", None)
|
||||
callbacks = setup_callbacks(sync=True, trace_id=trace_id, **kwargs)
|
||||
output = langchain_object(inputs, callbacks=callbacks)
|
||||
|
||||
# if langfuse callback is present, run callback.langfuse.flush()
|
||||
flush_langfuse_callback_if_present(callbacks)
|
||||
|
||||
intermediate_steps = (
|
||||
output.get("intermediate_steps", []) if isinstance(output, dict) else []
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@ import json
|
|||
from pathlib import Path
|
||||
from langchain.schema import AgentAction
|
||||
from langflow.interface.run import (
|
||||
build_sorted_vertices_with_caching,
|
||||
build_sorted_vertices,
|
||||
get_memory_key,
|
||||
update_memory_keys,
|
||||
)
|
||||
from langflow.services.getters import get_session_service
|
||||
from loguru import logger
|
||||
from langflow.graph import Graph
|
||||
from langchain.chains.base import Chain
|
||||
from langchain.vectorstores.base import VectorStore
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from langchain.schema import Document
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def fix_memory_inputs(langchain_object):
|
||||
|
|
@ -64,7 +68,7 @@ def get_result_and_thought(langchain_object: Any, inputs: dict):
|
|||
langchain_object.verbose = True
|
||||
|
||||
if hasattr(langchain_object, "return_intermediate_steps"):
|
||||
langchain_object.return_intermediate_steps = True
|
||||
langchain_object.return_intermediate_steps = False
|
||||
|
||||
fix_memory_inputs(langchain_object)
|
||||
|
||||
|
|
@ -92,26 +96,19 @@ def get_build_result(data_graph, session_id):
|
|||
# otherwise, build the graph and return the result
|
||||
if session_id:
|
||||
logger.debug(f"Loading LangChain object from session {session_id}")
|
||||
result = build_sorted_vertices_with_caching.get_result_by_session_id(session_id)
|
||||
result = build_sorted_vertices(data_graph=data_graph)
|
||||
if result is not None:
|
||||
logger.debug("Loaded LangChain object")
|
||||
return result
|
||||
|
||||
logger.debug("Building langchain object")
|
||||
return build_sorted_vertices_with_caching(data_graph)
|
||||
|
||||
|
||||
def clear_caches_if_needed(clear_cache: bool):
|
||||
if clear_cache:
|
||||
build_sorted_vertices_with_caching.clear_cache()
|
||||
logger.debug("Cleared cache")
|
||||
return build_sorted_vertices(data_graph)
|
||||
|
||||
|
||||
def load_langchain_object(
|
||||
data_graph: Dict[str, Any], session_id: str
|
||||
) -> Tuple[Union[Chain, VectorStore], Dict[str, Any], str]:
|
||||
langchain_object, artifacts = get_build_result(data_graph, session_id)
|
||||
session_id = build_sorted_vertices_with_caching.hash
|
||||
logger.debug("Loaded LangChain object")
|
||||
|
||||
if langchain_object is None:
|
||||
|
|
@ -139,33 +136,47 @@ def generate_result(langchain_object: Union[Chain, VectorStore], inputs: dict):
|
|||
raise ValueError("Inputs must be provided for a Chain")
|
||||
logger.debug("Generating result and thought")
|
||||
result = get_result_and_thought(langchain_object, inputs)
|
||||
|
||||
logger.debug("Generated result and thought")
|
||||
elif isinstance(langchain_object, VectorStore):
|
||||
result = langchain_object.search(**inputs)
|
||||
elif isinstance(langchain_object, Document):
|
||||
result = langchain_object.dict()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown langchain_object type: {type(langchain_object).__name__}"
|
||||
)
|
||||
logger.warning(f"Unknown langchain_object type: {type(langchain_object)}")
|
||||
result = langchain_object
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def process_graph_cached(
|
||||
class Result(BaseModel):
|
||||
result: Any
|
||||
session_id: str
|
||||
|
||||
|
||||
async def process_graph_cached(
|
||||
data_graph: Dict[str, Any],
|
||||
inputs: Optional[dict] = None,
|
||||
clear_cache=False,
|
||||
session_id=None,
|
||||
) -> Tuple[Any, str]:
|
||||
clear_caches_if_needed(clear_cache)
|
||||
# If session_id is provided, load the langchain_object from the session
|
||||
# else build the graph and return the result and the new session_id
|
||||
langchain_object, artifacts, session_id = load_langchain_object(
|
||||
data_graph, session_id
|
||||
)
|
||||
) -> Result:
|
||||
session_service = get_session_service()
|
||||
if clear_cache:
|
||||
session_service.clear_session(session_id)
|
||||
if session_id is None:
|
||||
session_id = session_service.generate_key(
|
||||
session_id=session_id, data_graph=data_graph
|
||||
)
|
||||
# Load the graph using SessionService
|
||||
graph, artifacts = session_service.load_session(session_id, data_graph)
|
||||
built_object = graph.build()
|
||||
processed_inputs = process_inputs(inputs, artifacts)
|
||||
result = generate_result(langchain_object, processed_inputs)
|
||||
result = generate_result(built_object, processed_inputs)
|
||||
# langchain_object is now updated with the new memory
|
||||
# we need to update the cache with the updated langchain_object
|
||||
session_service.update_session(session_id, (graph, artifacts))
|
||||
|
||||
return result, session_id
|
||||
return Result(result=result, session_id=session_id)
|
||||
|
||||
|
||||
def load_flow_from_json(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from gunicorn.app.base import BaseApplication # type: ignore
|
|||
class LangflowApplication(BaseApplication):
|
||||
def __init__(self, app, options=None):
|
||||
self.options = options or {}
|
||||
|
||||
self.options["worker_class"] = "uvicorn.workers.UvicornWorker"
|
||||
self.application = app
|
||||
super().__init__()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from langflow.services.factory import ServiceFactory
|
||||
from langflow.services.auth.service import AuthManager
|
||||
from langflow.services.auth.service import AuthService
|
||||
|
||||
|
||||
class AuthManagerFactory(ServiceFactory):
|
||||
name = "auth_manager"
|
||||
class AuthServiceFactory(ServiceFactory):
|
||||
name = "auth_service"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(AuthManager)
|
||||
super().__init__(AuthService)
|
||||
|
||||
def create(self, settings_manager):
|
||||
return AuthManager(settings_manager)
|
||||
def create(self, settings_service):
|
||||
return AuthService(settings_service)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ from langflow.services.base import Service
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.manager import SettingsManager
|
||||
from langflow.services.settings.manager import SettingsService
|
||||
|
||||
|
||||
class AuthManager(Service):
|
||||
name = "auth_manager"
|
||||
class AuthService(Service):
|
||||
name = "auth_service"
|
||||
|
||||
def __init__(self, settings_manager: "SettingsManager"):
|
||||
self.settings_manager = settings_manager
|
||||
def __init__(self, settings_service: "SettingsService"):
|
||||
self.settings_service = settings_service
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ from langflow.services.database.models.user.crud import (
|
|||
get_user_by_username,
|
||||
update_user_last_login_at,
|
||||
)
|
||||
from langflow.services.utils import get_session, get_settings_manager
|
||||
from langflow.services.getters import get_session, get_settings_service
|
||||
from sqlmodel import Session
|
||||
|
||||
oauth2_login = OAuth2PasswordBearer(tokenUrl="api/v1/login")
|
||||
|
||||
API_KEY_NAME = "api-key"
|
||||
API_KEY_NAME = "x-api-key"
|
||||
|
||||
api_key_query = APIKeyQuery(
|
||||
name=API_KEY_NAME, scheme_name="API key query", auto_error=False
|
||||
|
|
@ -33,19 +33,17 @@ async def api_key_security(
|
|||
header_param: str = Security(api_key_header),
|
||||
db: Session = Depends(get_session),
|
||||
) -> Optional[User]:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
result: Optional[Union[ApiKey, User]] = None
|
||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||
if settings_service.auth_settings.AUTO_LOGIN:
|
||||
# Get the first user
|
||||
if not settings_manager.auth_settings.FIRST_SUPERUSER:
|
||||
if not settings_service.auth_settings.SUPERUSER:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Missing first superuser credentials",
|
||||
)
|
||||
|
||||
result = get_user_by_username(
|
||||
db, settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
)
|
||||
result = get_user_by_username(db, settings_service.auth_settings.SUPERUSER)
|
||||
|
||||
elif not query_param and not header_param:
|
||||
raise HTTPException(
|
||||
|
|
@ -74,7 +72,7 @@ async def get_current_user(
|
|||
token: Annotated[str, Depends(oauth2_login)],
|
||||
db: Session = Depends(get_session),
|
||||
) -> User:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
|
@ -85,14 +83,14 @@ async def get_current_user(
|
|||
if isinstance(token, Coroutine):
|
||||
token = await token
|
||||
|
||||
if settings_manager.auth_settings.SECRET_KEY is None:
|
||||
if settings_service.auth_settings.SECRET_KEY is None:
|
||||
raise credentials_exception
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.auth_settings.ALGORITHM],
|
||||
settings_service.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_service.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
|
@ -132,19 +130,19 @@ def get_current_active_superuser(
|
|||
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
settings_manager = get_settings_manager()
|
||||
return settings_manager.auth_settings.pwd_context.verify(
|
||||
settings_service = get_settings_service()
|
||||
return settings_service.auth_settings.pwd_context.verify(
|
||||
plain_password, hashed_password
|
||||
)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
settings_manager = get_settings_manager()
|
||||
return settings_manager.auth_settings.pwd_context.hash(password)
|
||||
settings_service = get_settings_service()
|
||||
return settings_service.auth_settings.pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_token(data: dict, expires_delta: timedelta):
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
|
||||
to_encode = data.copy()
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
|
|
@ -152,8 +150,8 @@ def create_token(data: dict, expires_delta: timedelta):
|
|||
|
||||
return jwt.encode(
|
||||
to_encode,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithm=settings_manager.auth_settings.ALGORITHM,
|
||||
settings_service.auth_settings.SECRET_KEY,
|
||||
algorithm=settings_service.auth_settings.ALGORITHM,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -181,9 +179,9 @@ def create_super_user(
|
|||
|
||||
|
||||
def create_user_longterm_token(db: Session = Depends(get_session)) -> dict:
|
||||
settings_manager = get_settings_manager()
|
||||
username = settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
password = settings_manager.auth_settings.FIRST_SUPERUSER_PASSWORD
|
||||
settings_service = get_settings_service()
|
||||
username = settings_service.auth_settings.SUPERUSER
|
||||
password = settings_service.auth_settings.SUPERUSER_PASSWORD
|
||||
if not username or not password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -227,10 +225,10 @@ def get_user_id_from_token(token: str) -> UUID:
|
|||
def create_user_tokens(
|
||||
user_id: UUID, db: Session = Depends(get_session), update_last_login: bool = False
|
||||
) -> dict:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
|
||||
access_token_expires = timedelta(
|
||||
minutes=settings_manager.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
minutes=settings_service.auth_settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
access_token = create_token(
|
||||
data={"sub": str(user_id)},
|
||||
|
|
@ -238,7 +236,7 @@ def create_user_tokens(
|
|||
)
|
||||
|
||||
refresh_token_expires = timedelta(
|
||||
minutes=settings_manager.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES
|
||||
minutes=settings_service.auth_settings.REFRESH_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
refresh_token = create_token(
|
||||
data={"sub": str(user_id), "type": "rf"},
|
||||
|
|
@ -257,13 +255,13 @@ def create_user_tokens(
|
|||
|
||||
|
||||
def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)):
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings_manager.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_manager.auth_settings.ALGORITHM],
|
||||
settings_service.auth_settings.SECRET_KEY,
|
||||
algorithms=[settings_service.auth_settings.ALGORITHM],
|
||||
)
|
||||
user_id: UUID = payload.get("sub") # type: ignore
|
||||
token_type: str = payload.get("type") # type: ignore
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ from abc import ABC
|
|||
|
||||
class Service(ABC):
|
||||
name: str
|
||||
ready: bool = False
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def set_ready(self):
|
||||
self.ready = True
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from . import factory, manager
|
||||
from langflow.services.cache.manager import cache_manager
|
||||
from langflow.services.cache.flow import InMemoryCache
|
||||
from langflow.services.cache.manager import InMemoryCache
|
||||
|
||||
|
||||
__all__ = [
|
||||
"cache_manager",
|
||||
"factory",
|
||||
"manager",
|
||||
"InMemoryCache",
|
||||
|
|
|
|||
14
src/backend/langflow/services/cache/base.py
vendored
14
src/backend/langflow/services/cache/base.py
vendored
|
|
@ -1,11 +1,13 @@
|
|||
import abc
|
||||
|
||||
|
||||
class BaseCache(abc.ABC):
|
||||
class BaseCacheService(abc.ABC):
|
||||
"""
|
||||
Abstract base class for a cache.
|
||||
"""
|
||||
|
||||
name = "cache_service"
|
||||
|
||||
@abc.abstractmethod
|
||||
def get(self, key):
|
||||
"""
|
||||
|
|
@ -28,6 +30,16 @@ class BaseCache(abc.ABC):
|
|||
value: The value to cache.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def upsert(self, key, value):
|
||||
"""
|
||||
Add an item to the cache if it doesn't exist, or update it if it does.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete(self, key):
|
||||
"""
|
||||
|
|
|
|||
36
src/backend/langflow/services/cache/factory.py
vendored
36
src/backend/langflow/services/cache/factory.py
vendored
|
|
@ -1,11 +1,35 @@
|
|||
from langflow.services.cache.manager import CacheManager
|
||||
from langflow.services.cache.manager import InMemoryCache, RedisCache, BaseCacheService
|
||||
from langflow.services.factory import ServiceFactory
|
||||
from langflow.utils.logger import logger
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.manager import SettingsService
|
||||
|
||||
|
||||
class CacheManagerFactory(ServiceFactory):
|
||||
class CacheServiceFactory(ServiceFactory):
|
||||
def __init__(self):
|
||||
super().__init__(CacheManager)
|
||||
super().__init__(BaseCacheService)
|
||||
|
||||
def create(self):
|
||||
# Here you would have logic to create and configure a CacheManager
|
||||
return CacheManager()
|
||||
def create(self, settings_service: "SettingsService"):
|
||||
# Here you would have logic to create and configure a CacheService
|
||||
# based on the settings_service
|
||||
|
||||
if settings_service.settings.CACHE_TYPE == "redis":
|
||||
logger.debug("Creating Redis cache")
|
||||
redis_cache = RedisCache(
|
||||
host=settings_service.settings.REDIS_HOST,
|
||||
port=settings_service.settings.REDIS_PORT,
|
||||
db=settings_service.settings.REDIS_DB,
|
||||
expiration_time=settings_service.settings.REDIS_CACHE_EXPIRE,
|
||||
)
|
||||
if redis_cache.is_connected():
|
||||
logger.debug("Redis cache is connected")
|
||||
return redis_cache
|
||||
logger.warning(
|
||||
"Redis cache is not connected, falling back to in-memory cache"
|
||||
)
|
||||
return InMemoryCache()
|
||||
|
||||
elif settings_service.settings.CACHE_TYPE == "memory":
|
||||
return InMemoryCache()
|
||||
|
|
|
|||
146
src/backend/langflow/services/cache/flow.py
vendored
146
src/backend/langflow/services/cache/flow.py
vendored
|
|
@ -1,146 +0,0 @@
|
|||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from langflow.services.cache.base import BaseCache
|
||||
|
||||
|
||||
class InMemoryCache(BaseCache):
|
||||
"""
|
||||
A simple in-memory cache using an OrderedDict.
|
||||
|
||||
This cache supports setting a maximum size and expiration time for cached items.
|
||||
When the cache is full, it uses a Least Recently Used (LRU) eviction policy.
|
||||
Thread-safe using a threading Lock.
|
||||
|
||||
Attributes:
|
||||
max_size (int, optional): Maximum number of items to store in the cache.
|
||||
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||
|
||||
Example:
|
||||
|
||||
cache = InMemoryCache(max_size=3, expiration_time=5)
|
||||
|
||||
# setting cache values
|
||||
cache.set("a", 1)
|
||||
cache.set("b", 2)
|
||||
cache["c"] = 3
|
||||
|
||||
# getting cache values
|
||||
a = cache.get("a")
|
||||
b = cache["b"]
|
||||
"""
|
||||
|
||||
def __init__(self, max_size=None, expiration_time=60 * 60):
|
||||
"""
|
||||
Initialize a new InMemoryCache instance.
|
||||
|
||||
Args:
|
||||
max_size (int, optional): Maximum number of items to store in the cache.
|
||||
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||
"""
|
||||
self._cache = OrderedDict()
|
||||
self._lock = threading.Lock()
|
||||
self.max_size = max_size
|
||||
self.expiration_time = expiration_time
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
Retrieve an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to retrieve.
|
||||
|
||||
Returns:
|
||||
The value associated with the key, or None if the key is not found or the item has expired.
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._cache:
|
||||
item = self._cache.pop(key)
|
||||
if (
|
||||
self.expiration_time is None
|
||||
or time.time() - item["time"] < self.expiration_time
|
||||
):
|
||||
# Move the key to the end to make it recently used
|
||||
self._cache[key] = item
|
||||
return item["value"]
|
||||
else:
|
||||
self.delete(key)
|
||||
return None
|
||||
|
||||
def set(self, key, value):
|
||||
"""
|
||||
Add an item to the cache.
|
||||
|
||||
If the cache is full, the least recently used item is evicted.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._cache:
|
||||
# Remove existing key before re-inserting to update order
|
||||
self.delete(key)
|
||||
elif self.max_size and len(self._cache) >= self.max_size:
|
||||
# Remove least recently used item
|
||||
self._cache.popitem(last=False)
|
||||
self._cache[key] = {"value": value, "time": time.time()}
|
||||
|
||||
def get_or_set(self, key, value):
|
||||
"""
|
||||
Retrieve an item from the cache. If the item does not exist, set it with the provided value.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache if the item doesn't exist.
|
||||
|
||||
Returns:
|
||||
The cached value associated with the key.
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._cache:
|
||||
return self.get(key)
|
||||
self.set(key, value)
|
||||
return value
|
||||
|
||||
def delete(self, key):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to remove.
|
||||
"""
|
||||
# with self._lock:
|
||||
self._cache.pop(key, None)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear all items from the cache.
|
||||
"""
|
||||
with self._lock:
|
||||
self._cache.clear()
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Check if the key is in the cache."""
|
||||
return key in self._cache
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Retrieve an item from the cache using the square bracket notation."""
|
||||
return self.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Add an item to the cache using the square bracket notation."""
|
||||
self.set(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Remove an item from the cache using the square bracket notation."""
|
||||
self.delete(key)
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of items in the cache."""
|
||||
return len(self._cache)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation of the InMemoryCache instance."""
|
||||
return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})"
|
||||
429
src/backend/langflow/services/cache/manager.py
vendored
429
src/backend/langflow/services/cache/manager.py
vendored
|
|
@ -1,153 +1,336 @@
|
|||
from contextlib import contextmanager
|
||||
from typing import Any, Awaitable, Callable, List, Optional
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from langflow.services.base import Service
|
||||
|
||||
import pandas as pd
|
||||
from PIL import Image
|
||||
from langflow.services.cache.base import BaseCacheService
|
||||
|
||||
import pickle
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class Subject:
|
||||
"""Base class for implementing the observer pattern."""
|
||||
class InMemoryCache(BaseCacheService, Service):
|
||||
|
||||
def __init__(self):
|
||||
self.observers: List[Callable[[], None]] = []
|
||||
"""
|
||||
A simple in-memory cache using an OrderedDict.
|
||||
|
||||
def attach(self, observer: Callable[[], None]):
|
||||
"""Attach an observer to the subject."""
|
||||
self.observers.append(observer)
|
||||
This cache supports setting a maximum size and expiration time for cached items.
|
||||
When the cache is full, it uses a Least Recently Used (LRU) eviction policy.
|
||||
Thread-safe using a threading Lock.
|
||||
|
||||
def detach(self, observer: Callable[[], None]):
|
||||
"""Detach an observer from the subject."""
|
||||
self.observers.remove(observer)
|
||||
Attributes:
|
||||
max_size (int, optional): Maximum number of items to store in the cache.
|
||||
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||
|
||||
def notify(self):
|
||||
"""Notify all observers about an event."""
|
||||
for observer in self.observers:
|
||||
if observer is None:
|
||||
continue
|
||||
observer()
|
||||
Example:
|
||||
|
||||
cache = InMemoryCache(max_size=3, expiration_time=5)
|
||||
|
||||
class AsyncSubject:
|
||||
"""Base class for implementing the async observer pattern."""
|
||||
# setting cache values
|
||||
cache.set("a", 1)
|
||||
cache.set("b", 2)
|
||||
cache["c"] = 3
|
||||
|
||||
def __init__(self):
|
||||
self.observers: List[Callable[[], Awaitable]] = []
|
||||
# getting cache values
|
||||
a = cache.get("a")
|
||||
b = cache["b"]
|
||||
"""
|
||||
|
||||
def attach(self, observer: Callable[[], Awaitable]):
|
||||
"""Attach an observer to the subject."""
|
||||
self.observers.append(observer)
|
||||
|
||||
def detach(self, observer: Callable[[], Awaitable]):
|
||||
"""Detach an observer from the subject."""
|
||||
self.observers.remove(observer)
|
||||
|
||||
async def notify(self):
|
||||
"""Notify all observers about an event."""
|
||||
for observer in self.observers:
|
||||
if observer is None:
|
||||
continue
|
||||
await observer()
|
||||
|
||||
|
||||
class CacheManager(Subject, Service):
|
||||
"""Manages cache for different clients and notifies observers on changes."""
|
||||
|
||||
name = "cache_manager"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._cache = {}
|
||||
self.current_client_id = None
|
||||
self.current_cache = {}
|
||||
|
||||
@contextmanager
|
||||
def set_client_id(self, client_id: str):
|
||||
def __init__(self, max_size=None, expiration_time=60 * 60):
|
||||
"""
|
||||
Context manager to set the current client_id and associated cache.
|
||||
Initialize a new InMemoryCache instance.
|
||||
|
||||
Args:
|
||||
client_id (str): The client identifier.
|
||||
max_size (int, optional): Maximum number of items to store in the cache.
|
||||
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||
"""
|
||||
self._cache = OrderedDict()
|
||||
self._lock = threading.RLock()
|
||||
self.max_size = max_size
|
||||
self.expiration_time = expiration_time
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
Retrieve an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to retrieve.
|
||||
|
||||
Returns:
|
||||
The value associated with the key, or None if the key is not found or the item has expired.
|
||||
"""
|
||||
with self._lock:
|
||||
return self._get_without_lock(key)
|
||||
|
||||
def _get_without_lock(self, key):
|
||||
"""
|
||||
Retrieve an item from the cache without acquiring the lock.
|
||||
"""
|
||||
if item := self._cache.get(key):
|
||||
if (
|
||||
self.expiration_time is None
|
||||
or time.time() - item["time"] < self.expiration_time
|
||||
):
|
||||
# Move the key to the end to make it recently used
|
||||
self._cache.move_to_end(key)
|
||||
# Check if the value is pickled
|
||||
if isinstance(item["value"], bytes):
|
||||
value = pickle.loads(item["value"])
|
||||
else:
|
||||
value = item["value"]
|
||||
return value
|
||||
else:
|
||||
self.delete(key)
|
||||
return None
|
||||
|
||||
def set(self, key, value, pickle=False):
|
||||
"""
|
||||
Add an item to the cache.
|
||||
|
||||
If the cache is full, the least recently used item is evicted.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._cache:
|
||||
# Remove existing key before re-inserting to update order
|
||||
self.delete(key)
|
||||
elif self.max_size and len(self._cache) >= self.max_size:
|
||||
# Remove least recently used item
|
||||
self._cache.popitem(last=False)
|
||||
# pickle locally to mimic Redis
|
||||
if pickle:
|
||||
value = pickle.dumps(value)
|
||||
|
||||
self._cache[key] = {"value": value, "time": time.time()}
|
||||
|
||||
def upsert(self, key, value):
|
||||
"""
|
||||
Inserts or updates a value in the cache.
|
||||
If the existing value and the new value are both dictionaries, they are merged.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to insert or update.
|
||||
"""
|
||||
with self._lock:
|
||||
existing_value = self._get_without_lock(key)
|
||||
if (
|
||||
existing_value is not None
|
||||
and isinstance(existing_value, dict)
|
||||
and isinstance(value, dict)
|
||||
):
|
||||
existing_value.update(value)
|
||||
value = existing_value
|
||||
|
||||
self.set(key, value)
|
||||
|
||||
def get_or_set(self, key, value):
|
||||
"""
|
||||
Retrieve an item from the cache. If the item does not exist,
|
||||
set it with the provided value.
|
||||
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache if the item doesn't exist.
|
||||
|
||||
Returns:
|
||||
The cached value associated with the key.
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._cache:
|
||||
return self.get(key)
|
||||
self.set(key, value)
|
||||
return value
|
||||
|
||||
def delete(self, key):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to remove.
|
||||
"""
|
||||
with self._lock:
|
||||
self._cache.pop(key, None)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear all items from the cache.
|
||||
"""
|
||||
with self._lock:
|
||||
self._cache.clear()
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Check if the key is in the cache."""
|
||||
return key in self._cache
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Retrieve an item from the cache using the square bracket notation."""
|
||||
return self.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Add an item to the cache using the square bracket notation."""
|
||||
self.set(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Remove an item from the cache using the square bracket notation."""
|
||||
self.delete(key)
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of items in the cache."""
|
||||
return len(self._cache)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation of the InMemoryCache instance."""
|
||||
return f"InMemoryCache(max_size={self.max_size}, expiration_time={self.expiration_time})"
|
||||
|
||||
|
||||
class RedisCache(BaseCacheService, Service):
|
||||
"""
|
||||
A Redis-based cache implementation.
|
||||
|
||||
This cache supports setting an expiration time for cached items.
|
||||
|
||||
Attributes:
|
||||
expiration_time (int, optional): Time in seconds after which a cached item expires. Default is 1 hour.
|
||||
|
||||
Example:
|
||||
|
||||
cache = RedisCache(expiration_time=5)
|
||||
|
||||
# setting cache values
|
||||
cache.set("a", 1)
|
||||
cache.set("b", 2)
|
||||
cache["c"] = 3
|
||||
|
||||
# getting cache values
|
||||
a = cache.get("a")
|
||||
b = cache["b"]
|
||||
"""
|
||||
|
||||
def __init__(self, host="localhost", port=6379, db=0, expiration_time=60 * 60):
|
||||
"""
|
||||
Initialize a new RedisCache instance.
|
||||
|
||||
Args:
|
||||
host (str, optional): Redis host.
|
||||
port (int, optional): Redis port.
|
||||
db (int, optional): Redis DB.
|
||||
expiration_time (int, optional): Time in seconds after which a
|
||||
ached item expires. Default is 1 hour.
|
||||
"""
|
||||
previous_client_id = self.current_client_id
|
||||
self.current_client_id = client_id
|
||||
self.current_cache = self._cache.setdefault(client_id, {})
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.current_client_id = previous_client_id
|
||||
self.current_cache = self._cache.get(self.current_client_id, {})
|
||||
import redis
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"RedisCache requires the redis-py package."
|
||||
" Please install Langflow with the deploy extra: pip install langflow[deploy]"
|
||||
) from exc
|
||||
logger.warning(
|
||||
"RedisCache is an experimental feature and may not work as expected."
|
||||
" Please report any issues to our GitHub repository."
|
||||
)
|
||||
self._client = redis.StrictRedis(host=host, port=port, db=db)
|
||||
self.expiration_time = expiration_time
|
||||
|
||||
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
|
||||
# check connection
|
||||
def is_connected(self):
|
||||
"""
|
||||
Add an object to the current client's cache.
|
||||
Check if the Redis client is connected.
|
||||
"""
|
||||
import redis
|
||||
|
||||
try:
|
||||
self._client.ping()
|
||||
return True
|
||||
except redis.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
Retrieve an item from the cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
obj (Any): The object to cache.
|
||||
obj_type (str): The type of the object.
|
||||
"""
|
||||
object_extensions = {
|
||||
"image": "png",
|
||||
"pandas": "csv",
|
||||
}
|
||||
if obj_type in object_extensions:
|
||||
_extension = object_extensions[obj_type]
|
||||
else:
|
||||
_extension = type(obj).__name__.lower()
|
||||
self.current_cache[name] = {
|
||||
"obj": obj,
|
||||
"type": obj_type,
|
||||
"extension": extension or _extension,
|
||||
}
|
||||
self.notify()
|
||||
|
||||
def add_pandas(self, name: str, obj: Any):
|
||||
"""
|
||||
Add a pandas DataFrame or Series to the current client's cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
obj (Any): The pandas DataFrame or Series object.
|
||||
"""
|
||||
if isinstance(obj, (pd.DataFrame, pd.Series)):
|
||||
self.add(name, obj.to_csv(), "pandas", extension="csv")
|
||||
else:
|
||||
raise ValueError("Object is not a pandas DataFrame or Series")
|
||||
|
||||
def add_image(self, name: str, obj: Any, extension: str = "png"):
|
||||
"""
|
||||
Add a PIL Image to the current client's cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
obj (Any): The PIL Image object.
|
||||
"""
|
||||
if isinstance(obj, Image.Image):
|
||||
self.add(name, obj, "image", extension=extension)
|
||||
else:
|
||||
raise ValueError("Object is not a PIL Image")
|
||||
|
||||
def get(self, name: str):
|
||||
"""
|
||||
Get an object from the current client's cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
key: The key of the item to retrieve.
|
||||
|
||||
Returns:
|
||||
The cached object associated with the given cache key.
|
||||
The value associated with the key, or None if the key is not found.
|
||||
"""
|
||||
return self.current_cache[name]
|
||||
value = self._client.get(key)
|
||||
return pickle.loads(value) if value else None
|
||||
|
||||
def get_last(self):
|
||||
def set(self, key, value):
|
||||
"""
|
||||
Get the last added item in the current client's cache.
|
||||
Add an item to the cache.
|
||||
|
||||
Returns:
|
||||
The last added item in the cache.
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to cache.
|
||||
"""
|
||||
return list(self.current_cache.values())[-1]
|
||||
try:
|
||||
if pickled := pickle.dumps(value):
|
||||
result = self._client.setex(key, self.expiration_time, pickled)
|
||||
if not result:
|
||||
raise ValueError("RedisCache could not set the value.")
|
||||
except TypeError as exc:
|
||||
raise TypeError(
|
||||
"RedisCache only accepts values that can be pickled. "
|
||||
) from exc
|
||||
|
||||
def upsert(self, key, value):
|
||||
"""
|
||||
Inserts or updates a value in the cache.
|
||||
If the existing value and the new value are both dictionaries, they are merged.
|
||||
|
||||
cache_manager = CacheManager()
|
||||
Args:
|
||||
key: The key of the item.
|
||||
value: The value to insert or update.
|
||||
"""
|
||||
existing_value = self.get(key)
|
||||
if (
|
||||
existing_value is not None
|
||||
and isinstance(existing_value, dict)
|
||||
and isinstance(value, dict)
|
||||
):
|
||||
existing_value.update(value)
|
||||
value = existing_value
|
||||
|
||||
self.set(key, value)
|
||||
|
||||
def delete(self, key):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key of the item to remove.
|
||||
"""
|
||||
self._client.delete(key)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear all items from the cache.
|
||||
"""
|
||||
self._client.flushdb()
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Check if the key is in the cache."""
|
||||
return False if key is None else self._client.exists(key)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Retrieve an item from the cache using the square bracket notation."""
|
||||
return self.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Add an item to the cache using the square bracket notation."""
|
||||
self.set(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Remove an item from the cache using the square bracket notation."""
|
||||
self.delete(key)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return a string representation of the RedisCache instance."""
|
||||
return f"RedisCache(expiration_time={self.expiration_time})"
|
||||
|
|
|
|||
33
src/backend/langflow/services/cache/utils.py
vendored
33
src/backend/langflow/services/cache/utils.py
vendored
|
|
@ -6,10 +6,15 @@ import os
|
|||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
from appdirs import user_cache_dir
|
||||
from fastapi import UploadFile
|
||||
from langflow.api.v1.schemas import BuildStatus
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
CACHE: Dict[str, Any] = {}
|
||||
|
||||
CACHE_DIR = user_cache_dir("langflow", "langflow")
|
||||
|
|
@ -152,7 +157,7 @@ def save_binary_file(content: str, file_name: str, accepted_types: list[str]) ->
|
|||
|
||||
|
||||
@create_cache_folder
|
||||
def save_uploaded_file(file, folder_name):
|
||||
def save_uploaded_file(file: UploadFile, folder_name):
|
||||
"""
|
||||
Save an uploaded file to the specified folder with a hash of its content as the file name.
|
||||
|
||||
|
|
@ -165,6 +170,12 @@ def save_uploaded_file(file, folder_name):
|
|||
"""
|
||||
cache_path = Path(CACHE_DIR)
|
||||
folder_path = cache_path / folder_name
|
||||
filename = file.filename
|
||||
if isinstance(filename, str) or isinstance(filename, Path):
|
||||
file_extension = Path(filename).suffix
|
||||
else:
|
||||
file_extension = ""
|
||||
file_object = file.file
|
||||
|
||||
# Create the folder if it doesn't exist
|
||||
if not folder_path.exists():
|
||||
|
|
@ -173,22 +184,30 @@ def save_uploaded_file(file, folder_name):
|
|||
# Create a hash of the file content
|
||||
sha256_hash = hashlib.sha256()
|
||||
# Reset the file cursor to the beginning of the file
|
||||
file.seek(0)
|
||||
file_object.seek(0)
|
||||
# Iterate over the uploaded file in small chunks to conserve memory
|
||||
while chunk := file.read(8192): # Read 8KB at a time (adjust as needed)
|
||||
while chunk := file_object.read(8192): # Read 8KB at a time (adjust as needed)
|
||||
sha256_hash.update(chunk)
|
||||
|
||||
# Use the hex digest of the hash as the file name
|
||||
hex_dig = sha256_hash.hexdigest()
|
||||
file_name = hex_dig
|
||||
file_name = f"{hex_dig}{file_extension}"
|
||||
|
||||
# Reset the file cursor to the beginning of the file
|
||||
file.seek(0)
|
||||
file_object.seek(0)
|
||||
|
||||
# Save the file with the hash as its name
|
||||
file_path = folder_path / file_name
|
||||
with open(file_path, "wb") as new_file:
|
||||
while chunk := file.read(8192):
|
||||
while chunk := file_object.read(8192):
|
||||
new_file.write(chunk)
|
||||
|
||||
return file_path
|
||||
|
||||
|
||||
def update_build_status(cache_service, flow_id: str, status: BuildStatus):
|
||||
cached_flow = cache_service[flow_id]
|
||||
if cached_flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found in cache")
|
||||
cached_flow["status"] = status
|
||||
cache_service[flow_id] = cached_flow
|
||||
|
|
|
|||
153
src/backend/langflow/services/chat/cache.py
Normal file
153
src/backend/langflow/services/chat/cache.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
from contextlib import contextmanager
|
||||
from typing import Any, Awaitable, Callable, List, Optional
|
||||
from langflow.services.base import Service
|
||||
|
||||
import pandas as pd
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class Subject:
|
||||
"""Base class for implementing the observer pattern."""
|
||||
|
||||
def __init__(self):
|
||||
self.observers: List[Callable[[], None]] = []
|
||||
|
||||
def attach(self, observer: Callable[[], None]):
|
||||
"""Attach an observer to the subject."""
|
||||
self.observers.append(observer)
|
||||
|
||||
def detach(self, observer: Callable[[], None]):
|
||||
"""Detach an observer from the subject."""
|
||||
self.observers.remove(observer)
|
||||
|
||||
def notify(self):
|
||||
"""Notify all observers about an event."""
|
||||
for observer in self.observers:
|
||||
if observer is None:
|
||||
continue
|
||||
observer()
|
||||
|
||||
|
||||
class AsyncSubject:
|
||||
"""Base class for implementing the async observer pattern."""
|
||||
|
||||
def __init__(self):
|
||||
self.observers: List[Callable[[], Awaitable]] = []
|
||||
|
||||
def attach(self, observer: Callable[[], Awaitable]):
|
||||
"""Attach an observer to the subject."""
|
||||
self.observers.append(observer)
|
||||
|
||||
def detach(self, observer: Callable[[], Awaitable]):
|
||||
"""Detach an observer from the subject."""
|
||||
self.observers.remove(observer)
|
||||
|
||||
async def notify(self):
|
||||
"""Notify all observers about an event."""
|
||||
for observer in self.observers:
|
||||
if observer is None:
|
||||
continue
|
||||
await observer()
|
||||
|
||||
|
||||
class CacheService(Subject, Service):
|
||||
"""Manages cache for different clients and notifies observers on changes."""
|
||||
|
||||
name = "cache_service"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._cache = {}
|
||||
self.current_client_id = None
|
||||
self.current_cache = {}
|
||||
|
||||
@contextmanager
|
||||
def set_client_id(self, client_id: str):
|
||||
"""
|
||||
Context manager to set the current client_id and associated cache.
|
||||
|
||||
Args:
|
||||
client_id (str): The client identifier.
|
||||
"""
|
||||
previous_client_id = self.current_client_id
|
||||
self.current_client_id = client_id
|
||||
self.current_cache = self._cache.setdefault(client_id, {})
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.current_client_id = previous_client_id
|
||||
self.current_cache = self._cache.get(self.current_client_id, {})
|
||||
|
||||
def add(self, name: str, obj: Any, obj_type: str, extension: Optional[str] = None):
|
||||
"""
|
||||
Add an object to the current client's cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
obj (Any): The object to cache.
|
||||
obj_type (str): The type of the object.
|
||||
"""
|
||||
object_extensions = {
|
||||
"image": "png",
|
||||
"pandas": "csv",
|
||||
}
|
||||
if obj_type in object_extensions:
|
||||
_extension = object_extensions[obj_type]
|
||||
else:
|
||||
_extension = type(obj).__name__.lower()
|
||||
self.current_cache[name] = {
|
||||
"obj": obj,
|
||||
"type": obj_type,
|
||||
"extension": extension or _extension,
|
||||
}
|
||||
self.notify()
|
||||
|
||||
def add_pandas(self, name: str, obj: Any):
|
||||
"""
|
||||
Add a pandas DataFrame or Series to the current client's cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
obj (Any): The pandas DataFrame or Series object.
|
||||
"""
|
||||
if isinstance(obj, (pd.DataFrame, pd.Series)):
|
||||
self.add(name, obj.to_csv(), "pandas", extension="csv")
|
||||
else:
|
||||
raise ValueError("Object is not a pandas DataFrame or Series")
|
||||
|
||||
def add_image(self, name: str, obj: Any, extension: str = "png"):
|
||||
"""
|
||||
Add a PIL Image to the current client's cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
obj (Any): The PIL Image object.
|
||||
"""
|
||||
if isinstance(obj, Image.Image):
|
||||
self.add(name, obj, "image", extension=extension)
|
||||
else:
|
||||
raise ValueError("Object is not a PIL Image")
|
||||
|
||||
def get(self, name: str):
|
||||
"""
|
||||
Get an object from the current client's cache.
|
||||
|
||||
Args:
|
||||
name (str): The cache key.
|
||||
|
||||
Returns:
|
||||
The cached object associated with the given cache key.
|
||||
"""
|
||||
return self.current_cache[name]
|
||||
|
||||
def get_last(self):
|
||||
"""
|
||||
Get the last added item in the current client's cache.
|
||||
|
||||
Returns:
|
||||
The last added item in the cache.
|
||||
"""
|
||||
return list(self.current_cache.values())[-1]
|
||||
|
||||
|
||||
cache_service = CacheService()
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
from langflow.services.chat.manager import ChatManager
|
||||
from langflow.services.chat.manager import ChatService
|
||||
from langflow.services.factory import ServiceFactory
|
||||
|
||||
|
||||
class ChatManagerFactory(ServiceFactory):
|
||||
class ChatServiceFactory(ServiceFactory):
|
||||
def __init__(self):
|
||||
super().__init__(ChatManager)
|
||||
super().__init__(ChatService)
|
||||
|
||||
def create(self):
|
||||
# Here you would have logic to create and configure a ChatManager
|
||||
return ChatManager()
|
||||
# Here you would have logic to create and configure a ChatService
|
||||
return ChatService()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
from collections import defaultdict
|
||||
import uuid
|
||||
from fastapi import WebSocket, status
|
||||
from starlette.websockets import WebSocketState
|
||||
from langflow.api.v1.schemas import ChatMessage, ChatResponse, FileResponse
|
||||
from langflow.services.base import Service
|
||||
from langflow.services import service_manager
|
||||
from langflow.services.cache.manager import Subject
|
||||
from langflow.services.chat.utils import process_graph
|
||||
from langflow.interface.utils import pil_to_base64
|
||||
from langflow.services.schema import ServiceType
|
||||
from langflow.services.base import Service
|
||||
from langflow.services.chat.cache import Subject
|
||||
from langflow.services.chat.utils import process_graph
|
||||
from loguru import logger
|
||||
|
||||
|
||||
from .cache import cache_service
|
||||
import asyncio
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langflow.services.cache.flow import InMemoryCache
|
||||
from langflow.services import service_manager, ServiceType
|
||||
import orjson
|
||||
|
||||
|
||||
|
|
@ -44,19 +44,20 @@ class ChatHistory(Subject):
|
|||
self.history[client_id] = []
|
||||
|
||||
|
||||
class ChatManager(Service):
|
||||
name = "chat_manager"
|
||||
class ChatService(Service):
|
||||
name = "chat_service"
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: Dict[str, WebSocket] = {}
|
||||
self.connection_ids: Dict[str, str] = {}
|
||||
self.chat_history = ChatHistory()
|
||||
self.cache_manager = service_manager.get(ServiceType.CACHE_MANAGER)
|
||||
self.cache_manager.attach(self.update)
|
||||
self.in_memory_cache = InMemoryCache()
|
||||
self.chat_cache = cache_service
|
||||
self.chat_cache.attach(self.update)
|
||||
self.cache_service = service_manager.get(ServiceType.CACHE_SERVICE)
|
||||
|
||||
def on_chat_history_update(self):
|
||||
"""Send the last chat message to the client."""
|
||||
client_id = self.cache_manager.current_client_id
|
||||
client_id = self.chat_cache.current_client_id
|
||||
if client_id in self.active_connections:
|
||||
chat_response = self.chat_history.get_history(
|
||||
client_id, filter_messages=False
|
||||
|
|
@ -77,8 +78,8 @@ class ChatManager(Service):
|
|||
asyncio.run_coroutine_threadsafe(coroutine, loop)
|
||||
|
||||
def update(self):
|
||||
if self.cache_manager.current_client_id in self.active_connections:
|
||||
self.last_cached_object_dict = self.cache_manager.get_last()
|
||||
if self.chat_cache.current_client_id in self.active_connections:
|
||||
self.last_cached_object_dict = self.chat_cache.get_last()
|
||||
# Add a new ChatResponse with the data
|
||||
chat_response = FileResponse(
|
||||
message=None,
|
||||
|
|
@ -88,14 +89,18 @@ class ChatManager(Service):
|
|||
)
|
||||
|
||||
self.chat_history.add_message(
|
||||
self.cache_manager.current_client_id, chat_response
|
||||
self.chat_cache.current_client_id, chat_response
|
||||
)
|
||||
|
||||
async def connect(self, client_id: str, websocket: WebSocket):
|
||||
self.active_connections[client_id] = websocket
|
||||
# This is to avoid having multiple clients with the same id
|
||||
#! Temporary solution
|
||||
self.connection_ids[client_id] = f"{client_id}-{uuid.uuid4()}"
|
||||
|
||||
def disconnect(self, client_id: str):
|
||||
self.active_connections.pop(client_id, None)
|
||||
self.connection_ids.pop(client_id, None)
|
||||
|
||||
async def send_message(self, client_id: str, message: str):
|
||||
websocket = self.active_connections[client_id]
|
||||
|
|
@ -121,7 +126,8 @@ class ChatManager(Service):
|
|||
):
|
||||
# Process the graph data and chat message
|
||||
chat_inputs = payload.pop("inputs", {})
|
||||
chat_inputs = ChatMessage(message=chat_inputs)
|
||||
chatkey = payload.pop("chatKey", None)
|
||||
chat_inputs = ChatMessage(message=chat_inputs, chatKey=chatkey)
|
||||
self.chat_history.add_message(client_id, chat_inputs)
|
||||
|
||||
# graph_data = payload
|
||||
|
|
@ -136,8 +142,10 @@ class ChatManager(Service):
|
|||
result, intermediate_steps = await process_graph(
|
||||
langchain_object=langchain_object,
|
||||
chat_inputs=chat_inputs,
|
||||
websocket=self.active_connections[client_id],
|
||||
client_id=client_id,
|
||||
session_id=self.connection_ids[client_id],
|
||||
)
|
||||
self.set_cache(client_id, langchain_object)
|
||||
except Exception as e:
|
||||
# Log stack trace
|
||||
logger.exception(e)
|
||||
|
|
@ -173,9 +181,15 @@ class ChatManager(Service):
|
|||
"""
|
||||
Set the cache for a client.
|
||||
"""
|
||||
# client_id is the flow id but that already exists in the cache
|
||||
# so we need to change it to something else
|
||||
|
||||
self.in_memory_cache.set(client_id, langchain_object)
|
||||
return client_id in self.in_memory_cache
|
||||
result_dict = {
|
||||
"result": langchain_object,
|
||||
"type": type(langchain_object),
|
||||
}
|
||||
self.cache_service.upsert(client_id, result_dict)
|
||||
return client_id in self.cache_service
|
||||
|
||||
async def handle_websocket(self, client_id: str, websocket: WebSocket):
|
||||
await self.connect(client_id, websocket)
|
||||
|
|
@ -188,33 +202,45 @@ class ChatManager(Service):
|
|||
|
||||
while True:
|
||||
json_payload = await websocket.receive_json()
|
||||
try:
|
||||
if isinstance(json_payload, str):
|
||||
payload = orjson.loads(json_payload)
|
||||
except Exception:
|
||||
elif isinstance(json_payload, dict):
|
||||
payload = json_payload
|
||||
if "clear_history" in payload:
|
||||
if "clear_history" in payload and payload["clear_history"]:
|
||||
self.chat_history.history[client_id] = []
|
||||
continue
|
||||
|
||||
with self.cache_manager.set_client_id(client_id):
|
||||
langchain_object = self.in_memory_cache.get(client_id)
|
||||
await self.process_message(client_id, payload, langchain_object)
|
||||
with self.chat_cache.set_client_id(client_id):
|
||||
if langchain_object := self.cache_service.get(client_id).get(
|
||||
"result"
|
||||
):
|
||||
await self.process_message(client_id, payload, langchain_object)
|
||||
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Could not find a build result for client_id {client_id}"
|
||||
)
|
||||
except Exception as exc:
|
||||
# Handle any exceptions that might occur
|
||||
logger.error(f"Error handling websocket: {exc}")
|
||||
await self.close_connection(
|
||||
client_id=client_id,
|
||||
code=status.WS_1011_INTERNAL_ERROR,
|
||||
reason=str(exc)[:120],
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
logger.exception(f"Error handling websocket: {exc}")
|
||||
if websocket.client_state == WebSocketState.CONNECTED:
|
||||
await self.close_connection(
|
||||
client_id=client_id,
|
||||
code=status.WS_1000_NORMAL_CLOSURE,
|
||||
reason="Client disconnected",
|
||||
code=status.WS_1011_INTERNAL_ERROR,
|
||||
reason=str(exc)[:120],
|
||||
)
|
||||
elif websocket.client_state == WebSocketState.DISCONNECTED:
|
||||
self.disconnect(client_id)
|
||||
|
||||
finally:
|
||||
try:
|
||||
# first check if the connection is still open
|
||||
if websocket.client_state == WebSocketState.CONNECTED:
|
||||
await self.close_connection(
|
||||
client_id=client_id,
|
||||
code=status.WS_1000_NORMAL_CLOSURE,
|
||||
reason="Client disconnected",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error closing connection: {exc}")
|
||||
self.disconnect(client_id)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from fastapi import WebSocket
|
||||
from langflow.api.v1.schemas import ChatMessage
|
||||
from langflow.processing.base import get_result_and_steps
|
||||
from langflow.interface.utils import try_setting_streaming_options
|
||||
|
|
@ -8,9 +7,10 @@ from loguru import logger
|
|||
async def process_graph(
|
||||
langchain_object,
|
||||
chat_inputs: ChatMessage,
|
||||
websocket: WebSocket,
|
||||
client_id: str,
|
||||
session_id: str,
|
||||
):
|
||||
langchain_object = try_setting_streaming_options(langchain_object, websocket)
|
||||
langchain_object = try_setting_streaming_options(langchain_object)
|
||||
logger.debug("Loaded langchain object")
|
||||
|
||||
if langchain_object is None:
|
||||
|
|
@ -27,7 +27,10 @@ async def process_graph(
|
|||
|
||||
logger.debug("Generating result and thought")
|
||||
result, intermediate_steps = await get_result_and_steps(
|
||||
langchain_object, chat_inputs.message, websocket=websocket
|
||||
langchain_object,
|
||||
chat_inputs.message,
|
||||
client_id=client_id,
|
||||
session_id=session_id,
|
||||
)
|
||||
logger.debug("Generated result and intermediate_steps")
|
||||
return result, intermediate_steps
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
from typing import TYPE_CHECKING
|
||||
from langflow.services.database.manager import DatabaseManager
|
||||
from langflow.services.database.manager import DatabaseService
|
||||
from langflow.services.factory import ServiceFactory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.settings.manager import SettingsManager
|
||||
from langflow.services.settings.manager import SettingsService
|
||||
|
||||
|
||||
class DatabaseManagerFactory(ServiceFactory):
|
||||
class DatabaseServiceFactory(ServiceFactory):
|
||||
def __init__(self):
|
||||
super().__init__(DatabaseManager)
|
||||
super().__init__(DatabaseService)
|
||||
|
||||
def create(self, settings_manager: "SettingsManager"):
|
||||
# Here you would have logic to create and configure a DatabaseManager
|
||||
if not settings_manager.settings.DATABASE_URL:
|
||||
def create(self, settings_service: "SettingsService"):
|
||||
# Here you would have logic to create and configure a DatabaseService
|
||||
if not settings_service.settings.DATABASE_URL:
|
||||
raise ValueError("No database URL provided")
|
||||
return DatabaseManager(settings_manager.settings.DATABASE_URL)
|
||||
return DatabaseService(settings_service.settings.DATABASE_URL)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ from typing import TYPE_CHECKING
|
|||
from langflow.services.base import Service
|
||||
from langflow.services.database.models.user.crud import get_user_by_username
|
||||
from langflow.services.database.utils import Result, TableResults
|
||||
from langflow.services.utils import get_settings_manager
|
||||
from langflow.services.getters import get_settings_service
|
||||
from sqlalchemy import inspect
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlmodel import SQLModel, Session, create_engine
|
||||
from loguru import logger
|
||||
from alembic.config import Config
|
||||
|
|
@ -16,8 +17,8 @@ if TYPE_CHECKING:
|
|||
from sqlalchemy.engine import Engine
|
||||
|
||||
|
||||
class DatabaseManager(Service):
|
||||
name = "database_manager"
|
||||
class DatabaseService(Service):
|
||||
name = "database_service"
|
||||
|
||||
def __init__(self, database_url: str):
|
||||
self.database_url = database_url
|
||||
|
|
@ -30,10 +31,10 @@ class DatabaseManager(Service):
|
|||
|
||||
def _create_engine(self) -> "Engine":
|
||||
"""Create the engine for the database."""
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
if (
|
||||
settings_manager.settings.DATABASE_URL
|
||||
and settings_manager.settings.DATABASE_URL.startswith("sqlite")
|
||||
settings_service.settings.DATABASE_URL
|
||||
and settings_service.settings.DATABASE_URL.startswith("sqlite")
|
||||
):
|
||||
connect_args = {"check_same_thread": False}
|
||||
else:
|
||||
|
|
@ -58,6 +59,27 @@ class DatabaseManager(Service):
|
|||
with Session(self.engine) as session:
|
||||
yield session
|
||||
|
||||
def migrate_flows_if_auto_login(self):
|
||||
# if auto_login is enabled, we need to migrate the flows
|
||||
# to the default superuser if they don't have a user id
|
||||
# associated with them
|
||||
settings_service = get_settings_service()
|
||||
if settings_service.auth_settings.AUTO_LOGIN:
|
||||
with Session(self.engine) as session:
|
||||
flows = (
|
||||
session.query(models.Flow)
|
||||
.filter(models.Flow.user_id == None) # noqa
|
||||
.all()
|
||||
)
|
||||
if flows:
|
||||
logger.debug("Migrating flows to default superuser")
|
||||
username = settings_service.auth_settings.SUPERUSER
|
||||
user = get_user_by_username(session, username)
|
||||
for flow in flows:
|
||||
flow.user_id = user.id
|
||||
session.commit()
|
||||
logger.debug("Flows migrated successfully")
|
||||
|
||||
def check_schema_health(self) -> bool:
|
||||
inspector = inspect(self.engine)
|
||||
|
||||
|
|
@ -94,9 +116,7 @@ class DatabaseManager(Service):
|
|||
return True
|
||||
|
||||
def run_migrations(self):
|
||||
logger.info(
|
||||
f"Running DB migrations in {self.script_location} on {self.database_url}"
|
||||
)
|
||||
logger.info(f"Running DB migrations in {self.script_location}")
|
||||
alembic_cfg = Config()
|
||||
alembic_cfg.set_main_option("script_location", str(self.script_location))
|
||||
alembic_cfg.set_main_option("sqlalchemy.url", self.database_url)
|
||||
|
|
@ -107,12 +127,10 @@ class DatabaseManager(Service):
|
|||
# We will check that all models are in the database
|
||||
# and that the database is up to date with all columns
|
||||
sql_models = [models.Flow, models.User, models.ApiKey]
|
||||
results = []
|
||||
for sql_model in sql_models:
|
||||
results.append(
|
||||
TableResults(sql_model.__tablename__, self.check_table(sql_model))
|
||||
)
|
||||
return results
|
||||
return [
|
||||
TableResults(sql_model.__tablename__, self.check_table(sql_model))
|
||||
for sql_model in sql_models
|
||||
]
|
||||
|
||||
def check_table(self, model):
|
||||
results = []
|
||||
|
|
@ -137,19 +155,31 @@ class DatabaseManager(Service):
|
|||
return results
|
||||
|
||||
def create_db_and_tables(self):
|
||||
logger.debug("Creating database and tables")
|
||||
try:
|
||||
SQLModel.metadata.create_all(self.engine)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error creating database and tables: {exc}")
|
||||
raise RuntimeError("Error creating database and tables") from exc
|
||||
|
||||
# Now check if the table "flow" exists, if not, something went wrong
|
||||
# and we need to create the tables again.
|
||||
from sqlalchemy import inspect
|
||||
|
||||
inspector = inspect(self.engine)
|
||||
table_names = inspector.get_table_names()
|
||||
current_tables = ["flow", "user", "apikey"]
|
||||
|
||||
if table_names and all(table in table_names for table in current_tables):
|
||||
logger.debug("Database and tables already exist")
|
||||
return
|
||||
|
||||
logger.debug("Creating database and tables")
|
||||
|
||||
for table in SQLModel.metadata.sorted_tables:
|
||||
try:
|
||||
table.create(self.engine, checkfirst=True)
|
||||
except OperationalError as oe:
|
||||
logger.warning(
|
||||
f"Table {table} already exists, skipping. Exception: {oe}"
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error creating table {table}: {exc}")
|
||||
raise RuntimeError(f"Error creating table {table}") from exc
|
||||
|
||||
# Now check if the required tables exist, if not, something went wrong.
|
||||
inspector = inspect(self.engine)
|
||||
table_names = inspector.get_table_names()
|
||||
for table in current_tables:
|
||||
if table not in table_names:
|
||||
|
|
@ -164,12 +194,12 @@ class DatabaseManager(Service):
|
|||
def teardown(self):
|
||||
logger.debug("Tearing down database")
|
||||
try:
|
||||
settings_manager = get_settings_manager()
|
||||
settings_service = get_settings_service()
|
||||
# remove the default superuser if auto_login is enabled
|
||||
# using the FIRST_SUPERUSER to get the user
|
||||
if settings_manager.auth_settings.AUTO_LOGIN:
|
||||
# using the SUPERUSER to get the user
|
||||
if settings_service.auth_settings.AUTO_LOGIN:
|
||||
logger.debug("Removing default superuser")
|
||||
username = settings_manager.auth_settings.FIRST_SUPERUSER
|
||||
username = settings_service.auth_settings.SUPERUSER
|
||||
with Session(self.engine) as session:
|
||||
user = get_user_by_username(session, username)
|
||||
session.delete(user)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||
class ApiKeyBase(SQLModelSerializable):
|
||||
name: Optional[str] = Field(index=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
last_used_at: Optional[datetime] = Field(default=None)
|
||||
last_used_at: Optional[datetime] = Field(default=None, nullable=True)
|
||||
total_uses: int = Field(default=0)
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
|
|
@ -22,8 +22,11 @@ class ApiKey(ApiKeyBase, table=True):
|
|||
|
||||
api_key: str = Field(index=True, unique=True)
|
||||
# User relationship
|
||||
# Delete API keys when user is deleted
|
||||
user_id: UUID = Field(index=True, foreign_key="user.id")
|
||||
user: "User" = Relationship(back_populates="api_keys")
|
||||
user: "User" = Relationship(
|
||||
back_populates="api_keys",
|
||||
)
|
||||
|
||||
|
||||
class ApiKeyCreate(ApiKeyBase):
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ def create_api_key(
|
|||
session: Session, api_key_create: ApiKeyCreate, user_id: UUID
|
||||
) -> UnmaskedApiKeyRead:
|
||||
# Generate a random API key with 32 bytes of randomness
|
||||
generated_api_key = f"lf-{secrets.token_urlsafe(32)}"
|
||||
generated_api_key = f"sk-{secrets.token_urlsafe(32)}"
|
||||
|
||||
api_key = ApiKey(
|
||||
api_key=generated_api_key,
|
||||
|
|
@ -63,9 +63,15 @@ def check_key(session: Session, api_key: str) -> Optional[ApiKey]:
|
|||
|
||||
def update_total_uses(session, api_key: ApiKey):
|
||||
"""Update the total uses and last used at."""
|
||||
api_key.total_uses += 1
|
||||
api_key.last_used_at = datetime.datetime.now(datetime.timezone.utc)
|
||||
session.add(api_key)
|
||||
session.commit()
|
||||
session.refresh(api_key)
|
||||
return api_key
|
||||
# This is running in a separate thread to avoid slowing down the request
|
||||
# but session is not thread safe so we need to create a new session
|
||||
|
||||
with Session(session.get_bind()) as new_session:
|
||||
new_api_key = new_session.get(ApiKey, api_key.id)
|
||||
if new_api_key is None:
|
||||
raise ValueError("API Key not found")
|
||||
new_api_key.total_uses += 1
|
||||
new_api_key.last_used_at = datetime.datetime.now(datetime.timezone.utc)
|
||||
new_session.add(new_api_key)
|
||||
new_session.commit()
|
||||
return new_api_key
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from langflow.services.database.models.base import SQLModelSerializable
|
||||
from pydantic import validator
|
||||
|
||||
from sqlmodel import Field, JSON, Column, Relationship
|
||||
from uuid import UUID, uuid4
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
|
@ -13,7 +14,7 @@ if TYPE_CHECKING:
|
|||
class FlowBase(SQLModelSerializable):
|
||||
name: str = Field(index=True)
|
||||
description: Optional[str] = Field(index=True)
|
||||
data: Optional[Dict] = Field(default=None)
|
||||
data: Optional[Dict] = Field(default=None, nullable=True)
|
||||
|
||||
@validator("data")
|
||||
def validate_json(v):
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import Union
|
||||
from uuid import UUID
|
||||
from fastapi import Depends, HTTPException
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from langflow.services.database.models.user.user import User, UserUpdate
|
||||
from langflow.services.utils import get_session
|
||||
from langflow.services.getters import get_session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import Session
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
|
|
@ -20,20 +20,26 @@ def get_user_by_id(db: Session, id: UUID) -> Union[User, None]:
|
|||
|
||||
|
||||
def update_user(
|
||||
user_id: UUID, user: UserUpdate, db: Session = Depends(get_session)
|
||||
user_db: Optional[User], user: UserUpdate, db: Session = Depends(get_session)
|
||||
) -> User:
|
||||
user_db = get_user_by_id(db, user_id)
|
||||
if not user_db:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user_db_by_username = get_user_by_username(db, user.username) # type: ignore
|
||||
if user_db_by_username and user_db_by_username.id != user_id:
|
||||
raise HTTPException(status_code=409, detail="Username already exists")
|
||||
# user_db_by_username = get_user_by_username(db, user.username) # type: ignore
|
||||
# if user_db_by_username and user_db_by_username.id != user_id:
|
||||
# raise HTTPException(status_code=409, detail="Username already exists")
|
||||
|
||||
user_data = user.dict(exclude_unset=True)
|
||||
changed = False
|
||||
for attr, value in user_data.items():
|
||||
if hasattr(user_db, attr) and value is not None:
|
||||
setattr(user_db, attr, value)
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_304_NOT_MODIFIED, detail="Nothing to update"
|
||||
)
|
||||
|
||||
user_db.updated_at = datetime.now(timezone.utc)
|
||||
flag_modified(user_db, "updated_at")
|
||||
|
|
@ -49,5 +55,5 @@ def update_user(
|
|||
|
||||
def update_user_last_login_at(user_id: UUID, db: Session = Depends(get_session)):
|
||||
user_data = UserUpdate(last_login_at=datetime.now(timezone.utc)) # type: ignore
|
||||
|
||||
return update_user(user_id, user_data, db)
|
||||
user = get_user_by_id(db, user_id)
|
||||
return update_user(user, user_data, db)
|
||||
|
|
|
|||
|
|
@ -15,12 +15,16 @@ class User(SQLModelSerializable, table=True):
|
|||
id: UUID = Field(default_factory=uuid4, primary_key=True, unique=True)
|
||||
username: str = Field(index=True, unique=True)
|
||||
password: str = Field()
|
||||
profile_image: Optional[str] = Field(default=None, nullable=True)
|
||||
is_active: bool = Field(default=False)
|
||||
is_superuser: bool = Field(default=False)
|
||||
create_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
api_keys: list["ApiKey"] = Relationship(back_populates="user")
|
||||
api_keys: list["ApiKey"] = Relationship(
|
||||
back_populates="user",
|
||||
sa_relationship_kwargs={"cascade": "delete"},
|
||||
)
|
||||
flows: list["Flow"] = Relationship(back_populates="user")
|
||||
|
||||
|
||||
|
|
@ -32,6 +36,7 @@ class UserCreate(SQLModel):
|
|||
class UserRead(SQLModel):
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
username: str = Field()
|
||||
profile_image: Optional[str] = Field()
|
||||
is_active: bool = Field()
|
||||
is_superuser: bool = Field()
|
||||
create_at: datetime = Field()
|
||||
|
|
@ -41,6 +46,8 @@ class UserRead(SQLModel):
|
|||
|
||||
class UserUpdate(SQLModel):
|
||||
username: Optional[str] = Field()
|
||||
profile_image: Optional[str] = Field()
|
||||
password: Optional[str] = Field()
|
||||
is_active: Optional[bool] = Field()
|
||||
is_superuser: Optional[bool] = Field()
|
||||
last_login_at: Optional[datetime] = Field()
|
||||
|
|
|
|||
|
|
@ -6,21 +6,31 @@ from alembic.util.exc import CommandError
|
|||
from sqlmodel import Session
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.manager import DatabaseManager
|
||||
from langflow.services.database.manager import DatabaseService
|
||||
|
||||
|
||||
def initialize_database():
|
||||
logger.debug("Initializing database")
|
||||
from langflow.services import service_manager, ServiceType
|
||||
|
||||
database_manager = service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||
database_service: "DatabaseService" = service_manager.get(
|
||||
ServiceType.DATABASE_SERVICE
|
||||
)
|
||||
try:
|
||||
database_manager.check_schema_health()
|
||||
database_service.create_db_and_tables()
|
||||
except Exception as exc:
|
||||
# if the exception involves tables already existing
|
||||
# we can ignore it
|
||||
if "already exists" not in str(exc):
|
||||
logger.error(f"Error creating DB and tables: {exc}")
|
||||
raise RuntimeError("Error creating DB and tables") from exc
|
||||
try:
|
||||
database_service.check_schema_health()
|
||||
except Exception as exc:
|
||||
logger.error(f"Error checking schema health: {exc}")
|
||||
raise RuntimeError("Error checking schema health") from exc
|
||||
try:
|
||||
database_manager.run_migrations()
|
||||
database_service.run_migrations()
|
||||
except CommandError as exc:
|
||||
if "Can't locate revision identified by" not in str(exc):
|
||||
raise exc
|
||||
|
|
@ -30,23 +40,22 @@ def initialize_database():
|
|||
logger.warning(
|
||||
"Wrong revision in DB, deleting alembic_version table and running migrations again"
|
||||
)
|
||||
with session_getter(database_manager) as session:
|
||||
with session_getter(database_service) as session:
|
||||
session.execute("DROP TABLE alembic_version")
|
||||
database_manager.run_migrations()
|
||||
database_service.run_migrations()
|
||||
except Exception as exc:
|
||||
# if the exception involves tables already existing
|
||||
# we can ignore it
|
||||
if "already exists" not in str(exc):
|
||||
logger.error(f"Error running migrations: {exc}")
|
||||
raise RuntimeError("Error running migrations") from exc
|
||||
database_manager.create_db_and_tables()
|
||||
logger.debug("Database initialized")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session_getter(db_manager: "DatabaseManager"):
|
||||
def session_getter(db_service: "DatabaseService"):
|
||||
try:
|
||||
session = Session(db_manager.engine)
|
||||
session = Session(db_service.engine)
|
||||
yield session
|
||||
except Exception as e:
|
||||
print("Session rollback because of exception:", e)
|
||||
|
|
|
|||
48
src/backend/langflow/services/getters.py
Normal file
48
src/backend/langflow/services/getters.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from langflow.services import ServiceType, service_manager
|
||||
from typing import TYPE_CHECKING, Generator
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.manager import DatabaseService
|
||||
from langflow.services.settings.manager import SettingsService
|
||||
from langflow.services.cache.manager import BaseCacheService
|
||||
from langflow.services.session.manager import SessionService
|
||||
from langflow.services.task.manager import TaskService
|
||||
from langflow.services.chat.manager import ChatService
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
def get_settings_service() -> "SettingsService":
|
||||
try:
|
||||
return service_manager.get(ServiceType.SETTINGS_SERVICE)
|
||||
except ValueError:
|
||||
# initialize settings service
|
||||
from langflow.services.manager import initialize_settings_service
|
||||
|
||||
initialize_settings_service()
|
||||
return service_manager.get(ServiceType.SETTINGS_SERVICE)
|
||||
|
||||
|
||||
def get_db_service() -> "DatabaseService":
|
||||
return service_manager.get(ServiceType.DATABASE_SERVICE)
|
||||
|
||||
|
||||
def get_session() -> Generator["Session", None, None]:
|
||||
db_service = service_manager.get(ServiceType.DATABASE_SERVICE)
|
||||
yield from db_service.get_session()
|
||||
|
||||
|
||||
def get_cache_service() -> "BaseCacheService":
|
||||
return service_manager.get(ServiceType.CACHE_SERVICE)
|
||||
|
||||
|
||||
def get_session_service() -> "SessionService":
|
||||
return service_manager.get(ServiceType.SESSION_SERVICE)
|
||||
|
||||
|
||||
def get_task_service() -> "TaskService":
|
||||
return service_manager.get(ServiceType.TASK_SERVICE)
|
||||
|
||||
|
||||
def get_chat_service() -> "ChatService":
|
||||
return service_manager.get(ServiceType.CHAT_SERVICE)
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
from langflow.services.schema import ServiceType
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
from loguru import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.factory import ServiceFactory
|
||||
from langflow.services.base import Service
|
||||
|
||||
|
||||
class ServiceManager:
|
||||
|
|
@ -12,7 +13,7 @@ class ServiceManager:
|
|||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.services = {}
|
||||
self.services: Dict[str, "Service"] = {}
|
||||
self.factories = {}
|
||||
self.dependencies = {}
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ class ServiceManager:
|
|||
self.services[service_name] = self.factories[service_name].create(
|
||||
**dependent_services
|
||||
)
|
||||
self.services[service_name].set_ready()
|
||||
|
||||
def _validate_service_creation(self, service_name: ServiceType):
|
||||
"""
|
||||
|
|
@ -85,8 +87,13 @@ class ServiceManager:
|
|||
Teardown all the services.
|
||||
"""
|
||||
for service in self.services.values():
|
||||
if service is None:
|
||||
continue
|
||||
logger.debug(f"Teardown service {service.name}")
|
||||
service.teardown()
|
||||
try:
|
||||
service.teardown()
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
self.services = {}
|
||||
self.factories = {}
|
||||
self.dependencies = {}
|
||||
|
|
@ -95,63 +102,53 @@ class ServiceManager:
|
|||
service_manager = ServiceManager()
|
||||
|
||||
|
||||
def initialize_services():
|
||||
def reinitialize_services():
|
||||
"""
|
||||
Initialize all the services needed.
|
||||
Reinitialize all the services needed.
|
||||
"""
|
||||
from langflow.services.database import factory as database_factory
|
||||
from langflow.services.cache import factory as cache_factory
|
||||
from langflow.services.chat import factory as chat_factory
|
||||
from langflow.services.settings import factory as settings_factory
|
||||
from langflow.services.auth import factory as auth_factory
|
||||
|
||||
service_manager.register_factory(settings_factory.SettingsManagerFactory())
|
||||
service_manager.register_factory(
|
||||
auth_factory.AuthManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER]
|
||||
)
|
||||
service_manager.register_factory(
|
||||
database_factory.DatabaseManagerFactory(),
|
||||
dependencies=[ServiceType.SETTINGS_MANAGER],
|
||||
)
|
||||
service_manager.register_factory(cache_factory.CacheManagerFactory())
|
||||
service_manager.register_factory(chat_factory.ChatManagerFactory())
|
||||
service_manager.update(ServiceType.SETTINGS_SERVICE)
|
||||
service_manager.update(ServiceType.DATABASE_SERVICE)
|
||||
service_manager.update(ServiceType.CACHE_SERVICE)
|
||||
service_manager.update(ServiceType.CHAT_SERVICE)
|
||||
service_manager.update(ServiceType.SESSION_SERVICE)
|
||||
service_manager.update(ServiceType.AUTH_SERVICE)
|
||||
service_manager.update(ServiceType.TASK_SERVICE)
|
||||
|
||||
# Test cache connection
|
||||
service_manager.get(ServiceType.CACHE_MANAGER)
|
||||
service_manager.get(ServiceType.CACHE_SERVICE)
|
||||
# Test database connection
|
||||
service_manager.get(ServiceType.DATABASE_MANAGER)
|
||||
service_manager.get(ServiceType.DATABASE_SERVICE)
|
||||
|
||||
# Test cache connection
|
||||
service_manager.get(ServiceType.CACHE_SERVICE)
|
||||
# Test database connection
|
||||
service_manager.get(ServiceType.DATABASE_SERVICE)
|
||||
|
||||
|
||||
def initialize_settings_manager():
|
||||
def initialize_settings_service():
|
||||
"""
|
||||
Initialize the settings manager.
|
||||
"""
|
||||
from langflow.services.settings import factory as settings_factory
|
||||
|
||||
service_manager.register_factory(settings_factory.SettingsManagerFactory())
|
||||
service_manager.register_factory(settings_factory.SettingsServiceFactory())
|
||||
|
||||
|
||||
def initialize_session_manager():
|
||||
def initialize_session_service():
|
||||
"""
|
||||
Initialize the session manager.
|
||||
"""
|
||||
from langflow.services.session import factory as session_manager_factory # type: ignore
|
||||
from langflow.services.session import factory as session_service_factory # type: ignore
|
||||
from langflow.services.cache import factory as cache_factory
|
||||
|
||||
initialize_settings_manager()
|
||||
initialize_settings_service()
|
||||
|
||||
service_manager.register_factory(
|
||||
cache_factory.CacheManagerFactory(), dependencies=[ServiceType.SETTINGS_MANAGER]
|
||||
cache_factory.CacheServiceFactory(), dependencies=[ServiceType.SETTINGS_SERVICE]
|
||||
)
|
||||
|
||||
service_manager.register_factory(
|
||||
session_manager_factory.SessionManagerFactory(),
|
||||
dependencies=[ServiceType.CACHE_MANAGER],
|
||||
session_service_factory.SessionServiceFactory(),
|
||||
dependencies=[ServiceType.CACHE_SERVICE],
|
||||
)
|
||||
|
||||
|
||||
def teardown_services():
|
||||
"""
|
||||
Teardown all the services.
|
||||
"""
|
||||
service_manager.teardown()
|
||||
|
|
|
|||
0
src/backend/langflow/services/plugins/__init__.py
Normal file
0
src/backend/langflow/services/plugins/__init__.py
Normal file
54
src/backend/langflow/services/plugins/langfuse.py
Normal file
54
src/backend/langflow/services/plugins/langfuse.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from langflow.services.getters import get_settings_service
|
||||
from langflow.utils.logger import logger
|
||||
|
||||
### Temporary implementation
|
||||
# This will be replaced by a plugin system once merged into 0.5.0
|
||||
|
||||
|
||||
class LangfuseInstance:
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def get(cls):
|
||||
logger.debug("Getting Langfuse instance")
|
||||
if cls._instance is None:
|
||||
cls.create()
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def create(cls):
|
||||
try:
|
||||
logger.debug("Creating Langfuse instance")
|
||||
from langfuse import Langfuse # type: ignore
|
||||
|
||||
settings_manager = get_settings_service()
|
||||
|
||||
if (
|
||||
settings_manager.settings.LANGFUSE_PUBLIC_KEY
|
||||
and settings_manager.settings.LANGFUSE_SECRET_KEY
|
||||
):
|
||||
logger.debug("Langfuse credentials found")
|
||||
cls._instance = Langfuse(
|
||||
public_key=settings_manager.settings.LANGFUSE_PUBLIC_KEY,
|
||||
secret_key=settings_manager.settings.LANGFUSE_SECRET_KEY,
|
||||
host=settings_manager.settings.LANGFUSE_HOST,
|
||||
)
|
||||
else:
|
||||
logger.debug("No Langfuse credentials found")
|
||||
cls._instance = None
|
||||
except ImportError:
|
||||
logger.debug("Langfuse not installed")
|
||||
cls._instance = None
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
logger.debug("Updating Langfuse instance")
|
||||
cls._instance = None
|
||||
cls.create()
|
||||
|
||||
@classmethod
|
||||
def teardown(cls):
|
||||
logger.debug("Tearing down Langfuse instance")
|
||||
if cls._instance is not None:
|
||||
cls._instance.flush()
|
||||
cls._instance = None
|
||||
|
|
@ -7,8 +7,10 @@ class ServiceType(str, Enum):
|
|||
registered with the service manager.
|
||||
"""
|
||||
|
||||
AUTH_MANAGER = "auth_manager"
|
||||
CACHE_MANAGER = "cache_manager"
|
||||
SETTINGS_MANAGER = "settings_manager"
|
||||
DATABASE_MANAGER = "database_manager"
|
||||
CHAT_MANAGER = "chat_manager"
|
||||
AUTH_SERVICE = "auth_service"
|
||||
CACHE_SERVICE = "cache_service"
|
||||
SETTINGS_SERVICE = "settings_service"
|
||||
DATABASE_SERVICE = "database_service"
|
||||
CHAT_SERVICE = "chat_service"
|
||||
SESSION_SERVICE = "session_service"
|
||||
TASK_SERVICE = "task_service"
|
||||
|
|
|
|||
0
src/backend/langflow/services/session/__init__.py
Normal file
0
src/backend/langflow/services/session/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue