1.0 Alpha (#1599)
* Update model kwargs and temperature values
* Update keyboard shortcuts for advanced editing
* make Message field have no handles
* Update OpenAI API Key handling in OpenAIEmbeddingsComponent
* Remove unnecessary field_type key from CustomComponent class
* Update required field behavior in CustomComponent class
* Refactor AzureOpenAIModel.py: Removed unnecessary "required" attribute from input parameters
* Update BaiduQianfanChatModel and OpenAIModel configurations
* Fix range_spec step type validation
* Update RangeSpec step_type default value to "float"
* Fix Save debounce
* Update parameterUtils to use debounce instead of throttle
* Update input type options in schemas and graph base classes
* Refactor run_flow_with_caching endpoint to include simplified and experimental versions
* Add PythonFunctionComponent and test case for it
* Add nest_asyncio to fix event loop issue
* Refactor test_initial_setup.py to use RunOutputs instead of ResultData
* Remove unused code in test_endpoints.py
* Add asyncio loop to uvicorn command
* Refactor load_session method to handle coroutine result
* Fixed saving
* Fixed debouncing
* Add InputType and OutputType literals to schema.py
* Update input type in Graph class
* Add new schema for simplified API request
* Add delete_messages function and update test_successful_run assertions
* Add STREAM_INFO_TEXT constant to model components
* Add session_id to simplified_run_flow_with_caching endpoint
* Add field_typing import to OpenAIModel.py
* update starter projects
* Add constants for Langflow base module
* Update setup.py to include latest component versions
* Update Starter Examples
* sets starter_project fixture to Basic Prompting
* Refactor test_endpoints.py: Update test names and add new tests for different output types
* Update HuggingFace Spaces link and add image for dark mode
* Remove filepath reference
* Update Vertex params in base.py
* Add tests for different input types
* Add type annotations and improve test coverage
* Add duplicate space link to README
* Update HuggingFace Spaces badge in README
* Add Python 3.10 installation requirement to README
* Refactor flow running endpoints
* Refactor SimplifiedAPIRequest and add documentation for Tweaks
* Refactor input_request parameter in simplified_run_flow function
* Add support for retrieving specific component output
* Add custom Uvicorn worker for Langflow application
* Add asyncio loop to LangflowApplication initialization
* Update Makefile with new variables and start command
* Fix indentation in Makefile
* Refactor run_graph function to add support for running a JSON flow
* Refactor getChatInputField function and update API code
* Update HuggingFace Spaces documentation with duplication process
* Add asyncio event loop to uvicorn command
* Add installation of backend in start target
* udpate some starter projects
* Fix formatting in hugging-face-spaces.mdx
* Update installation instructions for Langflow
* set examples order
* Update start command in Makefile
* Add installation and usage instructions for Langflow
* Update Langflow installation and usage instructions
* Fix langflow command in README.md
* Fix broken link to HuggingFace Spaces guide
* Add new SVG assets for blog post, chat bot, and cloud docs
* Refactor example rendering in NewFlowModal
* Add new SVG file for short bio section
* Remove unused import and add new component
* Update title in usage.mdx
* Update HuggingFace Spaces heading in usage.mdx
* Update usage instructions in getting-started/usage.mdx
* Update cache option in usage documentation
* Remove 'advanced' flag from 'n_messages' parameter in MemoryComponent.py
* Refactor code to improve performance and readability
* Update project names and flow examples
* fix document qa example
* Remove commented out code in sidebars.js
* Delete unused documentation files
* Fix bug in login functionality
* Remove global variables from components
* Fix bug in login functionality
* fix modal returning to input
* Update max-width of chat message sender name
* Update styling for chat message component
* Refactor OpenAIEmbeddingsComponent signature
* Update usage.mdx file
* Update path in Makefile
* Add new migration and what's new documentation files
* Add new chapters and migration guides
* Update version to 0.0.13 in pyproject.toml
* new locks
* Update dependencies in pyproject.toml
* general fixes
* Update dependencies in pyproject.toml and poetry.lock files
* add padding to modal
* ✨ (undrawCards/index.tsx): update the SVG used for BasicPrompt component to undraw_short_bio_re_fmx0.svg to match the desired design
♻️ (undrawCards/index.tsx): adjust the width and height of the BasicPrompt SVG to 65% to improve the visual appearance
* Commented out components/data in sidebars.js
* Refactor component names in outputs.mdx
* Update embedded chat script URL
* Add data component and fix formatting in outputs component
* Update dependencies in poetry.lock and pyproject.toml
* Update dependencies in poetry.lock and pyproject.toml
* Refactor code to improve performance and readability
* Update dependencies in poetry.lock and pyproject.toml
* Fixed IO Modal updates
* Remove dead code at API Modal
* Fixed overflow at CodeTabsComponent tweaks page
* ✨ (NewFlowModal/index.tsx): update the name of the example from "Blog Writter" to "Blog Writer" for better consistency and clarity
* Update dependencies versions
* Update langflow-base to version 0.0.15 and fix setup_env script
* Update dependencies in pyproject.toml
* Lock dependencies in parallel
* Add logging statement to setup_app function
* Fix Ace not having type="module" and breaking build
* Update authentication settings for access token cookie
* Update package versions in package-lock.json
* Add scripts directory to Dockerfile
* Add setup_env command to build_and_run target
* Remove unnecessary make command in setup_env
* Remove unnecessary installation step in build_and_run
* Add debug configuration for CLI
* 🔧 chore(Makefile): refactor build_langflow target to use a separate script for updating dependencies and building
✨ feat(update_dependencies.py): add script to update pyproject.toml dependency version based on langflow-base version in src/backend/base/pyproject.toml
* Add number_of_results parameter to AstraDBSearchComponent
* Update HuggingFace Spaces links
* Remove duplicate imports in hugging-face-spaces.mdx
* Add number_of_results parameter to vector search components
* Fixed supabase not commited
* Revert "Fixed supabase not commited"
This reverts commit afb10a6262.
* Update duplicate-space.png image
* Delete unused files and components
* Add/update script to update dependencies
* Add .bak files to .gitignore
* Update version numbers and remove unnecessary dependencies
* Update langflow-base dependency path
* Add Text import to VertexAiModel.py
* Update langflow-base version to 0.0.16 and update dependencies
* Delete start projects and commit session in delete_start_projects function
* Refactor backend startup script to handle autologin option
* Update poetry installation script to include pipx update check
* Update pipx installation script for different operating systems
* Update Makefile to improve setup process
* Add error handling on streaming and fix streaming bug on error
* Added description to Blog Writer
* Sort base classes alphabetically
* Update duplicate-space.png image
* update position on langflow prompt chaining
* Add Langflow CLI and first steps documentation
* Add exception handling for missing 'content' field in search_with_vector_store method
* Remove unused import and update type hinting
* fix bug on egdes after creating group component
* Refactor APIRequest class and update model imports
* Remove unused imports and fix formatting issues
* Refactor reactflowUtils and styleUtils
* Add CLI documentation to getting-started/cli.mdx
* Add CLI usage instructions
* Add ZoomableImage component to first-steps.mdx
* Update CLI and first steps documentation
* Remove duplicate import and add new imports for ThemedImage and useBaseUrl
* Update Langflow CLI documentation link
* Remove first-steps.mdx and update index.mdx and sidebars.js
* Update Docusaurus dependencies
* Add AstraDB RAG Flow guide
* Remove unused imports
* Remove unnecessary import statement
* Refactor guide for better readability
* Add data component documentation
* Update component headings and add prompt template
* Fix logging level and version display
* Add datetime import and buffer for alembic log
* Update flow names in NewFlowModal and documentation
* Add starter projects to sidebars.js
* Fix error handling in DirectoryReader class
* Handle exception when loading components in setup.py
* Update version numbers in pyproject.toml files
* Update build_langflow_base and build_langflow_backup in Makefile
* Added docs
* Update dependencies and build process
* Add Admonition component for API Key documentation
* Update API endpoint in async-api.mdx
* Remove async-api guidelines
* Fix UnicodeDecodeError in DirectoryReader
* Update dependency version and fix encoding issues
* Add conditional build and publish for base and main projects
* Update version to 1.0.0a2 in pyproject.toml
* Remove duplicate imports and unnecessary code in custom-component.mdx
* Fix poetry lock command in Makefile
* Update package versions in pyproject.toml
* Remove unused components and update imports
* 📦 chore(pre-release-base.yml): add pre-release workflow for base project
📦 chore(pre-release-langflow.yml): add pre-release workflow for langflow project
* Add ChatLiteLLMModelComponent to models package
* Add frontend installation and build steps
* Add Dockerfile for building and pushing base image
* Add emoji package and nest-asyncio dependency
* 📝 (components.mdx): update margin style of ZoomableImage to improve spacing
📝 (features.mdx): update margin style of ZoomableImage to improve spacing
📝 (login.mdx): update margin style of ZoomableImage to improve spacing
* Fix module import error in validate.py
* Fix error message in directory_reader.py
* Update version import and handle ImportError
* Add cryptography and langchain-openai dependencies
* Update poetry installation and remove poetry-monorepo-dependency-plugin
* Update workflow and Dockerfile for Langflow base pre-release
* Update display names and descriptions for AstraDB components
* Update installation instructions for Langflow
* Update Astra DB links and remove unnecessary imports
* Rename AstraDB
* Add new components and images
* Update HuggingFace Spaces URLs
* Update Langflow documentation and add new starter projects
* Update flow name to "Basic Prompting (Hello, world!)" in relevant files
* Update Basic Prompting flow name to "Ahoy World!"
* Remove HuggingFace Spaces documentation
* Add new files and update sidebars.js
* Remove async-tasks.mdx and update sidebars.js
* Update starter project URLs
* Enable migration of global variables
* Update OpenAIEmbeddings deployment and model
* 📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (rag-with-astradb.mdx): add margin to image styles to improve spacing and readability
* Update welcome message in index.mdx
* Add global variable feature to Langflow documentation
* Reorganized sidebar categories
* Update migration documentation
* Refactor SplitTextComponent class to accept inputs of type Record and Text
* Adjust embeddings docs
* ✨ (cardComponent/index.tsx): add a minimum height to the card component to ensure consistent layout and prevent content from overlapping when the card is empty or has minimal content
* Update flow name from "Ahoy World!" to "Hello, world!"
* Update documentation for embeddings, models, and vector stores
* Update CreateRecordComponent and parameterUtils.ts
* Add documentation for Text and Record types
* Remove commented lines in sidebars.js
* Add run_flow_from_json function to load.py
* Update Langflow package to run flow from JSON file
* Fix type annotations and import errors
* Refactor tests and fix test data
---------
Co-authored-by: Rodrigo Nader <rodrigosilvanader@gmail.com>
Co-authored-by: anovazzi1 <otavio2204@gmail.com>
Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
Co-authored-by: carlosrcoelho <carlosrodrigo.coelho@gmail.com>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
Co-authored-by: Matheus <jacquesmats@gmail.com>
This commit is contained in:
parent
20f640c4ce
commit
05cd6e4fd7
853 changed files with 59936 additions and 15456 deletions
|
|
@ -1,18 +1,20 @@
|
|||
import platform
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from dotenv import load_dotenv
|
||||
from multiprocess import cpu_count # type: ignore
|
||||
from multiprocess import Process, cpu_count # type: ignore
|
||||
from rich import box
|
||||
from rich import print as rprint
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from sqlmodel import select
|
||||
|
||||
from langflow.main import setup_app
|
||||
from langflow.services.database.utils import session_getter
|
||||
|
|
@ -71,7 +73,7 @@ def update_settings(
|
|||
dev: bool = False,
|
||||
remove_api_keys: bool = False,
|
||||
components_path: Optional[Path] = None,
|
||||
store: bool = False,
|
||||
store: bool = True,
|
||||
):
|
||||
"""Update the settings from a config file."""
|
||||
|
||||
|
|
@ -95,33 +97,6 @@ def update_settings(
|
|||
settings_service.settings.update_settings(STORE=False)
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
"""
|
||||
Show the version and exit.
|
||||
"""
|
||||
from langflow import __version__
|
||||
|
||||
if value:
|
||||
typer.echo(f"Langflow Version: {__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main_entry_point(
|
||||
version: bool = typer.Option(
|
||||
None,
|
||||
"--version",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
help="Show the version and exit.",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Main entry point for the Langflow CLI.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@app.command()
|
||||
def run(
|
||||
host: str = typer.Option("127.0.0.1", help="Host to bind the server to.", envvar="LANGFLOW_HOST"),
|
||||
|
|
@ -174,13 +149,13 @@ def run(
|
|||
Run the Langflow.
|
||||
"""
|
||||
|
||||
configure(log_level=log_level, log_file=log_file)
|
||||
set_var_for_macos_issue()
|
||||
# override env variables with .env file
|
||||
|
||||
if env_file:
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
configure(log_level=log_level, log_file=log_file)
|
||||
update_settings(
|
||||
config,
|
||||
dev=dev,
|
||||
|
|
@ -213,12 +188,23 @@ def run(
|
|||
run_on_windows(host, port, log_level, options, app)
|
||||
else:
|
||||
# Run using gunicorn on Linux
|
||||
run_on_mac_or_linux(host, port, log_level, options, app)
|
||||
run_on_mac_or_linux(host, port, log_level, options, app, open_browser)
|
||||
|
||||
|
||||
def run_on_mac_or_linux(host, port, log_level, options, app):
|
||||
def run_on_mac_or_linux(host, port, log_level, options, app, open_browser=True):
|
||||
webapp_process = Process(target=run_langflow, args=(host, port, log_level, options, app))
|
||||
webapp_process.start()
|
||||
status_code = 0
|
||||
while status_code != 200:
|
||||
try:
|
||||
status_code = httpx.get(f"http://{host}:{port}/health").status_code
|
||||
|
||||
except Exception:
|
||||
time.sleep(1)
|
||||
|
||||
print_banner(host, port)
|
||||
run_langflow(host, port, log_level, options, app)
|
||||
if open_browser:
|
||||
webbrowser.open(f"http://{host}:{port}")
|
||||
|
||||
|
||||
def run_on_windows(host, port, log_level, options, app):
|
||||
|
|
@ -260,10 +246,18 @@ def get_free_port(port):
|
|||
|
||||
|
||||
def print_banner(host, port):
|
||||
# console = Console()
|
||||
try:
|
||||
from langflow.version import __version__ # type: ignore
|
||||
|
||||
word = "Langflow"
|
||||
colors = ["#3300cc"]
|
||||
version = __version__
|
||||
word = "Langflow"
|
||||
except ImportError:
|
||||
from importlib import metadata
|
||||
|
||||
version = metadata.version("langflow-base")
|
||||
word = "Langflow Base"
|
||||
|
||||
colors = ["#6e42f5"]
|
||||
|
||||
styled_word = ""
|
||||
|
||||
|
|
@ -273,7 +267,7 @@ def print_banner(host, port):
|
|||
|
||||
# Title with emojis and gradient text
|
||||
title = (
|
||||
f"[bold]Welcome to :chains: {styled_word} [/bold]\n\n"
|
||||
f"[bold]Welcome to :chains: {styled_word} v{version}[/bold]\n"
|
||||
f"Access [link=http://{host}:{port}]http://{host}:{port}[/link]"
|
||||
)
|
||||
info_text = (
|
||||
|
|
@ -293,26 +287,25 @@ def run_langflow(host, port, log_level, options, app):
|
|||
Run Langflow server on localhost
|
||||
"""
|
||||
try:
|
||||
if platform.system() in ["Windows", "Darwin"]:
|
||||
if platform.system() in ["Windows"]:
|
||||
# Run using uvicorn on MacOS and Windows
|
||||
# Windows doesn't support gunicorn
|
||||
# MacOS requires an env variable to be set to use gunicorn
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level=log_level,
|
||||
log_level=log_level.lower(),
|
||||
loop="asyncio",
|
||||
)
|
||||
else:
|
||||
from langflow.server import LangflowApplication
|
||||
|
||||
LangflowApplication(app, options).run()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Shutting down server")
|
||||
sys.exit(0)
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
sys.exit(1)
|
||||
|
|
@ -322,7 +315,7 @@ def run_langflow(host, port, log_level, options, app):
|
|||
def superuser(
|
||||
username: str = typer.Option(..., prompt=True, help="Username for the 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"),
|
||||
log_level: str = typer.Option("error", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
|
||||
):
|
||||
"""
|
||||
Create a superuser.
|
||||
|
|
@ -337,7 +330,7 @@ def superuser(
|
|||
# Verify that the superuser was created
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
user: User = session.exec(select(User).where(User.username == username)).first()
|
||||
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
|
||||
|
|
@ -349,23 +342,11 @@ def superuser(
|
|||
|
||||
|
||||
@app.command()
|
||||
def migration(
|
||||
test: bool = typer.Option(True, help="Run migrations in test mode."),
|
||||
fix: bool = typer.Option(
|
||||
False,
|
||||
help="Fix migrations. This is a destructive operation, and should only be used if you know what you are doing.",
|
||||
),
|
||||
):
|
||||
def migration(test: bool = typer.Option(True, help="Run migrations in test mode.")):
|
||||
"""
|
||||
Run or test migrations.
|
||||
"""
|
||||
if fix:
|
||||
if not typer.confirm(
|
||||
"This will delete all data necessary to fix migrations. Are you sure you want to continue?"
|
||||
):
|
||||
raise typer.Abort()
|
||||
|
||||
initialize_services(fix_migration=fix)
|
||||
initialize_services()
|
||||
db_service = get_db_service()
|
||||
if not test:
|
||||
db_service.run_migrations()
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import warnings
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
|
|
@ -62,24 +63,32 @@ def run_migrations_online() -> None:
|
|||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
from langflow.services.deps import get_db_service
|
||||
|
||||
try:
|
||||
from langflow.services.database.factory import DatabaseServiceFactory
|
||||
from langflow.services.deps import get_db_service
|
||||
from langflow.services.manager import initialize_settings_service, service_manager
|
||||
|
||||
initialize_settings_service()
|
||||
service_manager.register_factory(DatabaseServiceFactory())
|
||||
connectable = get_db_service().engine
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database engine: {e}")
|
||||
url = os.getenv("LANGFLOW_DATABASE_URL")
|
||||
url = url or config.get_main_option("sqlalchemy.url")
|
||||
if url:
|
||||
config.set_main_option("sqlalchemy.url", url)
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata, render_as_batch=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata, render_as_batch=True)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
|
|
@ -23,10 +23,12 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
${downgrades if downgrades else "pass"}
|
||||
|
|
@ -26,9 +26,8 @@ def upgrade() -> None:
|
|||
flow_constraints = inspector.get_unique_constraints("flow")
|
||||
user_constraints = inspector.get_unique_constraints("user")
|
||||
try:
|
||||
if not any(constraint['column_names'] == ['id'] for constraint in api_key_constraints):
|
||||
if not any(constraint["column_names"] == ["id"] for constraint in api_key_constraints):
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
|
||||
batch_op.create_unique_constraint("uq_apikey_id", ["id"])
|
||||
if not any(constraint["column_names"] == ["id"] for constraint in flow_constraints):
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
|
|
@ -51,16 +50,13 @@ def downgrade() -> None:
|
|||
flow_constraints = inspector.get_unique_constraints("flow")
|
||||
user_constraints = inspector.get_unique_constraints("user")
|
||||
try:
|
||||
if any(
|
||||
constraint["name"] == "uq_apikey_id" for constraint in api_key_constraints
|
||||
):
|
||||
if any(constraint["name"] == "uq_apikey_id" for constraint in api_key_constraints):
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_user_id", type_="unique")
|
||||
if any(constraint["name"] == "uq_flow_id" for constraint in flow_constraints):
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_flow_id", type_="unique")
|
||||
if any(constraint["name"] == "uq_user_id" for constraint in user_constraints):
|
||||
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.drop_constraint("uq_apikey_id", type_="unique")
|
||||
except Exception as e:
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
"""Replace Credential table with Variable
|
||||
|
||||
Revision ID: 1a110b568907
|
||||
Revises: 63b9c451fd30
|
||||
Create Date: 2024-03-25 09:40:02.743453
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "1a110b568907"
|
||||
down_revision: Union[str, None] = "63b9c451fd30"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "variable" not in table_names:
|
||||
op.create_table(
|
||||
"variable",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("value", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("type", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_variable_user_id"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
if "credential" in table_names:
|
||||
op.drop_table("credential")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names()
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if "credential" not in table_names:
|
||||
op.create_table(
|
||||
"credential",
|
||||
sa.Column("name", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("value", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("provider", sa.VARCHAR(), nullable=True),
|
||||
sa.Column("user_id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("id", sa.CHAR(length=32), nullable=False),
|
||||
sa.Column("created_at", sa.DATETIME(), nullable=False),
|
||||
sa.Column("updated_at", sa.DATETIME(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_credential_user_id"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
if "variable" in table_names:
|
||||
op.drop_table("variable")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -24,10 +24,8 @@ def upgrade() -> None:
|
|||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"name", existing_type=sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
except Exception as e:
|
||||
batch_op.alter_column("name", existing_type=sqlmodel.sql.sqltypes.AutoString(), nullable=True)
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
@ -37,6 +35,6 @@ def downgrade() -> None:
|
|||
try:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=False)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -31,23 +31,16 @@ def upgrade() -> None:
|
|||
# and other related indices
|
||||
if "flowstyle" in existing_tables:
|
||||
op.drop_table("flowstyle")
|
||||
if "ix_flowstyle_flow_id" in [
|
||||
index["name"] for index in inspector.get_indexes("flowstyle")
|
||||
]:
|
||||
op.drop_index(
|
||||
"ix_flowstyle_flow_id", table_name="flowstyle", if_exists=True
|
||||
)
|
||||
if "ix_flowstyle_flow_id" in [index["name"] for index in inspector.get_indexes("flowstyle")]:
|
||||
op.drop_index("ix_flowstyle_flow_id", table_name="flowstyle", if_exists=True)
|
||||
|
||||
existing_indices_flow = []
|
||||
existing_fks_flow = []
|
||||
if "flow" in existing_tables:
|
||||
existing_indices_flow = [
|
||||
index["name"] for index in inspector.get_indexes("flow")
|
||||
]
|
||||
existing_indices_flow = [index["name"] for index in inspector.get_indexes("flow")]
|
||||
# Existing foreign keys for the 'flow' table, if it exists
|
||||
existing_fks_flow = [
|
||||
fk["referred_table"] + "." + fk["referred_columns"][0]
|
||||
for fk in inspector.get_foreign_keys("flow")
|
||||
fk["referred_table"] + "." + fk["referred_columns"][0] for fk in inspector.get_foreign_keys("flow")
|
||||
]
|
||||
# Now check if the columns user_id exists in the 'flow' table
|
||||
# If it does not exist, we need to create the foreign key
|
||||
|
|
@ -67,9 +60,7 @@ def upgrade() -> None:
|
|||
sa.UniqueConstraint("id", name="uq_user_id"),
|
||||
)
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_user_username"), ["username"], unique=True
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_user_username"), ["username"], unique=True)
|
||||
|
||||
if "apikey" not in existing_tables:
|
||||
op.create_table(
|
||||
|
|
@ -82,20 +73,14 @@ def upgrade() -> None:
|
|||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("api_key", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"], ["user.id"], name="fk_apikey_user_id_user"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_apikey_user_id_user"),
|
||||
sa.PrimaryKeyConstraint("id", name="pk_apikey"),
|
||||
sa.UniqueConstraint("id", name="uq_apikey_id"),
|
||||
)
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_apikey_api_key"), ["api_key"], unique=True
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_apikey_api_key"), ["api_key"], unique=True)
|
||||
batch_op.create_index(batch_op.f("ix_apikey_name"), ["name"], unique=False)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_apikey_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_apikey_user_id"), ["user_id"], unique=False)
|
||||
if "flow" not in existing_tables:
|
||||
op.create_table(
|
||||
"flow",
|
||||
|
|
@ -104,9 +89,7 @@ def upgrade() -> None:
|
|||
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"], ["user.id"], name="fk_flow_user_id_user"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["user.id"], name="fk_flow_user_id_user"),
|
||||
sa.PrimaryKeyConstraint("id", name="pk_flow"),
|
||||
sa.UniqueConstraint("id", name="uq_flow_id"),
|
||||
)
|
||||
|
|
@ -129,16 +112,12 @@ def upgrade() -> None:
|
|||
if "user.id" not in existing_fks_flow:
|
||||
batch_op.create_foreign_key("fk_flow_user_id", "user", ["user_id"], ["id"])
|
||||
if "ix_flow_description" not in existing_indices_flow:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_description"), ["description"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_description"), ["description"], unique=False)
|
||||
if "ix_flow_name" not in existing_indices_flow:
|
||||
batch_op.create_index(batch_op.f("ix_flow_name"), ["name"], unique=False)
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "ix_flow_user_id" not in existing_indices_flow:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_user_id"), ["user_id"], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
@ -169,10 +148,4 @@ def downgrade() -> None:
|
|||
batch_op.drop_index(batch_op.f("ix_user_username"), if_exists=True)
|
||||
|
||||
op.drop_table("user")
|
||||
|
||||
if "flowstyle" in existing_tables:
|
||||
op.drop_table("flowstyle")
|
||||
|
||||
if "component" in existing_tables:
|
||||
op.drop_table("component")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -31,9 +31,7 @@ def upgrade() -> None:
|
|||
"credential",
|
||||
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("value", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column(
|
||||
"provider", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
),
|
||||
sa.Column("provider", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
"""Add icon and icon_bg_color to Flow
|
||||
|
||||
Revision ID: 63b9c451fd30
|
||||
Revises: bc2f01c40e4a
|
||||
Create Date: 2024-03-06 10:53:47.148658
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
from alembic import op
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "63b9c451fd30"
|
||||
down_revision: Union[str, None] = "bc2f01c40e4a"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names() # noqa
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "icon" not in column_names:
|
||||
batch_op.add_column(sa.Column("icon", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
if "icon_bg_color" not in column_names:
|
||||
batch_op.add_column(sa.Column("icon_bg_color", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn) # type: ignore
|
||||
table_names = inspector.get_table_names() # noqa
|
||||
column_names = [column["name"] for column in inspector.get_columns("flow")]
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "icon" in column_names:
|
||||
batch_op.drop_column("icon")
|
||||
if "icon_bg_color" in column_names:
|
||||
batch_op.drop_column("icon_bg_color")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -29,18 +29,14 @@ def upgrade() -> None:
|
|||
try:
|
||||
if "is_component" not in flow_columns:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("is_component", sa.Boolean(), nullable=True)
|
||||
)
|
||||
except Exception as e:
|
||||
batch_op.add_column(sa.Column("is_component", sa.Boolean(), nullable=True))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if "store_api_key" not in user_columns:
|
||||
with op.batch_alter_table("user", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("store_api_key", sqlmodel.AutoString(), nullable=True)
|
||||
)
|
||||
except Exception as e:
|
||||
batch_op.add_column(sa.Column("store_api_key", sqlmodel.AutoString(), nullable=True))
|
||||
except Exception:
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
|
@ -30,9 +30,7 @@ def upgrade() -> None:
|
|||
try:
|
||||
if "name" in api_key_columns:
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"name", existing_type=sa.VARCHAR(), nullable=False
|
||||
)
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
|
@ -40,15 +38,9 @@ def upgrade() -> None:
|
|||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
batch_op.add_column(sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
|
@ -68,7 +60,6 @@ def downgrade() -> None:
|
|||
pass
|
||||
|
||||
try:
|
||||
|
||||
with op.batch_alter_table("apikey", schema=None) as batch_op:
|
||||
batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=True)
|
||||
except Exception as e:
|
||||
|
|
@ -32,33 +32,19 @@ def upgrade() -> None:
|
|||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
flow_columns = [column["name"] for column in inspector.get_columns("flow")]
|
||||
if "is_component" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("is_component", sa.Boolean(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("is_component", sa.Boolean(), nullable=True))
|
||||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
||||
)
|
||||
)
|
||||
batch_op.add_column(sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True))
|
||||
indices = inspector.get_indexes("flow")
|
||||
indices_names = [index["name"] for index in indices]
|
||||
if "ix_flow_user_id" not in indices_names:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_user_id"), ["user_id"], unique=False)
|
||||
if "fk_flow_user_id_user" not in indices_names:
|
||||
batch_op.create_foreign_key(
|
||||
"fk_flow_user_id_user", "user", ["user_id"], ["id"]
|
||||
)
|
||||
batch_op.create_foreign_key("fk_flow_user_id_user", "user", ["user_id"], ["id"])
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -33,21 +33,13 @@ def upgrade() -> None:
|
|||
if "updated_at" not in flow_columns:
|
||||
batch_op.add_column(sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
if "folder" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("folder", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
|
||||
if "user_id" not in flow_columns:
|
||||
batch_op.add_column(
|
||||
sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("user_id", sqlmodel.sql.sqltypes.GUID(), nullable=True))
|
||||
if "ix_flow_user_id" not in flow_indexes:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_flow_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(batch_op.f("ix_flow_user_id"), ["user_id"], unique=False)
|
||||
if "flow_user_id_fkey" not in flow_fks:
|
||||
batch_op.create_foreign_key(
|
||||
"flow_user_id_fkey", "user", ["user_id"], ["id"]
|
||||
)
|
||||
batch_op.create_foreign_key("flow_user_id_fkey", "user", ["user_id"], ["id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
|
@ -19,7 +19,7 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
connection = op.get_bind()
|
||||
connection = op.get_bind() # noqa
|
||||
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -22,9 +22,7 @@ def upgrade() -> None:
|
|||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"user_id", existing_type=sa.CHAR(length=32), nullable=True
|
||||
)
|
||||
batch_op.alter_column("user_id", existing_type=sa.CHAR(length=32), nullable=True)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -36,9 +34,7 @@ def downgrade() -> None:
|
|||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
try:
|
||||
with op.batch_alter_table("flow", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"user_id", existing_type=sa.CHAR(length=32), nullable=False
|
||||
)
|
||||
batch_op.alter_column("user_id", existing_type=sa.CHAR(length=32), nullable=False)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -31,9 +31,7 @@ def upgrade() -> None:
|
|||
try:
|
||||
if "credential" in tables and "fk_credential_user_id" not in foreign_keys_names:
|
||||
with op.batch_alter_table("credential", schema=None) as batch_op:
|
||||
batch_op.create_foreign_key(
|
||||
"fk_credential_user_id", "user", ["user_id"], ["id"]
|
||||
)
|
||||
batch_op.create_foreign_key("fk_credential_user_id", "user", ["user_id"], ["id"])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
|
@ -4,13 +4,15 @@ from fastapi import APIRouter
|
|||
from langflow.api.v1 import (
|
||||
api_key_router,
|
||||
chat_router,
|
||||
credentials_router,
|
||||
endpoints_router,
|
||||
files_router,
|
||||
flows_router,
|
||||
login_router,
|
||||
monitor_router,
|
||||
store_router,
|
||||
users_router,
|
||||
validate_router,
|
||||
variables_router,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -24,4 +26,6 @@ router.include_router(flows_router)
|
|||
router.include_router(users_router)
|
||||
router.include_router(api_key_router)
|
||||
router.include_router(login_router)
|
||||
router.include_router(credentials_router)
|
||||
router.include_router(variables_router)
|
||||
router.include_router(files_router)
|
||||
router.include_router(monitor_router)
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, List
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from platformdirs import user_cache_dir
|
||||
from sqlmodel import Session
|
||||
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.store.schema import StoreComponentCreate
|
||||
from langflow.services.store.utils import get_lf_version_from_pypi
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.services.database.models.flow.model import Flow
|
||||
|
||||
|
||||
|
|
@ -120,6 +125,9 @@ def update_template_field(frontend_template, key, value_dict):
|
|||
template_field["value"] = ""
|
||||
template_field["file_path"] = file_path_value
|
||||
|
||||
if "load_from_db" in value_dict and value_dict["load_from_db"]:
|
||||
template_field["load_from_db"] = value_dict["load_from_db"]
|
||||
|
||||
|
||||
def get_file_path_value(file_path):
|
||||
"""Get the file path value if the file exists, else return empty string."""
|
||||
|
|
@ -137,7 +145,7 @@ def get_file_path_value(file_path):
|
|||
return file_path
|
||||
|
||||
|
||||
def validate_is_component(flows: List["Flow"]):
|
||||
def validate_is_component(flows: list["Flow"]):
|
||||
for flow in flows:
|
||||
if not flow.data or flow.is_component is not None:
|
||||
continue
|
||||
|
|
@ -156,7 +164,7 @@ def get_is_component_from_data(data: dict):
|
|||
|
||||
|
||||
async def check_langflow_version(component: StoreComponentCreate):
|
||||
from langflow import __version__ as current_version
|
||||
from langflow.version.version import __version__ as current_version # type: ignore
|
||||
|
||||
if not component.last_tested_version:
|
||||
component.last_tested_version = current_version
|
||||
|
|
@ -171,19 +179,132 @@ async def check_langflow_version(component: StoreComponentCreate):
|
|||
)
|
||||
|
||||
|
||||
def format_elapsed_time(elapsed_time) -> str:
|
||||
# Format elapsed time to human readable format coming from
|
||||
# perf_counter()
|
||||
# If the elapsed time is less than 1 second, return ms
|
||||
# If the elapsed time is less than 1 minute, return seconds rounded to 2 decimals
|
||||
time_str = ""
|
||||
def format_elapsed_time(elapsed_time: float) -> str:
|
||||
"""Format elapsed time to a human-readable format coming from perf_counter().
|
||||
|
||||
- Less than 1 second: returns milliseconds
|
||||
- Less than 1 minute: returns seconds rounded to 2 decimals
|
||||
- 1 minute or more: returns minutes and seconds
|
||||
"""
|
||||
if elapsed_time < 1:
|
||||
elapsed_time = int(round(elapsed_time * 1000))
|
||||
time_str = f"{elapsed_time} ms"
|
||||
milliseconds = int(round(elapsed_time * 1000))
|
||||
return f"{milliseconds} ms"
|
||||
elif elapsed_time < 60:
|
||||
elapsed_time = round(elapsed_time, 2)
|
||||
time_str = f"{elapsed_time} seconds"
|
||||
seconds = round(elapsed_time, 2)
|
||||
unit = "second" if seconds == 1 else "seconds"
|
||||
return f"{seconds} {unit}"
|
||||
else:
|
||||
elapsed_time = round(elapsed_time / 60, 2)
|
||||
time_str = f"{elapsed_time} minutes"
|
||||
return time_str
|
||||
minutes = int(elapsed_time // 60)
|
||||
seconds = round(elapsed_time % 60, 2)
|
||||
minutes_unit = "minute" if minutes == 1 else "minutes"
|
||||
seconds_unit = "second" if seconds == 1 else "seconds"
|
||||
return f"{minutes} {minutes_unit}, {seconds} {seconds_unit}"
|
||||
|
||||
|
||||
async def build_and_cache_graph(
|
||||
flow_id: str,
|
||||
session: Session,
|
||||
chat_service: "ChatService",
|
||||
graph: Optional[Graph] = None,
|
||||
):
|
||||
"""Build and cache the graph."""
|
||||
flow: Optional[Flow] = session.get(Flow, flow_id)
|
||||
if not flow or not flow.data:
|
||||
raise ValueError("Invalid flow ID")
|
||||
other_graph = Graph.from_payload(flow.data, flow_id)
|
||||
if graph is None:
|
||||
graph = other_graph
|
||||
else:
|
||||
graph = graph.update(other_graph)
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
return graph
|
||||
|
||||
|
||||
def format_syntax_error_message(exc: SyntaxError) -> str:
|
||||
"""Format a SyntaxError message for returning to the frontend."""
|
||||
if exc.text is None:
|
||||
return f"Syntax error in code. Error on line {exc.lineno}"
|
||||
return f"Syntax error in code. Error on line {exc.lineno}: {exc.text.strip()}"
|
||||
|
||||
|
||||
def get_causing_exception(exc: BaseException) -> BaseException:
|
||||
"""Get the causing exception from an exception."""
|
||||
if hasattr(exc, "__cause__") and exc.__cause__:
|
||||
return get_causing_exception(exc.__cause__)
|
||||
return exc
|
||||
|
||||
|
||||
def format_exception_message(exc: Exception) -> str:
|
||||
"""Format an exception message for returning to the frontend."""
|
||||
# We need to check if the __cause__ is a SyntaxError
|
||||
# If it is, we need to return the message of the SyntaxError
|
||||
causing_exception = get_causing_exception(exc)
|
||||
if isinstance(causing_exception, SyntaxError):
|
||||
return format_syntax_error_message(causing_exception)
|
||||
return str(exc)
|
||||
|
||||
|
||||
async def get_next_runnable_vertices(
|
||||
graph: Graph,
|
||||
vertex: "Vertex",
|
||||
vertex_id: str,
|
||||
chat_service: ChatService,
|
||||
flow_id: str,
|
||||
):
|
||||
"""
|
||||
Retrieves the next runnable vertices in the graph for a given vertex.
|
||||
|
||||
Args:
|
||||
graph (Graph): The graph object representing the flow.
|
||||
vertex (Vertex): The current vertex.
|
||||
vertex_id (str): The ID of the current vertex.
|
||||
chat_service (ChatService): The chat service object.
|
||||
flow_id (str): The ID of the flow.
|
||||
|
||||
Returns:
|
||||
list: A list of IDs of the next runnable vertices.
|
||||
|
||||
"""
|
||||
async with chat_service._cache_locks[flow_id] as lock:
|
||||
graph.remove_from_predecessors(vertex_id)
|
||||
direct_successors_ready = [v for v in vertex.successors_ids if graph.is_vertex_runnable(v)]
|
||||
if not direct_successors_ready:
|
||||
# No direct successors ready, look for runnable predecessors of successors
|
||||
next_runnable_vertices = graph.find_runnable_predecessors_for_successors(vertex_id)
|
||||
else:
|
||||
next_runnable_vertices = direct_successors_ready
|
||||
|
||||
for v_id in set(next_runnable_vertices): # Use set to avoid duplicates
|
||||
graph.vertices_to_run.remove(v_id)
|
||||
graph.remove_from_predecessors(v_id)
|
||||
await chat_service.set_cache(flow_id=flow_id, data=graph, lock=lock)
|
||||
return next_runnable_vertices
|
||||
|
||||
|
||||
def get_top_level_vertices(graph, vertices_ids):
|
||||
"""
|
||||
Retrieves the top-level vertices from the given graph based on the provided vertex IDs.
|
||||
|
||||
Args:
|
||||
graph (Graph): The graph object containing the vertices.
|
||||
vertices_ids (list): A list of vertex IDs.
|
||||
|
||||
Returns:
|
||||
list: A list of top-level vertex IDs.
|
||||
|
||||
"""
|
||||
top_level_vertices = []
|
||||
for vertex_id in vertices_ids:
|
||||
vertex = graph.get_vertex(vertex_id)
|
||||
if vertex.parent_is_top_level:
|
||||
top_level_vertices.append(vertex.parent_node_id)
|
||||
else:
|
||||
top_level_vertices.append(vertex_id)
|
||||
return top_level_vertices
|
||||
|
||||
|
||||
def parse_exception(exc):
|
||||
"""Parse the exception message."""
|
||||
if hasattr(exc, "body"):
|
||||
return exc.body["message"]
|
||||
return str(exc)
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
from langflow.api.v1.api_key import router as api_key_router
|
||||
from langflow.api.v1.chat import router as chat_router
|
||||
from langflow.api.v1.credential import router as credentials_router
|
||||
from langflow.api.v1.endpoints import router as endpoints_router
|
||||
from langflow.api.v1.files import router as files_router
|
||||
from langflow.api.v1.flows import router as flows_router
|
||||
from langflow.api.v1.login import router as login_router
|
||||
from langflow.api.v1.monitor import router as monitor_router
|
||||
from langflow.api.v1.store import router as store_router
|
||||
from langflow.api.v1.users import router as users_router
|
||||
from langflow.api.v1.validate import router as validate_router
|
||||
from langflow.api.v1.variable import router as variables_router
|
||||
|
||||
__all__ = [
|
||||
"chat_router",
|
||||
|
|
@ -17,5 +19,7 @@ __all__ = [
|
|||
"users_router",
|
||||
"api_key_router",
|
||||
"login_router",
|
||||
"credentials_router",
|
||||
"variables_router",
|
||||
"monitor_router",
|
||||
"files_router",
|
||||
]
|
||||
|
|
@ -8,20 +8,10 @@ from langflow.api.v1.schemas import ApiKeyCreateRequest, ApiKeysResponse
|
|||
from langflow.services.auth import utils as auth_utils
|
||||
|
||||
# Assuming you have these methods in your service layer
|
||||
from langflow.services.database.models.api_key.crud import (
|
||||
create_api_key,
|
||||
delete_api_key,
|
||||
get_api_keys,
|
||||
)
|
||||
from langflow.services.database.models.api_key.model import (
|
||||
ApiKeyCreate,
|
||||
UnmaskedApiKeyRead,
|
||||
)
|
||||
from langflow.services.database.models.api_key.crud import create_api_key, delete_api_key, get_api_keys
|
||||
from langflow.services.database.models.api_key.model import ApiKeyCreate, UnmaskedApiKeyRead
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import (
|
||||
get_session,
|
||||
get_settings_service,
|
||||
)
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
165
src/backend/base/langflow/api/v1/base.py
Normal file
165
src/backend/base/langflow/api/v1/base.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, field_validator, model_serializer
|
||||
|
||||
from langflow.template.frontend_node.base import FrontendNode
|
||||
|
||||
|
||||
class CacheResponse(BaseModel):
|
||||
data: dict
|
||||
|
||||
|
||||
class Code(BaseModel):
|
||||
code: str
|
||||
|
||||
|
||||
class FrontendNodeRequest(FrontendNode):
|
||||
template: dict # type: ignore
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def serialize_model(self, handler):
|
||||
# Override the default serialization method in FrontendNode
|
||||
# because we don't need the name in the response (i.e. {name: {}})
|
||||
return handler(self)
|
||||
|
||||
|
||||
class ValidatePromptRequest(BaseModel):
|
||||
name: str
|
||||
template: str
|
||||
custom_fields: Optional[dict] = None
|
||||
frontend_node: Optional[FrontendNodeRequest] = None
|
||||
|
||||
|
||||
# Build ValidationResponse class for {"imports": {"errors": []}, "function": {"errors": []}}
|
||||
class CodeValidationResponse(BaseModel):
|
||||
imports: dict
|
||||
function: dict
|
||||
|
||||
@field_validator("imports")
|
||||
@classmethod
|
||||
def validate_imports(cls, v):
|
||||
return v or {"errors": []}
|
||||
|
||||
@field_validator("function")
|
||||
@classmethod
|
||||
def validate_function(cls, v):
|
||||
return v or {"errors": []}
|
||||
|
||||
|
||||
class PromptValidationResponse(BaseModel):
|
||||
input_variables: list
|
||||
# object return for tweak call
|
||||
frontend_node: Optional[FrontendNodeRequest] = None
|
||||
|
||||
|
||||
INVALID_CHARACTERS = {
|
||||
" ",
|
||||
",",
|
||||
".",
|
||||
":",
|
||||
";",
|
||||
"!",
|
||||
"?",
|
||||
"/",
|
||||
"\\",
|
||||
"(",
|
||||
")",
|
||||
"[",
|
||||
"]",
|
||||
}
|
||||
|
||||
INVALID_NAMES = {
|
||||
"input_variables",
|
||||
"output_parser",
|
||||
"partial_variables",
|
||||
"template",
|
||||
"template_format",
|
||||
"validate_template",
|
||||
}
|
||||
|
||||
|
||||
def is_json_like(var):
|
||||
if var.startswith("{{") and var.endswith("}}"):
|
||||
# If it is a double brance variable
|
||||
# we don't want to validate any of its content
|
||||
return True
|
||||
# the above doesn't work on all cases because the json string can be multiline
|
||||
# or indented which can add \n or spaces at the start or end of the string
|
||||
# test_case_3 new_var == '\n{{\n "test": "hello",\n "text": "world"\n}}\n'
|
||||
# what we can do is to remove the \n and spaces from the start and end of the string
|
||||
# and then check if the string starts with {{ and ends with }}
|
||||
var = var.strip()
|
||||
var = var.replace("\n", "")
|
||||
var = var.replace(" ", "")
|
||||
# Now it should be a valid json string
|
||||
return var.startswith("{{") and var.endswith("}}")
|
||||
|
||||
|
||||
def fix_variable(var, invalid_chars, wrong_variables):
|
||||
if not var:
|
||||
return var, invalid_chars, wrong_variables
|
||||
new_var = var
|
||||
|
||||
# Handle variables starting with a number
|
||||
if var[0].isdigit():
|
||||
invalid_chars.append(var[0])
|
||||
new_var, invalid_chars, wrong_variables = fix_variable(var[1:], invalid_chars, wrong_variables)
|
||||
|
||||
# Temporarily replace {{ and }} to avoid treating them as invalid
|
||||
new_var = new_var.replace("{{", "ᴛᴇᴍᴘᴏᴘᴇɴ").replace("}}", "ᴛᴇᴍᴘᴄʟᴏsᴇ")
|
||||
|
||||
# Remove invalid characters
|
||||
for char in new_var:
|
||||
if char in INVALID_CHARACTERS:
|
||||
invalid_chars.append(char)
|
||||
new_var = new_var.replace(char, "")
|
||||
if var not in wrong_variables: # Avoid duplicating entries
|
||||
wrong_variables.append(var)
|
||||
|
||||
# Restore {{ and }}
|
||||
new_var = new_var.replace("ᴛᴇᴍᴘᴏᴘᴇɴ", "{{").replace("ᴛᴇᴍᴘᴄʟᴏsᴇ", "}}")
|
||||
|
||||
return new_var, invalid_chars, wrong_variables
|
||||
|
||||
|
||||
def check_variable(var, invalid_chars, wrong_variables, empty_variables):
|
||||
if any(char in invalid_chars for char in var):
|
||||
wrong_variables.append(var)
|
||||
elif var == "":
|
||||
empty_variables.append(var)
|
||||
return wrong_variables, empty_variables
|
||||
|
||||
|
||||
def check_for_errors(input_variables, fixed_variables, wrong_variables, empty_variables):
|
||||
if any(var for var in input_variables if var not in fixed_variables):
|
||||
error_message = (
|
||||
f"Error: Input variables contain invalid characters or formats. \n"
|
||||
f"Invalid variables: {', '.join(wrong_variables)}.\n"
|
||||
f"Empty variables: {', '.join(empty_variables)}. \n"
|
||||
f"Fixed variables: {', '.join(fixed_variables)}."
|
||||
)
|
||||
raise ValueError(error_message)
|
||||
|
||||
|
||||
def check_input_variables(input_variables):
|
||||
invalid_chars = []
|
||||
fixed_variables = []
|
||||
wrong_variables = []
|
||||
empty_variables = []
|
||||
variables_to_check = []
|
||||
|
||||
for var in input_variables:
|
||||
# First, let's check if the variable is a JSON string
|
||||
# because if it is, it won't be considered a variable
|
||||
# and we don't need to validate it
|
||||
if is_json_like(var):
|
||||
continue
|
||||
|
||||
new_var, wrong_variables, empty_variables = fix_variable(var, invalid_chars, wrong_variables)
|
||||
wrong_variables, empty_variables = check_variable(var, INVALID_CHARACTERS, wrong_variables, empty_variables)
|
||||
fixed_variables.append(new_var)
|
||||
variables_to_check.append(var)
|
||||
|
||||
check_for_errors(variables_to_check, fixed_variables, wrong_variables, empty_variables)
|
||||
|
||||
return fixed_variables
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
import asyncio
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from langchain.schema import AgentAction, AgentFinish
|
||||
from langchain_core.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
|
||||
from langflow.api.v1.schemas import ChatResponse, PromptResponse
|
||||
from langflow.services.deps import get_chat_service
|
||||
from langflow.utils.util import remove_ansi_escape_codes
|
||||
from langchain_core.callbacks.base import AsyncCallbackHandler
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.schemas import ChatResponse, PromptResponse
|
||||
from langflow.services.deps import get_chat_service, get_socket_service
|
||||
from langflow.utils.util import remove_ansi_escape_codes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.socket.service import SocketIOService
|
||||
|
||||
|
||||
# https://github.com/hwchase17/chat-langchain/blob/master/callback.py
|
||||
class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
||||
class AsyncStreamingLLMCallbackHandleSIO(AsyncCallbackHandler):
|
||||
"""Callback handler for streaming LLM responses."""
|
||||
|
||||
@property
|
||||
|
|
@ -19,14 +22,16 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
"""Whether to ignore chain callbacks."""
|
||||
return False
|
||||
|
||||
def __init__(self, client_id: str):
|
||||
def __init__(self, session_id: str):
|
||||
self.chat_service = get_chat_service()
|
||||
self.client_id = client_id
|
||||
self.websocket = self.chat_service.active_connections[self.client_id]
|
||||
self.client_id = session_id
|
||||
self.socketio_service: "SocketIOService" = get_socket_service()
|
||||
self.sid = session_id
|
||||
# self.socketio_service = 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.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_tool_start(self, serialized: Dict[str, Any], input_str: str, **kwargs: Any) -> Any:
|
||||
"""Run when tool starts running."""
|
||||
|
|
@ -35,7 +40,7 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
type="stream",
|
||||
intermediate_steps=f"Tool input: {input_str}",
|
||||
)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_tool_end(self, output: str, **kwargs: Any) -> Any:
|
||||
"""Run when tool ends running."""
|
||||
|
|
@ -66,7 +71,7 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
try:
|
||||
# This is to emulate the stream of tokens
|
||||
for resp in resps:
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
except Exception as exc:
|
||||
logger.error(f"Error sending response: {exc}")
|
||||
|
||||
|
|
@ -92,8 +97,7 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
resp = PromptResponse(
|
||||
prompt=text,
|
||||
)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
self.chat_service.chat_history.add_message(self.client_id, resp)
|
||||
await self.socketio_service.emit_message(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_agent_action(self, action: AgentAction, **kwargs: Any):
|
||||
log = f"Thought: {action.log}"
|
||||
|
|
@ -103,10 +107,10 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
logs = log.split("\n")
|
||||
for log in logs:
|
||||
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
else:
|
||||
resp = ChatResponse(message="", type="stream", intermediate_steps=log)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
|
||||
async def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any:
|
||||
"""Run on agent end."""
|
||||
|
|
@ -115,20 +119,4 @@ class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
|
|||
type="stream",
|
||||
intermediate_steps=finish.log,
|
||||
)
|
||||
await self.websocket.send_json(resp.model_dump())
|
||||
|
||||
|
||||
class StreamingLLMCallbackHandler(BaseCallbackHandler):
|
||||
"""Callback handler for streaming LLM responses."""
|
||||
|
||||
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="")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
coroutine = self.websocket.send_json(resp.model_dump())
|
||||
asyncio.run_coroutine_threadsafe(coroutine, loop)
|
||||
await self.socketio_service.emit_token(to=self.sid, data=resp.model_dump())
|
||||
329
src/backend/base/langflow/api/v1/chat.py
Normal file
329
src/backend/base/langflow/api/v1/chat.py
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
import time
|
||||
import uuid
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.utils import (
|
||||
build_and_cache_graph,
|
||||
format_elapsed_time,
|
||||
format_exception_message,
|
||||
get_top_level_vertices,
|
||||
parse_exception,
|
||||
)
|
||||
from langflow.api.v1.schemas import (
|
||||
InputValueRequest,
|
||||
ResultDataResponse,
|
||||
StreamData,
|
||||
VertexBuildResponse,
|
||||
VerticesOrderResponse,
|
||||
)
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.chat.service import ChatService
|
||||
from langflow.services.deps import get_chat_service, get_session, get_session_service
|
||||
from langflow.services.monitor.utils import log_vertex_build
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.graph.vertex.types import ChatVertex
|
||||
from langflow.services.session.service import SessionService
|
||||
|
||||
router = APIRouter(tags=["Chat"])
|
||||
|
||||
|
||||
async def try_running_celery_task(vertex, user_id):
|
||||
# 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
|
||||
await vertex.build(user_id=user_id)
|
||||
return vertex
|
||||
|
||||
|
||||
@router.get("/build/{flow_id}/vertices", response_model=VerticesOrderResponse)
|
||||
async def get_vertices(
|
||||
flow_id: str,
|
||||
stop_component_id: Optional[str] = None,
|
||||
start_component_id: Optional[str] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
session=Depends(get_session),
|
||||
):
|
||||
"""
|
||||
Retrieve the vertices order for a given flow.
|
||||
|
||||
Args:
|
||||
flow_id (str): The ID of the flow.
|
||||
stop_component_id (str, optional): The ID of the stop component. Defaults to None.
|
||||
start_component_id (str, optional): The ID of the start component. Defaults to None.
|
||||
chat_service (ChatService, optional): The chat service dependency. Defaults to Depends(get_chat_service).
|
||||
session (Session, optional): The session dependency. Defaults to Depends(get_session).
|
||||
|
||||
Returns:
|
||||
VerticesOrderResponse: The response containing the ordered vertex IDs and the run ID.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error checking the build status.
|
||||
"""
|
||||
try:
|
||||
# First, we need to check if the flow_id is in the cache
|
||||
graph = None
|
||||
if cache := await chat_service.get_cache(flow_id):
|
||||
graph = cache.get("result")
|
||||
graph = await build_and_cache_graph(flow_id, session, chat_service, graph)
|
||||
if stop_component_id or start_component_id:
|
||||
try:
|
||||
first_layer = graph.sort_vertices(stop_component_id, start_component_id)
|
||||
except Exception as exc:
|
||||
logger.error(exc)
|
||||
first_layer = graph.sort_vertices()
|
||||
else:
|
||||
first_layer = graph.sort_vertices()
|
||||
# When we send vertices to the frontend
|
||||
# we need to remove them from the predecessors
|
||||
# so they are not considered for building again
|
||||
# which duplicates the results
|
||||
for vertex_id in first_layer:
|
||||
graph.remove_from_predecessors(vertex_id)
|
||||
|
||||
# Now vertices is a list of lists
|
||||
# We need to get the id of each vertex
|
||||
# and return the same structure but only with the ids
|
||||
run_id = uuid.uuid4()
|
||||
graph.set_run_id(run_id)
|
||||
vertices_to_run = list(graph.vertices_to_run) + get_top_level_vertices(graph, graph.vertices_to_run)
|
||||
return VerticesOrderResponse(ids=first_layer, run_id=run_id, vertices_to_run=vertices_to_run)
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"Error checking build status: {exc}")
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/build/{flow_id}/vertices/{vertex_id}")
|
||||
async def build_vertex(
|
||||
flow_id: str,
|
||||
vertex_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
inputs: Annotated[Optional[InputValueRequest], Body(embed=True)] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
current_user=Depends(get_current_active_user),
|
||||
):
|
||||
"""Build a vertex instead of the entire graph.
|
||||
|
||||
Args:
|
||||
flow_id (str): The ID of the flow.
|
||||
vertex_id (str): The ID of the vertex to build.
|
||||
background_tasks (BackgroundTasks): The background tasks object for logging.
|
||||
inputs (Optional[InputValueRequest], optional): The input values for the vertex. Defaults to None.
|
||||
chat_service (ChatService, optional): The chat service dependency. Defaults to Depends(get_chat_service).
|
||||
current_user (Any, optional): The current user dependency. Defaults to Depends(get_current_active_user).
|
||||
|
||||
Returns:
|
||||
VertexBuildResponse: The response containing the built vertex information.
|
||||
|
||||
Raises:
|
||||
HTTPException: If there is an error building the vertex.
|
||||
|
||||
"""
|
||||
|
||||
start_time = time.perf_counter()
|
||||
next_runnable_vertices = []
|
||||
top_level_vertices = []
|
||||
try:
|
||||
start_time = time.perf_counter()
|
||||
cache = await chat_service.get_cache(flow_id)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
logger.warning(f"No cache found for {flow_id}. Building graph starting at {vertex_id}")
|
||||
graph = await build_and_cache_graph(flow_id=flow_id, session=next(get_session()), chat_service=chat_service)
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
result_data_response = ResultDataResponse(results={})
|
||||
duration = ""
|
||||
vertex = graph.get_vertex(vertex_id)
|
||||
try:
|
||||
lock = chat_service._cache_locks[flow_id]
|
||||
set_cache_coro = partial(chat_service.set_cache, flow_id=flow_id)
|
||||
(
|
||||
next_runnable_vertices,
|
||||
top_level_vertices,
|
||||
result_dict,
|
||||
params,
|
||||
valid,
|
||||
artifacts,
|
||||
vertex,
|
||||
) = await graph.build_vertex(
|
||||
lock=lock,
|
||||
set_cache_coro=set_cache_coro,
|
||||
vertex_id=vertex_id,
|
||||
user_id=current_user.id,
|
||||
inputs_dict=inputs.model_dump() if inputs else {},
|
||||
)
|
||||
result_data_response = ResultDataResponse(**result_dict.model_dump())
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(f"Error building vertex: {exc}")
|
||||
params = format_exception_message(exc)
|
||||
valid = False
|
||||
result_data_response = ResultDataResponse(results={})
|
||||
artifacts = {}
|
||||
# If there's an error building the vertex
|
||||
# we need to clear the cache
|
||||
await chat_service.clear_cache(flow_id)
|
||||
|
||||
# Log the vertex build
|
||||
if not vertex.will_stream:
|
||||
background_tasks.add_task(
|
||||
log_vertex_build,
|
||||
flow_id=flow_id,
|
||||
vertex_id=vertex_id,
|
||||
valid=valid,
|
||||
params=params,
|
||||
data=result_data_response,
|
||||
artifacts=artifacts,
|
||||
)
|
||||
|
||||
timedelta = time.perf_counter() - start_time
|
||||
duration = format_elapsed_time(timedelta)
|
||||
result_data_response.duration = duration
|
||||
result_data_response.timedelta = timedelta
|
||||
vertex.add_build_time(timedelta)
|
||||
inactivated_vertices = None
|
||||
inactivated_vertices = list(graph.inactivated_vertices)
|
||||
graph.reset_inactivated_vertices()
|
||||
graph.reset_activated_vertices()
|
||||
await chat_service.set_cache(flow_id, graph)
|
||||
|
||||
# graph.stop_vertex tells us if the user asked
|
||||
# to stop the build of the graph at a certain vertex
|
||||
# if it is in next_vertices_ids, we need to remove other
|
||||
# vertices from next_vertices_ids
|
||||
if graph.stop_vertex and graph.stop_vertex in next_runnable_vertices:
|
||||
next_runnable_vertices = [graph.stop_vertex]
|
||||
|
||||
build_response = VertexBuildResponse(
|
||||
inactivated_vertices=inactivated_vertices,
|
||||
next_vertices_ids=next_runnable_vertices,
|
||||
top_level_vertices=top_level_vertices,
|
||||
valid=valid,
|
||||
params=params,
|
||||
id=vertex.id,
|
||||
data=result_data_response,
|
||||
)
|
||||
return build_response
|
||||
except Exception as exc:
|
||||
logger.error(f"Error building vertex: {exc}")
|
||||
logger.exception(exc)
|
||||
message = parse_exception(exc)
|
||||
raise HTTPException(status_code=500, detail=message) from exc
|
||||
|
||||
|
||||
@router.get("/build/{flow_id}/{vertex_id}/stream", response_class=StreamingResponse)
|
||||
async def build_vertex_stream(
|
||||
flow_id: str,
|
||||
vertex_id: str,
|
||||
session_id: Optional[str] = None,
|
||||
chat_service: "ChatService" = Depends(get_chat_service),
|
||||
session_service: "SessionService" = Depends(get_session_service),
|
||||
):
|
||||
"""Build a vertex instead of the entire graph.
|
||||
|
||||
This function is responsible for building a single vertex instead of the entire graph.
|
||||
It takes the `flow_id` and `vertex_id` as required parameters, and an optional `session_id`.
|
||||
It also depends on the `ChatService` and `SessionService` services.
|
||||
|
||||
If `session_id` is not provided, it retrieves the graph from the cache using the `chat_service`.
|
||||
If `session_id` is provided, it loads the session data using the `session_service`.
|
||||
|
||||
Once the graph is obtained, it retrieves the specified vertex using the `vertex_id`.
|
||||
If the vertex does not support streaming, an error is raised.
|
||||
If the vertex has a built result, it sends the result as a chunk.
|
||||
If the vertex is not frozen or not built, it streams the vertex data.
|
||||
If the vertex has a result, it sends the result as a chunk.
|
||||
If none of the above conditions are met, an error is raised.
|
||||
|
||||
If any exception occurs during the process, an error message is sent.
|
||||
Finally, the stream is closed.
|
||||
|
||||
Returns:
|
||||
A `StreamingResponse` object with the streamed vertex data in text/event-stream format.
|
||||
|
||||
Raises:
|
||||
HTTPException: If an error occurs while building the vertex.
|
||||
"""
|
||||
try:
|
||||
|
||||
async def stream_vertex():
|
||||
try:
|
||||
if not session_id:
|
||||
cache = await chat_service.get_cache(flow_id)
|
||||
if not cache:
|
||||
# If there's no cache
|
||||
raise ValueError(f"No cache found for {flow_id}.")
|
||||
else:
|
||||
graph = cache.get("result")
|
||||
else:
|
||||
session_data = await session_service.load_session(session_id, flow_id=flow_id)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if not graph:
|
||||
raise ValueError(f"No graph found for {flow_id}.")
|
||||
|
||||
vertex: "ChatVertex" = graph.get_vertex(vertex_id)
|
||||
if not hasattr(vertex, "stream"):
|
||||
raise ValueError(f"Vertex {vertex_id} does not support streaming")
|
||||
if isinstance(vertex._built_result, str) and vertex._built_result:
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"message": f"Streaming vertex {vertex_id}"},
|
||||
)
|
||||
yield str(stream_data)
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": vertex._built_result},
|
||||
)
|
||||
yield str(stream_data)
|
||||
|
||||
elif not vertex.frozen or not vertex._built:
|
||||
logger.debug(f"Streaming vertex {vertex_id}")
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"message": f"Streaming vertex {vertex_id}"},
|
||||
)
|
||||
yield str(stream_data)
|
||||
async for chunk in vertex.stream():
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": chunk},
|
||||
)
|
||||
yield str(stream_data)
|
||||
elif vertex.result is not None:
|
||||
stream_data = StreamData(
|
||||
event="message",
|
||||
data={"chunk": vertex._built_result},
|
||||
)
|
||||
yield str(stream_data)
|
||||
else:
|
||||
raise ValueError(f"No result found for vertex {vertex_id}")
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(f"Error building vertex: {exc}")
|
||||
exc_message = parse_exception(exc)
|
||||
if exc_message == "The message must be an iterator or an async iterator.":
|
||||
exc_message = "This stream has already been closed."
|
||||
yield str(StreamData(event="error", data={"error": exc_message}))
|
||||
finally:
|
||||
logger.debug("Closing stream")
|
||||
yield str(StreamData(event="close", data={"message": "Stream closed"}))
|
||||
|
||||
return StreamingResponse(stream_vertex(), media_type="text/event-stream")
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail="Error building vertex") from exc
|
||||
455
src/backend/base/langflow/api/v1/endpoints.py
Normal file
455
src/backend/base/langflow/api/v1/endpoints.py
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
from http import HTTPStatus
|
||||
from typing import Annotated, List, Optional, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status
|
||||
from loguru import logger
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.api.utils import update_frontend_node_with_template_values
|
||||
from langflow.api.v1.schemas import (
|
||||
CustomComponentRequest,
|
||||
InputValueRequest,
|
||||
ProcessResponse,
|
||||
RunResponse,
|
||||
SimplifiedAPIRequest,
|
||||
TaskStatusResponse,
|
||||
Tweaks,
|
||||
UpdateCustomComponentRequest,
|
||||
UploadFileResponse,
|
||||
)
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.interface.custom.directory_reader import DirectoryReader
|
||||
from langflow.interface.custom.utils import build_custom_component_template
|
||||
from langflow.processing.process import process_tweaks, run_graph_internal
|
||||
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.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_session_service, get_settings_service, get_task_service
|
||||
from langflow.services.session.service import SessionService
|
||||
from langflow.services.task.service import TaskService
|
||||
|
||||
# build router
|
||||
router = APIRouter(tags=["Base"])
|
||||
|
||||
|
||||
@router.get("/all", dependencies=[Depends(get_current_active_user)])
|
||||
def get_all(
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
from langflow.interface.types import get_all_types_dict
|
||||
|
||||
logger.debug("Building langchain types dict")
|
||||
try:
|
||||
all_types_dict = get_all_types_dict(settings_service.settings.COMPONENTS_PATH)
|
||||
return all_types_dict
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/run/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
|
||||
async def simplified_run_flow(
|
||||
db: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
input_request: SimplifiedAPIRequest = SimplifiedAPIRequest(),
|
||||
stream: bool = False,
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
):
|
||||
"""
|
||||
Executes a specified flow by ID with input customization, performance enhancements through caching, and optional data streaming.
|
||||
|
||||
### Parameters:
|
||||
- `db` (Session): Database session for executing queries.
|
||||
- `flow_id` (str): Unique identifier of the flow to be executed.
|
||||
- `input_request` (SimplifiedAPIRequest): Request object containing input values, types, output selection, tweaks, and session ID.
|
||||
- `api_key_user` (User): User object derived from the provided API key, used for authentication.
|
||||
- `session_service` (SessionService): Service for managing flow sessions, essential for session reuse and caching.
|
||||
|
||||
### SimplifiedAPIRequest:
|
||||
- `input_value` (Optional[str], default=""): Input value to pass to the flow.
|
||||
- `input_type` (Optional[Literal["chat", "text", "any"]], default="chat"): Type of the input value, determining how the input is interpreted.
|
||||
- `output_type` (Optional[Literal["chat", "text", "any", "debug"]], default="chat"): Desired type of output, affecting which components' outputs are included in the response. If set to "debug", all outputs are returned.
|
||||
- `output_component` (Optional[str], default=None): Specific component output to retrieve. If provided, only the output of the specified component is returned. This overrides the `output_type` parameter.
|
||||
- `tweaks` (Optional[Tweaks], default=None): Adjustments to the flow's behavior, allowing for custom execution parameters.
|
||||
- `session_id` (Optional[str], default=None): An identifier for reusing session data, aiding in performance for subsequent requests.
|
||||
|
||||
|
||||
### Tweaks
|
||||
A dictionary of tweaks to customize the flow execution. The tweaks can be used to modify the flow's parameters and components. Tweaks can be overridden by the input values.
|
||||
You can use Component's `id` or Display Name as key to tweak a specific component (e.g., `{"Component Name": {"parameter_name": "value"}}`).
|
||||
You can also use the parameter name as key to tweak all components with that parameter (e.g., `{"parameter_name": "value"}`).
|
||||
|
||||
### Returns:
|
||||
- A `RunResponse` object containing the execution results, including selected (or all, based on `output_type`) outputs of the flow and the session ID, facilitating result retrieval and further interactions in a session context.
|
||||
|
||||
### Raises:
|
||||
- HTTPException: 404 if the specified flow ID curl -X 'POST' \
|
||||
|
||||
### Example:
|
||||
```bash
|
||||
curl -X 'POST' \
|
||||
'http://<your_server>/run/{flow_id}' \
|
||||
-H 'accept: application/json' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'x-api-key: YOU_API_KEY' \
|
||||
-H '
|
||||
-d '{
|
||||
"input_value": "Sample input",
|
||||
"input_type": "chat",
|
||||
"output_type": "chat",
|
||||
"tweaks": {},
|
||||
}'
|
||||
```
|
||||
|
||||
This endpoint provides a powerful interface for executing flows with enhanced flexibility and efficiency, supporting a wide range of applications by allowing for dynamic input and output configuration along with performance optimizations through session management and caching.
|
||||
"""
|
||||
session_id = input_request.session_id
|
||||
try:
|
||||
task_result: List[RunOutputs] = []
|
||||
artifacts = {}
|
||||
if input_request.session_id:
|
||||
session_data = await session_service.load_session(input_request.session_id, flow_id=flow_id)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if graph is None:
|
||||
raise ValueError(f"Session {input_request.session_id} not found")
|
||||
else:
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||
flow = db.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first()
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
||||
if flow.data is None:
|
||||
raise ValueError(f"Flow {flow_id} has no data")
|
||||
graph_data = flow.data
|
||||
graph_data = process_tweaks(graph_data, input_request.tweaks or {})
|
||||
graph = Graph.from_payload(graph_data, flow_id=flow_id)
|
||||
inputs = [
|
||||
InputValueRequest(components=[], input_value=input_request.input_value, type=input_request.input_type)
|
||||
]
|
||||
# outputs is a list of all components that should return output
|
||||
# we need to get them by checking their type
|
||||
# if the output type is debug, we return all outputs
|
||||
# if the output type is any, we return all outputs that are either chat or text
|
||||
# if the output type is chat or text, we return only the outputs that match the type
|
||||
if input_request.output_component:
|
||||
outputs = [input_request.output_component]
|
||||
else:
|
||||
outputs = [
|
||||
vertex.id
|
||||
for vertex in graph.vertices
|
||||
if input_request.output_type == "debug"
|
||||
or (
|
||||
vertex.is_output
|
||||
and (input_request.output_type == "any" or input_request.output_type in vertex.id.lower())
|
||||
)
|
||||
]
|
||||
task_result, session_id = await run_graph_internal(
|
||||
graph=graph,
|
||||
flow_id=flow_id,
|
||||
session_id=input_request.session_id,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts=artifacts,
|
||||
session_service=session_service,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
return RunResponse(outputs=task_result, 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):
|
||||
logger.error(f"Flow ID {flow_id} is not a valid UUID")
|
||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
if f"Flow {flow_id} not found" in str(exc):
|
||||
logger.error(f"Flow {flow_id} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
elif f"Session {session_id} not found" in str(exc):
|
||||
logger.error(f"Session {session_id} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
else:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/run/advanced/{flow_id}", response_model=RunResponse, response_model_exclude_none=True)
|
||||
async def experimental_run_flow(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[List[InputValueRequest]] = [InputValueRequest(components=[], input_value="")],
|
||||
outputs: Optional[List[str]] = [],
|
||||
tweaks: Annotated[Optional[Tweaks], Body(embed=True)] = None, # noqa: F821
|
||||
stream: Annotated[bool, Body(embed=True)] = False, # noqa: F821
|
||||
session_id: Annotated[Union[None, str], Body(embed=True)] = None, # noqa: F821
|
||||
api_key_user: User = Depends(api_key_security),
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
):
|
||||
"""
|
||||
Executes a specified flow by ID with optional input values, output selection, tweaks, and streaming capability.
|
||||
This endpoint supports running flows with caching to enhance performance and efficiency.
|
||||
|
||||
### Parameters:
|
||||
- `flow_id` (str): The unique identifier of the flow to be executed.
|
||||
- `inputs` (List[InputValueRequest], optional): A list of inputs specifying the input values and components for the flow. Each input can target specific components and provide custom values.
|
||||
- `outputs` (List[str], optional): A list of output names to retrieve from the executed flow. If not provided, all outputs are returned.
|
||||
- `tweaks` (Optional[Tweaks], optional): A dictionary of tweaks to customize the flow execution. The tweaks can be used to modify the flow's parameters and components. Tweaks can be overridden by the input values.
|
||||
- `stream` (bool, optional): Specifies whether the results should be streamed. Defaults to False.
|
||||
- `session_id` (Union[None, str], optional): An optional session ID to utilize existing session data for the flow execution.
|
||||
- `api_key_user` (User): The user associated with the current API key. Automatically resolved from the API key.
|
||||
- `session_service` (SessionService): The session service object for managing flow sessions.
|
||||
|
||||
### Returns:
|
||||
A `RunResponse` object containing the selected outputs (or all if not specified) of the executed flow and the session ID. The structure of the response accommodates multiple inputs, providing a nested list of outputs for each input.
|
||||
|
||||
### Raises:
|
||||
HTTPException: Indicates issues with finding the specified flow, invalid input formats, or internal errors during flow execution.
|
||||
|
||||
### Example usage:
|
||||
```json
|
||||
POST /run/{flow_id}
|
||||
x-api-key: YOUR_API_KEY
|
||||
Payload:
|
||||
{
|
||||
"inputs": [
|
||||
{"components": ["component1"], "input_value": "value1"},
|
||||
{"components": ["component3"], "input_value": "value2"}
|
||||
],
|
||||
"outputs": ["Component Name", "component_id"],
|
||||
"tweaks": {"parameter_name": "value", "Component Name": {"parameter_name": "value"}, "component_id": {"parameter_name": "value"}}
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
This endpoint facilitates complex flow executions with customized inputs, outputs, and configurations, catering to diverse application requirements.
|
||||
"""
|
||||
try:
|
||||
if outputs is None:
|
||||
outputs = []
|
||||
|
||||
task_result: List[RunOutputs] = []
|
||||
artifacts = {}
|
||||
if session_id:
|
||||
session_data = await session_service.load_session(session_id, flow_id=flow_id)
|
||||
graph, artifacts = session_data if session_data else (None, None)
|
||||
if graph is None:
|
||||
raise ValueError(f"Session {session_id} not found")
|
||||
else:
|
||||
# Get the flow that matches the flow_id and belongs to the user
|
||||
# flow = session.query(Flow).filter(Flow.id == flow_id).filter(Flow.user_id == api_key_user.id).first()
|
||||
flow = session.exec(select(Flow).where(Flow.id == flow_id).where(Flow.user_id == api_key_user.id)).first()
|
||||
if flow is None:
|
||||
raise ValueError(f"Flow {flow_id} not found")
|
||||
|
||||
if flow.data is None:
|
||||
raise ValueError(f"Flow {flow_id} has no data")
|
||||
graph_data = flow.data
|
||||
graph_data = process_tweaks(graph_data, tweaks or {})
|
||||
graph = Graph.from_payload(graph_data, flow_id=flow_id)
|
||||
task_result, session_id = await run_graph_internal(
|
||||
graph=graph,
|
||||
flow_id=flow_id,
|
||||
session_id=session_id,
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
artifacts=artifacts,
|
||||
session_service=session_service,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
return RunResponse(outputs=task_result, 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):
|
||||
logger.error(f"Flow ID {flow_id} is not a valid UUID")
|
||||
# This means the Flow ID is not a valid UUID which means it can't find the flow
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
if f"Flow {flow_id} not found" in str(exc):
|
||||
logger.error(f"Flow {flow_id} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
elif f"Session {session_id} not found" in str(exc):
|
||||
logger.error(f"Session {session_id} not found")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
else:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/predict/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
dependencies=[Depends(api_key_security)],
|
||||
)
|
||||
@router.post(
|
||||
"/process/{flow_id}",
|
||||
response_model=ProcessResponse,
|
||||
)
|
||||
async def process(
|
||||
session: Annotated[Session, Depends(get_session)],
|
||||
flow_id: str,
|
||||
inputs: Optional[Union[List[dict], dict]] = None,
|
||||
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
|
||||
session_service: SessionService = Depends(get_session_service),
|
||||
):
|
||||
"""
|
||||
Endpoint to process an input with a given flow_id.
|
||||
"""
|
||||
# Raise a depreciation warning
|
||||
logger.warning(
|
||||
"The /process endpoint is deprecated and will be removed in a future version. " "Please use /run instead."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="The /process endpoint is deprecated and will be removed in a future version. "
|
||||
"Please use /run instead.",
|
||||
)
|
||||
|
||||
|
||||
@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 is None:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
if task.ready():
|
||||
result = task.result
|
||||
# If result isinstance of Exception, can we get the traceback?
|
||||
if isinstance(result, Exception):
|
||||
logger.exception(task.traceback)
|
||||
|
||||
if isinstance(result, dict) and "result" in result:
|
||||
result = result["result"]
|
||||
elif hasattr(result, "result"):
|
||||
result = result.result
|
||||
|
||||
if task.status == "FAILURE":
|
||||
result = str(task.result)
|
||||
logger.error(f"Task {task_id} failed: {task.traceback}")
|
||||
|
||||
return TaskStatusResponse(status=task.status, result=result)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/upload/{flow_id}",
|
||||
response_model=UploadFileResponse,
|
||||
status_code=HTTPStatus.CREATED,
|
||||
)
|
||||
async def create_upload_file(
|
||||
file: UploadFile,
|
||||
flow_id: str,
|
||||
):
|
||||
try:
|
||||
file_path = save_uploaded_file(file, folder_name=flow_id)
|
||||
|
||||
return UploadFileResponse(
|
||||
flowId=flow_id,
|
||||
file_path=file_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f"Error saving file: {exc}")
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
# get endpoint to return version of langflow
|
||||
@router.get("/version")
|
||||
def get_version():
|
||||
try:
|
||||
from langflow.version import __version__
|
||||
|
||||
version = __version__
|
||||
package = "Langflow"
|
||||
except ImportError:
|
||||
from importlib import metadata
|
||||
|
||||
version = metadata.version("langflow-base")
|
||||
package = "Langflow Base"
|
||||
return {"version": version, "package": package}
|
||||
|
||||
|
||||
@router.post("/custom_component", status_code=HTTPStatus.OK)
|
||||
async def custom_component(
|
||||
raw_code: CustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
component = CustomComponent(code=raw_code.code)
|
||||
|
||||
built_frontend_node, _ = build_custom_component_template(component, user_id=user.id)
|
||||
|
||||
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node)
|
||||
return built_frontend_node
|
||||
|
||||
|
||||
@router.post("/custom_component/reload", status_code=HTTPStatus.OK)
|
||||
async def reload_custom_component(path: str, user: User = Depends(get_current_active_user)):
|
||||
from langflow.interface.custom.utils import build_custom_component_template
|
||||
|
||||
try:
|
||||
reader = DirectoryReader("")
|
||||
valid, content = reader.process_file(path)
|
||||
if not valid:
|
||||
raise ValueError(content)
|
||||
|
||||
extractor = CustomComponent(code=content)
|
||||
frontend_node, _ = build_custom_component_template(extractor, user_id=user.id)
|
||||
return frontend_node
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
|
||||
@router.post("/custom_component/update", status_code=HTTPStatus.OK)
|
||||
async def custom_component_update(
|
||||
code_request: UpdateCustomComponentRequest,
|
||||
user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""
|
||||
Update a custom component with the provided code request.
|
||||
|
||||
This endpoint generates the CustomComponentFrontendNode normally but then runs the `update_build_config` method
|
||||
on the latest version of the template. This ensures that every time it runs, it has the latest version of the template.
|
||||
|
||||
Args:
|
||||
code_request (CustomComponentRequest): The code request containing the updated code for the custom component.
|
||||
user (User, optional): The user making the request. Defaults to the current active user.
|
||||
|
||||
Returns:
|
||||
dict: The updated custom component node.
|
||||
|
||||
"""
|
||||
try:
|
||||
component = CustomComponent(code=code_request.code)
|
||||
|
||||
component_node, cc_instance = build_custom_component_template(
|
||||
component,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
updated_build_config = cc_instance.update_build_config(
|
||||
build_config=code_request.get_template(),
|
||||
field_value=code_request.field_value,
|
||||
field_name=code_request.field,
|
||||
)
|
||||
component_node["template"] = updated_build_config
|
||||
|
||||
return component_node
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
116
src/backend/base/langflow/api/v1/files.py
Normal file
116
src/backend/base/langflow/api/v1/files.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import hashlib
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
|
||||
from langflow.api.v1.schemas import UploadFileResponse
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import Flow
|
||||
from langflow.services.deps import get_session, get_storage_service
|
||||
from langflow.services.storage.service import StorageService
|
||||
from langflow.services.storage.utils import build_content_type_from_extension
|
||||
|
||||
router = APIRouter(tags=["Files"], prefix="/files")
|
||||
|
||||
|
||||
# Create dep that gets the flow_id from the request
|
||||
# then finds it in the database and returns it while
|
||||
# using the current user as the owner
|
||||
def get_flow_id(
|
||||
flow_id: str,
|
||||
current_user=Depends(get_current_active_user),
|
||||
session=Depends(get_session),
|
||||
):
|
||||
# AttributeError: 'SelectOfScalar' object has no attribute 'first'
|
||||
flow = session.get(Flow, flow_id)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
if flow.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="You don't have access to this flow")
|
||||
return flow_id
|
||||
|
||||
|
||||
@router.post("/upload/{flow_id}", status_code=HTTPStatus.CREATED)
|
||||
async def upload_file(
|
||||
file: UploadFile,
|
||||
flow_id: str = Depends(get_flow_id),
|
||||
storage_service: StorageService = Depends(get_storage_service),
|
||||
):
|
||||
try:
|
||||
file_content = await file.read()
|
||||
file_name = file.filename or hashlib.sha256(file_content).hexdigest()
|
||||
folder = flow_id
|
||||
await storage_service.save_file(flow_id=folder, file_name=file_name, data=file_content)
|
||||
return UploadFileResponse(flowId=flow_id, file_path=f"{folder}/{file_name}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/download/{flow_id}/{file_name}")
|
||||
async def download_file(file_name: str, flow_id: str, storage_service: StorageService = Depends(get_storage_service)):
|
||||
try:
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
if not extension:
|
||||
raise HTTPException(status_code=500, detail=f"Extension not found for file {file_name}")
|
||||
|
||||
content_type = build_content_type_from_extension(extension)
|
||||
|
||||
if not content_type:
|
||||
raise HTTPException(status_code=500, detail=f"Content type not found for extension {extension}")
|
||||
|
||||
file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
|
||||
headers = {
|
||||
"Content-Disposition": f"attachment; filename={file_name} filename*=UTF-8''{file_name}",
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": str(len(file_content)),
|
||||
}
|
||||
return StreamingResponse(BytesIO(file_content), media_type=content_type, headers=headers)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/images/{flow_id}/{file_name}")
|
||||
async def download_image(file_name: str, flow_id: str, storage_service: StorageService = Depends(get_storage_service)):
|
||||
try:
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
if not extension:
|
||||
raise HTTPException(status_code=500, detail=f"Extension not found for file {file_name}")
|
||||
|
||||
content_type = build_content_type_from_extension(extension)
|
||||
|
||||
if not content_type:
|
||||
raise HTTPException(status_code=500, detail=f"Content type not found for extension {extension}")
|
||||
elif not content_type.startswith("image"):
|
||||
raise HTTPException(status_code=500, detail=f"Content type {content_type} is not an image")
|
||||
|
||||
file_content = await storage_service.get_file(flow_id=flow_id, file_name=file_name)
|
||||
return StreamingResponse(BytesIO(file_content), media_type=content_type)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/list/{flow_id}")
|
||||
async def list_files(
|
||||
flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
):
|
||||
try:
|
||||
files = await storage_service.list_files(flow_id=flow_id)
|
||||
return {"files": files}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/delete/{flow_id}/{file_name}")
|
||||
async def delete_file(
|
||||
file_name: str, flow_id: str = Depends(get_flow_id), storage_service: StorageService = Depends(get_storage_service)
|
||||
):
|
||||
try:
|
||||
await storage_service.delete_file(flow_id=flow_id, file_name=file_name)
|
||||
return {"message": f"File {file_name} deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
|
@ -5,14 +5,17 @@ from uuid import UUID
|
|||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from loguru import logger
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.api.utils import remove_api_keys, validate_is_component
|
||||
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
from langflow.services.settings.service import SettingsService
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/flows", tags=["Flows"])
|
||||
|
|
@ -42,11 +45,36 @@ def create_flow(
|
|||
def read_flows(
|
||||
*,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
session: Session = Depends(get_session),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
):
|
||||
"""Read all flows."""
|
||||
try:
|
||||
flows = current_user.flows
|
||||
flows = validate_is_component(flows)
|
||||
auth_settings = settings_service.auth_settings
|
||||
if auth_settings.AUTO_LOGIN:
|
||||
flows = session.exec(
|
||||
select(Flow).where(
|
||||
(Flow.user_id == None) | (Flow.user_id == current_user.id) # noqa
|
||||
)
|
||||
).all()
|
||||
else:
|
||||
flows = current_user.flows
|
||||
|
||||
flows = validate_is_component(flows) # type: ignore
|
||||
flow_ids = [flow.id for flow in flows]
|
||||
# with the session get the flows that DO NOT have a user_id
|
||||
try:
|
||||
example_flows = session.exec(
|
||||
select(Flow).where(
|
||||
Flow.user_id == None, # noqa
|
||||
Flow.folder == STARTER_FOLDER_NAME,
|
||||
)
|
||||
).all()
|
||||
for example_flow in example_flows:
|
||||
if example_flow.id not in flow_ids:
|
||||
flows.append(example_flow) # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
return [jsonable_encoder(flow) for flow in flows]
|
||||
|
|
@ -58,9 +86,18 @@ def read_flow(
|
|||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
):
|
||||
"""Read a flow."""
|
||||
if user_flow := (session.exec(select(Flow).where(Flow.id == flow_id, Flow.user_id == current_user.id)).first()):
|
||||
auth_settings = settings_service.auth_settings
|
||||
stmt = select(Flow).where(Flow.id == flow_id)
|
||||
if auth_settings.AUTO_LOGIN:
|
||||
# If auto login is enable user_id can be current_user.id or None
|
||||
# so write an OR
|
||||
stmt = stmt.where(
|
||||
(Flow.user_id == current_user.id) | (Flow.user_id == None) # noqa
|
||||
) # noqa
|
||||
if user_flow := session.exec(stmt).first():
|
||||
return user_flow
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
|
|
@ -77,7 +114,12 @@ def update_flow(
|
|||
):
|
||||
"""Update a flow."""
|
||||
|
||||
db_flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
db_flow = read_flow(
|
||||
session=session,
|
||||
flow_id=flow_id,
|
||||
current_user=current_user,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
if not db_flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
flow_data = flow.model_dump(exclude_unset=True)
|
||||
|
|
@ -99,9 +141,15 @@ def delete_flow(
|
|||
session: Session = Depends(get_session),
|
||||
flow_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Delete a flow."""
|
||||
flow = read_flow(session=session, flow_id=flow_id, current_user=current_user)
|
||||
flow = read_flow(
|
||||
session=session,
|
||||
flow_id=flow_id,
|
||||
current_user=current_user,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
if not flow:
|
||||
raise HTTPException(status_code=404, detail="Flow not found")
|
||||
session.delete(flow)
|
||||
|
|
@ -109,9 +157,6 @@ def delete_flow(
|
|||
return {"message": "Flow deleted successfully"}
|
||||
|
||||
|
||||
# Define a new model to handle multiple flows
|
||||
|
||||
|
||||
@router.post("/batch/", response_model=List[FlowRead], status_code=201)
|
||||
def create_flows(
|
||||
*,
|
||||
|
|
@ -157,8 +202,9 @@ async def upload_file(
|
|||
async def download_file(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Download all flows as a file."""
|
||||
flows = read_flows(current_user=current_user)
|
||||
flows = read_flows(current_user=current_user, session=session, settings_service=settings_service)
|
||||
return FlowListRead(flows=flows)
|
||||
|
|
@ -10,6 +10,7 @@ from langflow.services.auth.utils import (
|
|||
create_user_tokens,
|
||||
)
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
from langflow.services.settings.manager import SettingsService
|
||||
|
||||
router = APIRouter(tags=["Login"])
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ async def login_to_get_access_token(
|
|||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
|
|
@ -48,6 +50,7 @@ async def login_to_get_access_token(
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
)
|
||||
return tokens
|
||||
else:
|
||||
|
|
@ -73,6 +76,7 @@ async def auto_login(
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=None, # Set to None to make it a session cookie
|
||||
)
|
||||
return tokens
|
||||
|
||||
|
|
@ -87,7 +91,7 @@ async def auto_login(
|
|||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(
|
||||
request: Request, response: Response, settings_service=Depends(get_settings_service)
|
||||
request: Request, response: Response, settings_service: "SettingsService" = Depends(get_settings_service)
|
||||
):
|
||||
auth_settings = settings_service.auth_settings
|
||||
|
||||
|
|
@ -101,6 +105,7 @@ async def refresh_token(
|
|||
httponly=auth_settings.REFRESH_HTTPONLY,
|
||||
samesite=auth_settings.REFRESH_SAME_SITE,
|
||||
secure=auth_settings.REFRESH_SECURE,
|
||||
expires=auth_settings.REFRESH_TOKEN_EXPIRE_SECONDS,
|
||||
)
|
||||
response.set_cookie(
|
||||
"access_token_lf",
|
||||
|
|
@ -108,6 +113,7 @@ async def refresh_token(
|
|||
httponly=auth_settings.ACCESS_HTTPONLY,
|
||||
samesite=auth_settings.ACCESS_SAME_SITE,
|
||||
secure=auth_settings.ACCESS_SECURE,
|
||||
expires=auth_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||
)
|
||||
return tokens
|
||||
else:
|
||||
73
src/backend/base/langflow/api/v1/monitor.py
Normal file
73
src/backend/base/langflow/api/v1/monitor.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
|
||||
from langflow.services.deps import get_monitor_service
|
||||
from langflow.services.monitor.schema import VertexBuildMapModel
|
||||
from langflow.services.monitor.service import MonitorService
|
||||
|
||||
router = APIRouter(prefix="/monitor", tags=["Monitor"])
|
||||
|
||||
|
||||
# Get vertex_builds data from the monitor service
|
||||
@router.get("/builds", response_model=VertexBuildMapModel)
|
||||
async def get_vertex_builds(
|
||||
flow_id: Optional[str] = Query(None),
|
||||
vertex_id: Optional[str] = Query(None),
|
||||
valid: Optional[bool] = Query(None),
|
||||
order_by: Optional[str] = Query("timestamp"),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
vertex_build_dicts = monitor_service.get_vertex_builds(
|
||||
flow_id=flow_id, vertex_id=vertex_id, valid=valid, order_by=order_by
|
||||
)
|
||||
vertex_build_map = VertexBuildMapModel.from_list_of_dicts(vertex_build_dicts)
|
||||
return vertex_build_map
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/builds", status_code=204)
|
||||
async def delete_vertex_builds(
|
||||
flow_id: Optional[str] = Query(None),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
monitor_service.delete_vertex_builds(flow_id=flow_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/messages")
|
||||
async def get_messages(
|
||||
session_id: Optional[str] = Query(None),
|
||||
sender: Optional[str] = Query(None),
|
||||
sender_name: Optional[str] = Query(None),
|
||||
order_by: Optional[str] = Query("timestamp"),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
return monitor_service.get_messages(
|
||||
sender=sender,
|
||||
sender_name=sender_name,
|
||||
session_id=session_id,
|
||||
order_by=order_by,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/transactions")
|
||||
async def get_transactions(
|
||||
source: Optional[str] = Query(None),
|
||||
target: Optional[str] = Query(None),
|
||||
status: Optional[str] = Query(None),
|
||||
order_by: Optional[str] = Query("timestamp"),
|
||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||
):
|
||||
try:
|
||||
return monitor_service.get_transactions(source=source, target=target, status=status, order_by=order_by)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
325
src/backend/base/langflow/api/v1/schemas.py
Normal file
325
src/backend/base/langflow/api/v1/schemas.py
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, model_serializer
|
||||
|
||||
from langflow.graph.schema import RunOutputs
|
||||
from langflow.schema import dotdict
|
||||
from langflow.schema.schema import InputType, OutputType
|
||||
from langflow.services.database.models.api_key.model import ApiKeyRead
|
||||
from langflow.services.database.models.base import orjson_dumps
|
||||
from langflow.services.database.models.flow import FlowCreate, FlowRead
|
||||
from langflow.services.database.models.user import UserRead
|
||||
|
||||
|
||||
class BuildStatus(Enum):
|
||||
"""Status of the build."""
|
||||
|
||||
SUCCESS = "success"
|
||||
FAILURE = "failure"
|
||||
STARTED = "started"
|
||||
IN_PROGRESS = "in_progress"
|
||||
|
||||
|
||||
class TweaksRequest(BaseModel):
|
||||
tweaks: Optional[Dict[str, Dict[str, str]]] = Field(default_factory=dict)
|
||||
|
||||
|
||||
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: Any
|
||||
status: Optional[str] = None
|
||||
task: Optional[TaskResponse] = None
|
||||
session_id: Optional[str] = None
|
||||
backend: Optional[str] = None
|
||||
|
||||
|
||||
class RunResponse(BaseModel):
|
||||
"""Run response schema."""
|
||||
|
||||
outputs: Optional[List[RunOutputs]] = []
|
||||
session_id: Optional[str] = None
|
||||
|
||||
@model_serializer(mode="plain")
|
||||
def serialize(self):
|
||||
# Serialize all the outputs if they are base models
|
||||
serialized = {"session_id": self.session_id, "outputs": []}
|
||||
if self.outputs:
|
||||
serialized_outputs = []
|
||||
for output in self.outputs:
|
||||
if isinstance(output, BaseModel) and not isinstance(output, RunOutputs):
|
||||
serialized_outputs.append(output.model_dump(exclude_none=True))
|
||||
else:
|
||||
serialized_outputs.append(output)
|
||||
serialized["outputs"] = serialized_outputs
|
||||
return serialized
|
||||
|
||||
|
||||
class PreloadResponse(BaseModel):
|
||||
"""Preload response schema."""
|
||||
|
||||
session_id: Optional[str] = None
|
||||
is_clear: Optional[bool] = None
|
||||
|
||||
|
||||
class TaskStatusResponse(BaseModel):
|
||||
"""Task status response schema."""
|
||||
|
||||
status: str
|
||||
result: Optional[Any] = None
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
"""Chat message schema."""
|
||||
|
||||
is_bot: bool = False
|
||||
message: Union[str, None, dict] = None
|
||||
chatKey: Optional[str] = None
|
||||
type: str = "human"
|
||||
|
||||
|
||||
class ChatResponse(ChatMessage):
|
||||
"""Chat response schema."""
|
||||
|
||||
intermediate_steps: str
|
||||
|
||||
type: str
|
||||
is_bot: bool = True
|
||||
files: list = []
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
def validate_message_type(cls, v):
|
||||
if v not in ["start", "stream", "end", "error", "info", "file"]:
|
||||
raise ValueError("type must be start, stream, end, error, info, or file")
|
||||
return v
|
||||
|
||||
|
||||
class PromptResponse(ChatMessage):
|
||||
"""Prompt response schema."""
|
||||
|
||||
prompt: str
|
||||
type: str = "prompt"
|
||||
is_bot: bool = True
|
||||
|
||||
|
||||
class FileResponse(ChatMessage):
|
||||
"""File response schema."""
|
||||
|
||||
data: Any = None
|
||||
data_type: str
|
||||
type: str = "file"
|
||||
is_bot: bool = True
|
||||
|
||||
@field_validator("data_type")
|
||||
@classmethod
|
||||
def validate_data_type(cls, v):
|
||||
if v not in ["image", "csv"]:
|
||||
raise ValueError("data_type must be image or csv")
|
||||
return v
|
||||
|
||||
|
||||
class FlowListCreate(BaseModel):
|
||||
flows: List[FlowCreate]
|
||||
|
||||
|
||||
class FlowListRead(BaseModel):
|
||||
flows: List[FlowRead]
|
||||
|
||||
|
||||
class InitResponse(BaseModel):
|
||||
flowId: str
|
||||
|
||||
|
||||
class BuiltResponse(BaseModel):
|
||||
built: bool
|
||||
|
||||
|
||||
class UploadFileResponse(BaseModel):
|
||||
"""Upload file response schema."""
|
||||
|
||||
flowId: str
|
||||
file_path: Path
|
||||
|
||||
|
||||
class StreamData(BaseModel):
|
||||
event: str
|
||||
data: dict
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"event: {self.event}\ndata: {orjson_dumps(self.data, indent_2=False)}\n\n"
|
||||
|
||||
|
||||
class CustomComponentRequest(BaseModel):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
code: str
|
||||
frontend_node: Optional[dict] = None
|
||||
|
||||
|
||||
class UpdateCustomComponentRequest(CustomComponentRequest):
|
||||
field: str
|
||||
field_value: Optional[Union[str, int, float, bool, dict, list]] = None
|
||||
template: dict
|
||||
|
||||
def get_template(self):
|
||||
return dotdict(self.template)
|
||||
|
||||
|
||||
class CustomComponentResponseError(BaseModel):
|
||||
detail: str
|
||||
traceback: str
|
||||
|
||||
|
||||
class ComponentListCreate(BaseModel):
|
||||
flows: List[FlowCreate]
|
||||
|
||||
|
||||
class ComponentListRead(BaseModel):
|
||||
flows: List[FlowRead]
|
||||
|
||||
|
||||
class UsersResponse(BaseModel):
|
||||
total_count: int
|
||||
users: List[UserRead]
|
||||
|
||||
|
||||
class ApiKeyResponse(BaseModel):
|
||||
id: str
|
||||
api_key: str
|
||||
name: str
|
||||
created_at: str
|
||||
last_used_at: str
|
||||
|
||||
|
||||
class ApiKeysResponse(BaseModel):
|
||||
total_count: int
|
||||
user_id: UUID
|
||||
api_keys: List[ApiKeyRead]
|
||||
|
||||
|
||||
class CreateApiKeyRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class ApiKeyCreateRequest(BaseModel):
|
||||
api_key: str
|
||||
|
||||
|
||||
class VerticesOrderResponse(BaseModel):
|
||||
ids: List[str]
|
||||
run_id: UUID
|
||||
vertices_to_run: List[str]
|
||||
|
||||
|
||||
class ResultDataResponse(BaseModel):
|
||||
results: Optional[Any] = Field(default_factory=dict)
|
||||
artifacts: Optional[Any] = Field(default_factory=dict)
|
||||
timedelta: Optional[float] = None
|
||||
duration: Optional[str] = None
|
||||
|
||||
|
||||
class VertexBuildResponse(BaseModel):
|
||||
id: Optional[str] = None
|
||||
inactivated_vertices: Optional[List[str]] = None
|
||||
next_vertices_ids: Optional[List[str]] = None
|
||||
top_level_vertices: Optional[List[str]] = None
|
||||
valid: bool
|
||||
params: Optional[Any] = Field(default_factory=dict)
|
||||
"""JSON string of the params."""
|
||||
data: ResultDataResponse
|
||||
"""Mapping of vertex ids to result dict containing the param name and result value."""
|
||||
timestamp: Optional[datetime] = Field(default_factory=datetime.utcnow)
|
||||
"""Timestamp of the build."""
|
||||
|
||||
|
||||
class VerticesBuiltResponse(BaseModel):
|
||||
vertices: List[VertexBuildResponse]
|
||||
|
||||
|
||||
class InputValueRequest(BaseModel):
|
||||
components: Optional[List[str]] = []
|
||||
input_value: Optional[str] = None
|
||||
type: Optional[InputType] = Field(
|
||||
"any",
|
||||
description="Defines on which components the input value should be applied. 'any' applies to all input components.",
|
||||
)
|
||||
|
||||
# add an example
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"examples": [
|
||||
{
|
||||
"components": ["components_id", "Component Name"],
|
||||
"input_value": "input_value",
|
||||
},
|
||||
{"components": ["Component Name"], "input_value": "input_value"},
|
||||
{"input_value": "input_value"},
|
||||
{"type": "chat", "input_value": "input_value"},
|
||||
{"type": "json", "input_value": '{"key": "value"}'},
|
||||
]
|
||||
},
|
||||
extra="forbid",
|
||||
)
|
||||
|
||||
|
||||
class Tweaks(RootModel):
|
||||
root: dict[str, Union[str, dict[str, str]]] = Field(
|
||||
description="A dictionary of tweaks to adjust the flow's execution. Allows customizing flow behavior dynamically. All tweaks are overridden by the input values.",
|
||||
)
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"examples": [
|
||||
{
|
||||
"parameter_name": "value",
|
||||
"Component Name": {"parameter_name": "value"},
|
||||
"component_id": {"parameter_name": "value"},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# This should behave like a dict
|
||||
def __getitem__(self, key):
|
||||
return self.root[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.root[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.root[key]
|
||||
|
||||
def items(self):
|
||||
return self.root.items()
|
||||
|
||||
|
||||
class SimplifiedAPIRequest(BaseModel):
|
||||
input_value: Optional[str] = Field(default="", description="The input value")
|
||||
input_type: Optional[InputType] = Field(default="chat", description="The input type")
|
||||
output_type: Optional[OutputType] = Field(default="chat", description="The output type")
|
||||
output_component: Optional[str] = Field(
|
||||
default="",
|
||||
description="If there are multiple output components, you can specify the component to get the output from.",
|
||||
)
|
||||
tweaks: Optional[Tweaks] = Field(default=None, description="The tweaks")
|
||||
session_id: Optional[str] = Field(default=None, description="The session id")
|
||||
81
src/backend/base/langflow/api/v1/validate.py
Normal file
81
src/backend/base/langflow/api/v1/validate.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.base import Code, CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest
|
||||
from langflow.base.prompts.utils import (
|
||||
add_new_variables_to_template,
|
||||
get_old_custom_fields,
|
||||
remove_old_variables_from_template,
|
||||
update_input_variables_field,
|
||||
validate_prompt,
|
||||
)
|
||||
from langflow.utils.validate import validate_code
|
||||
|
||||
# build router
|
||||
router = APIRouter(prefix="/validate", tags=["Validate"])
|
||||
|
||||
|
||||
@router.post("/code", status_code=200, response_model=CodeValidationResponse)
|
||||
def post_validate_code(code: Code):
|
||||
try:
|
||||
errors = validate_code(code.code)
|
||||
return CodeValidationResponse(
|
||||
imports=errors.get("imports", {}),
|
||||
function=errors.get("function", {}),
|
||||
)
|
||||
except Exception as e:
|
||||
return HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/prompt", status_code=200, response_model=PromptValidationResponse)
|
||||
def post_validate_prompt(prompt_request: ValidatePromptRequest):
|
||||
try:
|
||||
input_variables = validate_prompt(prompt_request.template)
|
||||
# Check if frontend_node is None before proceeding to avoid attempting to update a non-existent node.
|
||||
if prompt_request.frontend_node is None:
|
||||
return PromptValidationResponse(
|
||||
input_variables=input_variables,
|
||||
frontend_node=None,
|
||||
)
|
||||
if not prompt_request.frontend_node.custom_fields:
|
||||
prompt_request.frontend_node.custom_fields = defaultdict(list)
|
||||
old_custom_fields = get_old_custom_fields(prompt_request.frontend_node.custom_fields, prompt_request.name)
|
||||
|
||||
add_new_variables_to_template(
|
||||
input_variables,
|
||||
prompt_request.frontend_node.custom_fields,
|
||||
prompt_request.frontend_node.template,
|
||||
prompt_request.name,
|
||||
)
|
||||
|
||||
remove_old_variables_from_template(
|
||||
old_custom_fields,
|
||||
input_variables,
|
||||
prompt_request.frontend_node.custom_fields,
|
||||
prompt_request.frontend_node.template,
|
||||
prompt_request.name,
|
||||
)
|
||||
|
||||
update_input_variables_field(input_variables, prompt_request.frontend_node.template)
|
||||
|
||||
# If frontend_node.template contains only one field that is type == 'prompt', then we can remove all fields that are not
|
||||
# 'code', and not in the input_variables list.
|
||||
prompt_fields = [
|
||||
key
|
||||
for key, field in prompt_request.frontend_node.template.items()
|
||||
if isinstance(field, dict) and field["type"] == "prompt"
|
||||
]
|
||||
|
||||
if len(prompt_fields) == 1:
|
||||
for key, field in prompt_request.frontend_node.template.copy().items():
|
||||
if isinstance(field, dict) and field["type"] != "code" and key not in input_variables + prompt_fields:
|
||||
del prompt_request.frontend_node.template[key]
|
||||
return PromptValidationResponse(
|
||||
input_variables=input_variables,
|
||||
frontend_node=prompt_request.frontend_node,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
113
src/backend/base/langflow/api/v1/variable.py
Normal file
113
src/backend/base/langflow/api/v1/variable.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from langflow.services.auth import utils as auth_utils
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.database.models.variable import Variable, VariableCreate, VariableRead, VariableUpdate
|
||||
from langflow.services.deps import get_session, get_settings_service
|
||||
|
||||
router = APIRouter(prefix="/variables", tags=["Variables"])
|
||||
|
||||
|
||||
@router.post("/", response_model=VariableRead, status_code=201)
|
||||
def create_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable: VariableCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
settings_service=Depends(get_settings_service),
|
||||
):
|
||||
"""Create a new variable."""
|
||||
try:
|
||||
# check if variable name already exists
|
||||
variable_exists = session.exec(
|
||||
select(Variable).where(
|
||||
Variable.name == variable.name,
|
||||
Variable.user_id == current_user.id,
|
||||
)
|
||||
).first()
|
||||
if variable_exists:
|
||||
raise HTTPException(status_code=400, detail="Variable name already exists")
|
||||
|
||||
variable_dict = variable.model_dump()
|
||||
variable_dict["user_id"] = current_user.id
|
||||
|
||||
db_variable = Variable.model_validate(variable_dict)
|
||||
if not db_variable.value:
|
||||
raise HTTPException(status_code=400, detail="Variable value cannot be empty")
|
||||
encrypted = auth_utils.encrypt_api_key(db_variable.value, settings_service=settings_service)
|
||||
db_variable.value = encrypted
|
||||
db_variable.user_id = current_user.id
|
||||
session.add(db_variable)
|
||||
session.commit()
|
||||
session.refresh(db_variable)
|
||||
return db_variable
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/", response_model=list[VariableRead], status_code=200)
|
||||
def read_variables(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Read all variables."""
|
||||
try:
|
||||
variables = session.exec(select(Variable).where(Variable.user_id == current_user.id)).all()
|
||||
return variables
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.patch("/{variable_id}", response_model=VariableRead, status_code=200)
|
||||
def update_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable_id: UUID,
|
||||
variable: VariableUpdate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Update a variable."""
|
||||
try:
|
||||
db_variable = session.exec(
|
||||
select(Variable).where(Variable.id == variable_id, Variable.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_variable:
|
||||
raise HTTPException(status_code=404, detail="Variable not found")
|
||||
|
||||
variable_data = variable.model_dump(exclude_unset=True)
|
||||
for key, value in variable_data.items():
|
||||
setattr(db_variable, key, value)
|
||||
db_variable.updated_at = datetime.utcnow()
|
||||
session.commit()
|
||||
session.refresh(db_variable)
|
||||
return db_variable
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.delete("/{variable_id}", status_code=204)
|
||||
def delete_variable(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
variable_id: UUID,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
"""Delete a variable."""
|
||||
try:
|
||||
db_variable = session.exec(
|
||||
select(Variable).where(Variable.id == variable_id, Variable.user_id == current_user.id)
|
||||
).first()
|
||||
if not db_variable:
|
||||
raise HTTPException(status_code=404, detail="Variable not found")
|
||||
session.delete(db_variable)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
75
src/backend/base/langflow/base/agents/agent.py
Normal file
75
src/backend/base/langflow/base/agents/agent.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from typing import List, Optional, Union, cast
|
||||
|
||||
from langchain.agents import AgentExecutor, BaseMultiActionAgent, BaseSingleActionAgent
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import BaseMemory, Text, Tool
|
||||
|
||||
|
||||
class LCAgentComponent(CustomComponent):
|
||||
def build_config(self):
|
||||
return {
|
||||
"lc": {
|
||||
"display_name": "LangChain",
|
||||
"info": "The LangChain to interact with.",
|
||||
},
|
||||
"handle_parsing_errors": {
|
||||
"display_name": "Handle Parsing Errors",
|
||||
"info": "If True, the agent will handle parsing errors. If False, the agent will raise an error.",
|
||||
"advanced": True,
|
||||
},
|
||||
"output_key": {
|
||||
"display_name": "Output Key",
|
||||
"info": "The key to use to get the output from the agent.",
|
||||
"advanced": True,
|
||||
},
|
||||
"memory": {
|
||||
"display_name": "Memory",
|
||||
"info": "Memory to use for the agent.",
|
||||
},
|
||||
"tools": {
|
||||
"display_name": "Tools",
|
||||
"info": "Tools the agent can use.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Input",
|
||||
"info": "Input text to pass to the agent.",
|
||||
},
|
||||
}
|
||||
|
||||
async def run_agent(
|
||||
self,
|
||||
agent: Union[Runnable, BaseSingleActionAgent, BaseMultiActionAgent, AgentExecutor],
|
||||
inputs: str,
|
||||
input_variables: list[str],
|
||||
tools: List[Tool],
|
||||
memory: Optional[BaseMemory] = None,
|
||||
handle_parsing_errors: bool = True,
|
||||
output_key: str = "output",
|
||||
) -> Text:
|
||||
if isinstance(agent, AgentExecutor):
|
||||
runnable = agent
|
||||
else:
|
||||
runnable = AgentExecutor.from_agent_and_tools(
|
||||
agent=agent, # type: ignore
|
||||
tools=tools,
|
||||
verbose=True,
|
||||
memory=memory,
|
||||
handle_parsing_errors=handle_parsing_errors,
|
||||
)
|
||||
input_dict = {"input": inputs}
|
||||
for var in input_variables:
|
||||
if var not in ["agent_scratchpad", "input"]:
|
||||
input_dict[var] = ""
|
||||
result = await runnable.ainvoke(input_dict)
|
||||
self.status = result
|
||||
if output_key in result:
|
||||
return cast(str, result.get(output_key))
|
||||
elif "output" not in result:
|
||||
if output_key != "output":
|
||||
raise ValueError(f"Output key not found in result. Tried '{output_key}' and 'output'.")
|
||||
else:
|
||||
raise ValueError("Output key not found in result. Tried 'output'.")
|
||||
|
||||
return cast(str, result.get("output"))
|
||||
27
src/backend/base/langflow/base/constants.py
Normal file
27
src/backend/base/langflow/base/constants.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""
|
||||
This module contains constants used in the Langflow base module.
|
||||
|
||||
Constants:
|
||||
- STREAM_INFO_TEXT: A string representing the information about streaming the response from the model.
|
||||
- NODE_FORMAT_ATTRIBUTES: A list of attributes used for formatting nodes.
|
||||
- FIELD_FORMAT_ATTRIBUTES: A list of attributes used for formatting fields.
|
||||
"""
|
||||
STREAM_INFO_TEXT = "Stream the response from the model. Streaming works only in Chat."
|
||||
|
||||
NODE_FORMAT_ATTRIBUTES = ["beta", "icon", "display_name", "description"]
|
||||
|
||||
|
||||
FIELD_FORMAT_ATTRIBUTES = [
|
||||
"info",
|
||||
"display_name",
|
||||
"required",
|
||||
"list",
|
||||
"multiline",
|
||||
"fileTypes",
|
||||
"password",
|
||||
"input_types",
|
||||
"title_case",
|
||||
"real_time_refresh",
|
||||
"refresh_button",
|
||||
"refresh_button_text",
|
||||
]
|
||||
141
src/backend/base/langflow/base/data/utils.py
Normal file
141
src/backend/base/langflow/base/data/utils.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from concurrent import futures
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Optional, Text
|
||||
|
||||
import yaml
|
||||
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
# Types of files that can be read simply by file.read()
|
||||
# and have 100% to be completely readable
|
||||
TEXT_FILE_TYPES = ["txt", "md", "mdx", "csv", "json", "yaml", "yml", "xml", "html", "htm", "pdf", "docx"]
|
||||
|
||||
|
||||
def is_hidden(path: Path) -> bool:
|
||||
return path.name.startswith(".")
|
||||
|
||||
|
||||
def retrieve_file_paths(
|
||||
path: str,
|
||||
load_hidden: bool,
|
||||
recursive: bool,
|
||||
depth: int,
|
||||
types: List[str] = TEXT_FILE_TYPES,
|
||||
) -> List[str]:
|
||||
path_obj = Path(path)
|
||||
if not path_obj.exists() or not path_obj.is_dir():
|
||||
raise ValueError(f"Path {path} must exist and be a directory.")
|
||||
|
||||
def match_types(p: Path) -> bool:
|
||||
return any(p.suffix == f".{t}" for t in types) if types else True
|
||||
|
||||
def is_not_hidden(p: Path) -> bool:
|
||||
return not is_hidden(p) or load_hidden
|
||||
|
||||
def walk_level(directory: Path, max_depth: int):
|
||||
directory = directory.resolve()
|
||||
prefix_length = len(directory.parts)
|
||||
for p in directory.rglob("*" if recursive else "[!.]*"):
|
||||
if len(p.parts) - prefix_length <= max_depth:
|
||||
yield p
|
||||
|
||||
glob = "**/*" if recursive else "*"
|
||||
paths = walk_level(path_obj, depth) if depth else path_obj.glob(glob)
|
||||
file_paths = [Text(p) for p in paths if p.is_file() and match_types(p) and is_not_hidden(p)]
|
||||
|
||||
return file_paths
|
||||
|
||||
|
||||
def partition_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
# Use the partition function to load the file
|
||||
from unstructured.partition.auto import partition # type: ignore
|
||||
|
||||
try:
|
||||
elements = partition(file_path)
|
||||
except Exception as e:
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
return None
|
||||
|
||||
# Create a Record
|
||||
text = "\n\n".join([Text(el) for el in elements])
|
||||
metadata = elements.metadata if hasattr(elements, "metadata") else {}
|
||||
metadata["file_path"] = file_path
|
||||
record = Record(text=text, data=metadata)
|
||||
return record
|
||||
|
||||
|
||||
def read_text_file(file_path: str) -> str:
|
||||
with open(file_path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def read_docx_file(file_path: str) -> str:
|
||||
from docx import Document # type: ignore
|
||||
|
||||
doc = Document(file_path)
|
||||
return "\n\n".join([p.text for p in doc.paragraphs])
|
||||
|
||||
|
||||
def parse_pdf_to_text(file_path: str) -> str:
|
||||
from pypdf import PdfReader # type: ignore
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
reader = PdfReader(f)
|
||||
return "\n\n".join([page.extract_text() for page in reader.pages])
|
||||
|
||||
|
||||
def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[Record]:
|
||||
try:
|
||||
if file_path.endswith(".pdf"):
|
||||
text = parse_pdf_to_text(file_path)
|
||||
elif file_path.endswith(".docx"):
|
||||
text = read_docx_file(file_path)
|
||||
else:
|
||||
text = read_text_file(file_path)
|
||||
# if file is json, yaml, or xml, we can parse it
|
||||
if file_path.endswith(".json"):
|
||||
text = json.loads(text)
|
||||
elif file_path.endswith(".yaml") or file_path.endswith(".yml"):
|
||||
text = yaml.safe_load(text)
|
||||
elif file_path.endswith(".xml"):
|
||||
xml_element = ET.fromstring(text)
|
||||
text = ET.tostring(xml_element, encoding="unicode")
|
||||
except Exception as e:
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Error loading file {file_path}: {e}") from e
|
||||
return None
|
||||
|
||||
record = Record(data={"file_path": file_path, "text": text})
|
||||
return record
|
||||
|
||||
|
||||
def get_elements(
|
||||
file_paths: List[str],
|
||||
silent_errors: bool,
|
||||
max_concurrency: int,
|
||||
use_multithreading: bool,
|
||||
) -> List[Optional[Record]]:
|
||||
if use_multithreading:
|
||||
records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
records = [partition_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
records = list(filter(None, records))
|
||||
return records
|
||||
|
||||
|
||||
def parallel_load_records(
|
||||
file_paths: List[str],
|
||||
silent_errors: bool,
|
||||
max_concurrency: int,
|
||||
load_function: Callable = parse_text_file_to_record,
|
||||
) -> List[Optional[Record]]:
|
||||
with futures.ThreadPoolExecutor(max_workers=max_concurrency) as executor:
|
||||
loaded_files = executor.map(
|
||||
lambda file_path: load_function(file_path, silent_errors),
|
||||
file_paths,
|
||||
)
|
||||
# loaded_files is an iterator, so we need to convert it to a list
|
||||
return list(loaded_files)
|
||||
118
src/backend/base/langflow/base/io/chat.py
Normal file
118
src/backend/base/langflow/base/io/chat.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import warnings
|
||||
from typing import Optional, Union
|
||||
|
||||
from langflow.field_typing import Text
|
||||
from langflow.helpers.record import records_to_text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.memory import add_messages
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ChatComponent(CustomComponent):
|
||||
display_name = "Chat Component"
|
||||
description = "Use as base for chat components."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"input_types": ["Text"],
|
||||
"display_name": "Message",
|
||||
"multiline": True,
|
||||
},
|
||||
"sender": {
|
||||
"options": ["Machine", "User"],
|
||||
"display_name": "Sender Type",
|
||||
"advanced": True,
|
||||
},
|
||||
"sender_name": {"display_name": "Sender Name"},
|
||||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "If provided, the message will be stored in the memory.",
|
||||
"advanced": True,
|
||||
},
|
||||
"return_record": {
|
||||
"display_name": "Return Record",
|
||||
"info": "Return the message as a record containing the sender, sender_name, and session_id.",
|
||||
"advanced": True,
|
||||
},
|
||||
"record_template": {
|
||||
"display_name": "Record Template",
|
||||
"multiline": True,
|
||||
"info": "In case of Message being a Record, this template will be used to convert it to text.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def store_message(
|
||||
self,
|
||||
message: Union[str, Text, Record],
|
||||
session_id: Optional[str] = None,
|
||||
sender: Optional[str] = None,
|
||||
sender_name: Optional[str] = None,
|
||||
) -> list[Record]:
|
||||
if not message:
|
||||
warnings.warn("No message provided.")
|
||||
return []
|
||||
|
||||
if not session_id or not sender or not sender_name:
|
||||
raise ValueError("All of session_id, sender, and sender_name must be provided.")
|
||||
if isinstance(message, Record):
|
||||
record = message
|
||||
record.data.update(
|
||||
{
|
||||
"session_id": session_id,
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
record = Record(
|
||||
data={
|
||||
"text": message,
|
||||
"session_id": session_id,
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
},
|
||||
)
|
||||
|
||||
self.status = record
|
||||
records = add_messages([record])
|
||||
return records[0]
|
||||
|
||||
def build(
|
||||
self,
|
||||
sender: Optional[str] = "User",
|
||||
sender_name: Optional[str] = "User",
|
||||
input_value: Optional[Union[str, Record]] = None,
|
||||
session_id: Optional[str] = None,
|
||||
return_record: Optional[bool] = False,
|
||||
record_template: str = "Text: {text}\nData: {data}",
|
||||
) -> Union[Text, Record]:
|
||||
input_value_record: Optional[Record] = None
|
||||
if return_record:
|
||||
if isinstance(input_value, Record):
|
||||
# Update the data of the record
|
||||
input_value.data["sender"] = sender
|
||||
input_value.data["sender_name"] = sender_name
|
||||
input_value.data["session_id"] = session_id
|
||||
else:
|
||||
input_value_record = Record(
|
||||
text=input_value,
|
||||
data={
|
||||
"sender": sender,
|
||||
"sender_name": sender_name,
|
||||
"session_id": session_id,
|
||||
},
|
||||
)
|
||||
elif isinstance(input_value, Record):
|
||||
input_value = records_to_text(template=record_template, records=input_value)
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
if return_record and input_value_record:
|
||||
result: Union[Text, Record] = input_value_record
|
||||
else:
|
||||
result = input_value
|
||||
self.status = result
|
||||
if session_id and isinstance(result, (Record, str)):
|
||||
self.store_message(result, session_id, sender, sender_name)
|
||||
return result
|
||||
42
src/backend/base/langflow/base/io/text.py
Normal file
42
src/backend/base/langflow/base/io/text.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow.field_typing import Text
|
||||
from langflow.helpers.record import records_to_text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class TextComponent(CustomComponent):
|
||||
display_name = "Text Component"
|
||||
description = "Used to pass text to the next component."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"display_name": "Value",
|
||||
"input_types": ["Text", "Record"],
|
||||
"info": "Text or Record to be passed.",
|
||||
},
|
||||
"record_template": {
|
||||
"display_name": "Record Template",
|
||||
"multiline": True,
|
||||
"info": "Template to convert Record to Text. If left empty, it will be dynamically set to the Record's text key.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Optional[Text] = "",
|
||||
record_template: Optional[str] = "{text}",
|
||||
) -> Text:
|
||||
if isinstance(input_value, Record):
|
||||
if record_template == "":
|
||||
# it should be dynamically set to the Record's .text_key value
|
||||
# meaning, if text_key = "bacon", then record_template = "{bacon}"
|
||||
record_template = "{" + input_value.text_key + "}"
|
||||
input_value = records_to_text(template=record_template, records=input_value)
|
||||
self.status = input_value
|
||||
if not input_value:
|
||||
input_value = ""
|
||||
return input_value
|
||||
3
src/backend/base/langflow/base/models/__init__.py
Normal file
3
src/backend/base/langflow/base/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .model import LCModelComponent
|
||||
|
||||
__all__ = ["LCModelComponent"]
|
||||
50
src/backend/base/langflow/base/models/model.py
Normal file
50
src/backend/base/langflow/base/models/model.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from typing import Optional, Union
|
||||
|
||||
from langchain_core.language_models.chat_models import BaseChatModel
|
||||
from langchain_core.language_models.llms import LLM
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class LCModelComponent(CustomComponent):
|
||||
display_name: str = "Model Name"
|
||||
description: str = "Model Description"
|
||||
|
||||
def get_result(self, runnable: LLM, stream: bool, input_value: str):
|
||||
"""
|
||||
Retrieves the result from the output of a Runnable object.
|
||||
|
||||
Args:
|
||||
output (Runnable): The output object to retrieve the result from.
|
||||
stream (bool): Indicates whether to use streaming or invocation mode.
|
||||
input_value (str): The input value to pass to the output object.
|
||||
|
||||
Returns:
|
||||
The result obtained from the output object.
|
||||
"""
|
||||
if stream:
|
||||
result = runnable.stream(input_value)
|
||||
else:
|
||||
message = runnable.invoke(input_value)
|
||||
result = message.content if hasattr(message, "content") else message
|
||||
self.status = result
|
||||
return result
|
||||
|
||||
def get_chat_result(
|
||||
self, runnable: BaseChatModel, stream: bool, input_value: str, system_message: Optional[str] = None
|
||||
):
|
||||
messages: list[Union[HumanMessage, SystemMessage]] = []
|
||||
if not input_value and not system_message:
|
||||
raise ValueError("The message you want to send to the model is empty.")
|
||||
if system_message:
|
||||
messages.append(SystemMessage(content=system_message))
|
||||
if input_value:
|
||||
messages.append(HumanMessage(content=input_value))
|
||||
if stream:
|
||||
return runnable.stream(messages)
|
||||
else:
|
||||
message = runnable.invoke(messages)
|
||||
result = message.content
|
||||
self.status = result
|
||||
return result
|
||||
137
src/backend/base/langflow/base/prompts/utils.py
Normal file
137
src/backend/base/langflow/base/prompts/utils.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
from fastapi import HTTPException
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain_core.documents import Document
|
||||
from loguru import logger
|
||||
|
||||
from langflow.api.v1.base import INVALID_NAMES, check_input_variables
|
||||
from langflow.interface.utils import extract_input_variables_from_prompt
|
||||
from langflow.schema import Record
|
||||
from langflow.template.field.prompt import DefaultPromptField
|
||||
|
||||
|
||||
def dict_values_to_string(d: dict) -> dict:
|
||||
"""
|
||||
Converts the values of a dictionary to strings.
|
||||
|
||||
Args:
|
||||
d (dict): The dictionary whose values need to be converted.
|
||||
|
||||
Returns:
|
||||
dict: The dictionary with values converted to strings.
|
||||
"""
|
||||
# Do something similar to the above
|
||||
for key, value in d.items():
|
||||
# it could be a list of records or documents or strings
|
||||
if isinstance(value, list):
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, Record):
|
||||
d[key][i] = record_to_string(item)
|
||||
elif isinstance(item, Document):
|
||||
d[key][i] = document_to_string(item)
|
||||
elif isinstance(value, Record):
|
||||
d[key] = record_to_string(value)
|
||||
elif isinstance(value, Document):
|
||||
d[key] = document_to_string(value)
|
||||
return d
|
||||
|
||||
|
||||
def record_to_string(record: Record) -> str:
|
||||
"""
|
||||
Convert a record to a string.
|
||||
|
||||
Args:
|
||||
record (Record): The record to convert.
|
||||
|
||||
Returns:
|
||||
str: The record as a string.
|
||||
"""
|
||||
return record.get_text()
|
||||
|
||||
|
||||
def document_to_string(document: Document) -> str:
|
||||
"""
|
||||
Convert a document to a string.
|
||||
|
||||
Args:
|
||||
document (Document): The document to convert.
|
||||
|
||||
Returns:
|
||||
str: The document as a string.
|
||||
"""
|
||||
return document.page_content
|
||||
|
||||
|
||||
def validate_prompt(prompt_template: str, silent_errors: bool = False) -> list[str]:
|
||||
input_variables = extract_input_variables_from_prompt(prompt_template)
|
||||
|
||||
# Check if there are invalid characters in the input_variables
|
||||
input_variables = check_input_variables(input_variables)
|
||||
if any(var in INVALID_NAMES for var in input_variables):
|
||||
raise ValueError(f"Invalid input variables. None of the variables can be named {', '.join(input_variables)}. ")
|
||||
|
||||
try:
|
||||
PromptTemplate(template=prompt_template, input_variables=input_variables)
|
||||
except Exception as exc:
|
||||
logger.error(f"Invalid prompt: {exc}")
|
||||
if not silent_errors:
|
||||
raise ValueError(f"Invalid prompt: {exc}") from exc
|
||||
|
||||
return input_variables
|
||||
|
||||
|
||||
def get_old_custom_fields(custom_fields, name):
|
||||
try:
|
||||
if len(custom_fields) == 1 and 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
|
||||
name = list(custom_fields.keys())[0]
|
||||
|
||||
old_custom_fields = custom_fields[name]
|
||||
if not old_custom_fields:
|
||||
old_custom_fields = []
|
||||
|
||||
old_custom_fields = old_custom_fields.copy()
|
||||
except KeyError:
|
||||
old_custom_fields = []
|
||||
custom_fields[name] = []
|
||||
return old_custom_fields
|
||||
|
||||
|
||||
def add_new_variables_to_template(input_variables, custom_fields, template, name):
|
||||
for variable in input_variables:
|
||||
try:
|
||||
template_field = DefaultPromptField(name=variable, display_name=variable)
|
||||
if variable in template:
|
||||
# Set the new field with the old value
|
||||
template_field.value = template[variable]["value"]
|
||||
|
||||
template[variable] = template_field.to_dict()
|
||||
|
||||
# Check if variable is not already in the list before appending
|
||||
if variable not in custom_fields[name]:
|
||||
custom_fields[name].append(variable)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def remove_old_variables_from_template(old_custom_fields, input_variables, custom_fields, template, name):
|
||||
for variable in old_custom_fields:
|
||||
if variable not in input_variables:
|
||||
try:
|
||||
# Remove the variable from custom_fields associated with the given name
|
||||
if variable in custom_fields[name]:
|
||||
custom_fields[name].remove(variable)
|
||||
|
||||
# Remove the variable from the template
|
||||
template.pop(variable, None)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
|
||||
def update_input_variables_field(input_variables, template):
|
||||
if "input_variables" in template:
|
||||
template["input_variables"]["value"] = input_variables
|
||||
17
src/backend/base/langflow/components/__init__.py
Normal file
17
src/backend/base/langflow/components/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
__all__ = [
|
||||
"agents",
|
||||
"chains",
|
||||
"documentloaders",
|
||||
"embeddings",
|
||||
"experimental",
|
||||
"inputs",
|
||||
"memories",
|
||||
"model_specs",
|
||||
"outputs",
|
||||
"retrievers",
|
||||
"textsplitters",
|
||||
"toolkits",
|
||||
"tools",
|
||||
"vectorsearch",
|
||||
"vectorstores",
|
||||
]
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
from typing import Callable, List, Optional, Union
|
||||
|
||||
from langchain.agents import AgentExecutor, AgentType, initialize_agent, types
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langflow.field_typing import BaseChatMemory, BaseLanguageModel, Tool
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class AgentInitializerComponent(CustomComponent):
|
||||
34
src/backend/base/langflow/components/agents/CSVAgent.py
Normal file
34
src/backend/base/langflow/components/agents/CSVAgent.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from langchain_experimental.agents.agent_toolkits.csv.base import create_csv_agent
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import AgentExecutor, BaseLanguageModel
|
||||
|
||||
|
||||
class CSVAgentComponent(CustomComponent):
|
||||
display_name = "CSVAgent"
|
||||
description = "Construct a CSV agent from a CSV and tools."
|
||||
documentation = "https://python.langchain.com/docs/modules/agents/toolkits/csv"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM", "type": BaseLanguageModel},
|
||||
"path": {"display_name": "Path", "field_type": "file", "suffixes": [".csv"], "file_types": [".csv"]},
|
||||
"handle_parsing_errors": {"display_name": "Handle Parse Errors", "advanced": True},
|
||||
"agent_type": {
|
||||
"display_name": "Agent Type",
|
||||
"options": ["zero-shot-react-description", "openai-functions", "openai-tools"],
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self, llm: BaseLanguageModel, path: str, handle_parsing_errors: bool = True, agent_type: str = "openai-tools"
|
||||
) -> AgentExecutor:
|
||||
# Instantiate and return the CSV agent class with the provided llm and path
|
||||
return create_csv_agent(
|
||||
llm=llm,
|
||||
path=path,
|
||||
agent_type=agent_type,
|
||||
verbose=True,
|
||||
agent_executor_kwargs=dict(handle_parsing_errors=handle_parsing_errors),
|
||||
)
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.agents import AgentExecutor, create_json_agent
|
||||
from langchain_community.agent_toolkits.json.toolkit import JsonToolkit
|
||||
|
||||
from langflow.field_typing import (
|
||||
BaseLanguageModel,
|
||||
)
|
||||
from langchain_community.agent_toolkits.json.toolkit import JsonToolkit
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class JsonAgentComponent(CustomComponent):
|
||||
|
|
@ -9,13 +9,15 @@ from langchain.prompts.chat import MessagesPlaceholder
|
|||
from langchain.schema.memory import BaseMemory
|
||||
from langchain.tools import Tool
|
||||
from langchain_community.chat_models import ChatOpenAI
|
||||
from langflow import CustomComponent
|
||||
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class ConversationalAgent(CustomComponent):
|
||||
display_name: str = "OpenAI Conversational Agent"
|
||||
description: str = "Conversational Agent that can use OpenAI's function calling API"
|
||||
icon = "OpenAI"
|
||||
|
||||
def build_config(self):
|
||||
openai_function_models = [
|
||||
|
|
@ -40,7 +42,7 @@ class ConversationalAgent(CustomComponent):
|
|||
"temperature": {
|
||||
"display_name": "Temperature",
|
||||
"value": 0.2,
|
||||
"range_spec": RangeSpec(min=0, max=2, step=0.1),
|
||||
"rangeSpec": RangeSpec(min=0, max=2, step=0.1),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Union, Callable
|
||||
from typing import Callable, Union
|
||||
|
||||
from langchain.agents import AgentExecutor
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
from langchain_community.agent_toolkits.sql.base import create_sql_agent
|
||||
from langchain.sql_database import SQLDatabase
|
||||
from langchain_community.agent_toolkits import SQLDatabaseToolkit
|
||||
from langchain_community.agent_toolkits.sql.base import create_sql_agent
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class SQLAgentComponent(CustomComponent):
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Callable, Union
|
||||
|
||||
from langchain.agents import AgentExecutor, create_vectorstore_agent
|
||||
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreToolkit
|
||||
from typing import Union, Callable
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class VectorStoreAgentComponent(CustomComponent):
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain_core.language_models.base import BaseLanguageModel
|
||||
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreRouterToolkit
|
||||
from langchain.agents import create_vectorstore_router_agent
|
||||
from typing import Callable
|
||||
|
||||
from langchain.agents import create_vectorstore_router_agent
|
||||
from langchain.agents.agent_toolkits.vectorstore.toolkit import VectorStoreRouterToolkit
|
||||
from langchain_core.language_models.base import BaseLanguageModel
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class VectorStoreRouterAgentComponent(CustomComponent):
|
||||
display_name = "VectorStoreRouterAgent"
|
||||
89
src/backend/base/langflow/components/agents/XMLAgent.py
Normal file
89
src/backend/base/langflow/components/agents/XMLAgent.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
from typing import List, Optional
|
||||
|
||||
from langchain.agents import create_xml_agent
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
|
||||
from langflow.base.agents.agent import LCAgentComponent
|
||||
from langflow.field_typing import BaseLLM, BaseMemory, Text, Tool
|
||||
|
||||
|
||||
class XMLAgentComponent(LCAgentComponent):
|
||||
display_name = "XMLAgent"
|
||||
description = "Construct an XML agent from an LLM and tools."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"tools": {"display_name": "Tools"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt",
|
||||
"multiline": True,
|
||||
"info": "This prompt must contain 'tools' and 'agent_scratchpad' keys.",
|
||||
"value": """You are a helpful assistant. Help the user answer any questions.
|
||||
|
||||
You have access to the following tools:
|
||||
|
||||
{tools}
|
||||
|
||||
In order to use a tool, you can use <tool></tool> and <tool_input></tool_input> tags. You will then get back a response in the form <observation></observation>
|
||||
For example, if you have a tool called 'search' that could run a google search, in order to search for the weather in SF you would respond:
|
||||
|
||||
<tool>search</tool><tool_input>weather in SF</tool_input>
|
||||
<observation>64 degrees</observation>
|
||||
|
||||
When you are done, respond with a final answer between <final_answer></final_answer>. For example:
|
||||
|
||||
<final_answer>The weather in SF is 64 degrees</final_answer>
|
||||
|
||||
Begin!
|
||||
|
||||
Previous Conversation:
|
||||
{chat_history}
|
||||
|
||||
Question: {input}
|
||||
{agent_scratchpad}""",
|
||||
},
|
||||
"tool_template": {
|
||||
"display_name": "Tool Template",
|
||||
"info": "Template for rendering tools in the prompt. Tools have 'name' and 'description' keys.",
|
||||
"advanced": True,
|
||||
},
|
||||
"handle_parsing_errors": {
|
||||
"display_name": "Handle Parsing Errors",
|
||||
"info": "If True, the agent will handle parsing errors. If False, the agent will raise an error.",
|
||||
"advanced": True,
|
||||
},
|
||||
"memory": {
|
||||
"display_name": "Memory",
|
||||
"info": "Memory to use for the agent.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Inputs",
|
||||
"info": "Input text to pass to the agent.",
|
||||
},
|
||||
}
|
||||
|
||||
async def build(
|
||||
self,
|
||||
input_value: str,
|
||||
llm: BaseLLM,
|
||||
tools: List[Tool],
|
||||
prompt: str,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
tool_template: str = "{name}: {description}",
|
||||
handle_parsing_errors: bool = True,
|
||||
) -> Text:
|
||||
if "input" not in prompt:
|
||||
raise ValueError("Prompt must contain 'input' key.")
|
||||
|
||||
def render_tool_description(tools):
|
||||
return "\n".join(
|
||||
[tool_template.format(name=tool.name, description=tool.description, args=tool.args) for tool in tools]
|
||||
)
|
||||
|
||||
prompt_template = PromptTemplate.from_template(prompt)
|
||||
input_variables = prompt_template.input_variables
|
||||
agent = create_xml_agent(llm, tools, prompt_template, tools_renderer=render_tool_description)
|
||||
result = await self.run_agent(agent, input_value, input_variables, tools, memory, handle_parsing_errors)
|
||||
self.status = result
|
||||
return result
|
||||
19
src/backend/base/langflow/components/agents/__init__.py
Normal file
19
src/backend/base/langflow/components/agents/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from .AgentInitializer import AgentInitializerComponent
|
||||
from .CSVAgent import CSVAgentComponent
|
||||
from .JsonAgent import JsonAgentComponent
|
||||
from .OpenAIConversationalAgent import ConversationalAgent
|
||||
from .SQLAgent import SQLAgentComponent
|
||||
from .VectorStoreAgent import VectorStoreAgentComponent
|
||||
from .VectorStoreRouterAgent import VectorStoreRouterAgentComponent
|
||||
from .XMLAgent import XMLAgentComponent
|
||||
|
||||
__all__ = [
|
||||
"AgentInitializerComponent",
|
||||
"CSVAgentComponent",
|
||||
"JsonAgentComponent",
|
||||
"ConversationalAgent",
|
||||
"SQLAgentComponent",
|
||||
"VectorStoreAgentComponent",
|
||||
"VectorStoreRouterAgentComponent",
|
||||
"XMLAgentComponent",
|
||||
]
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import ConversationChain
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class ConversationChainComponent(CustomComponent):
|
||||
display_name = "ConversationChain"
|
||||
description = "Chain to have a conversation and load context from memory."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"prompt": {"display_name": "Prompt"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"memory": {
|
||||
"display_name": "Memory",
|
||||
"info": "Memory to load context from. If none is provided, a ConversationBufferMemory will be used.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
llm: BaseLanguageModel,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Text:
|
||||
if memory is None:
|
||||
chain = ConversationChain(llm=llm)
|
||||
else:
|
||||
chain = ConversationChain(llm=llm, memory=memory)
|
||||
result = chain.invoke({"input": input_value})
|
||||
if isinstance(result, dict):
|
||||
result = result.get(chain.output_key, "") # type: ignore
|
||||
|
||||
elif isinstance(result, str):
|
||||
result = result
|
||||
else:
|
||||
result = result.get("response")
|
||||
self.status = result
|
||||
return str(result)
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
from typing import Callable, Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from langchain.chains import LLMChain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import (
|
||||
BaseLanguageModel,
|
||||
BaseMemory,
|
||||
BasePromptTemplate,
|
||||
Chain,
|
||||
)
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BasePromptTemplate, Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class LLMChainComponent(CustomComponent):
|
||||
|
|
@ -20,7 +15,6 @@ class LLMChainComponent(CustomComponent):
|
|||
"prompt": {"display_name": "Prompt"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"memory": {"display_name": "Memory"},
|
||||
"code": {"show": False},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -28,5 +22,10 @@ class LLMChainComponent(CustomComponent):
|
|||
prompt: BasePromptTemplate,
|
||||
llm: BaseLanguageModel,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Union[Chain, Callable, LLMChain]:
|
||||
return LLMChain(prompt=prompt, llm=llm, memory=memory)
|
||||
) -> Text:
|
||||
runnable = LLMChain(prompt=prompt, llm=llm, memory=memory)
|
||||
result_dict = runnable.invoke({})
|
||||
output_key = runnable.output_key
|
||||
result = result_dict[output_key]
|
||||
self.status = result
|
||||
return result
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from langchain.chains import LLMCheckerChain
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class LLMCheckerChainComponent(CustomComponent):
|
||||
display_name = "LLMCheckerChain"
|
||||
description = ""
|
||||
documentation = "https://python.langchain.com/docs/modules/chains/additional/llm_checker"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
llm: BaseLanguageModel,
|
||||
) -> Text:
|
||||
chain = LLMCheckerChain.from_llm(llm=llm)
|
||||
response = chain.invoke({chain.input_key: input_value})
|
||||
result = response.get(chain.output_key, "")
|
||||
result_str = Text(result)
|
||||
self.status = result_str
|
||||
return result_str
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
from typing import Callable, Optional, Union
|
||||
from typing import Optional
|
||||
|
||||
from langchain.chains import LLMChain, LLMMathChain
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Chain
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class LLMMathChainComponent(CustomComponent):
|
||||
|
|
@ -18,14 +18,30 @@ class LLMMathChainComponent(CustomComponent):
|
|||
"memory": {"display_name": "Memory"},
|
||||
"input_key": {"display_name": "Input Key"},
|
||||
"output_key": {"display_name": "Output Key"},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
llm: BaseLanguageModel,
|
||||
llm_chain: LLMChain,
|
||||
input_key: str = "question",
|
||||
output_key: str = "answer",
|
||||
memory: Optional[BaseMemory] = None,
|
||||
) -> Union[LLMMathChain, Callable, Chain]:
|
||||
return LLMMathChain(llm=llm, llm_chain=llm_chain, input_key=input_key, output_key=output_key, memory=memory)
|
||||
) -> Text:
|
||||
chain = LLMMathChain(
|
||||
llm=llm,
|
||||
llm_chain=llm_chain,
|
||||
input_key=input_key,
|
||||
output_key=output_key,
|
||||
memory=memory,
|
||||
)
|
||||
response = chain.invoke({input_key: input_value})
|
||||
result = response.get(output_key)
|
||||
result_str = Text(result)
|
||||
self.status = result_str
|
||||
return result_str
|
||||
68
src/backend/base/langflow/components/chains/RetrievalQA.py
Normal file
68
src/backend/base/langflow/components/chains/RetrievalQA.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains.retrieval_qa.base import RetrievalQA
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class RetrievalQAComponent(CustomComponent):
|
||||
display_name = "Retrieval QA"
|
||||
description = "Chain for question-answering against an index."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"chain_type": {"display_name": "Chain Type", "options": ["Stuff", "Map Reduce", "Refine", "Map Rerank"]},
|
||||
"retriever": {"display_name": "Retriever"},
|
||||
"memory": {"display_name": "Memory", "required": False},
|
||||
"input_key": {"display_name": "Input Key", "advanced": True},
|
||||
"output_key": {"display_name": "Output Key", "advanced": True},
|
||||
"return_source_documents": {"display_name": "Return Source Documents"},
|
||||
"input_value": {
|
||||
"display_name": "Input",
|
||||
"input_types": ["Record", "Document"],
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
llm: BaseLanguageModel,
|
||||
chain_type: str,
|
||||
retriever: BaseRetriever,
|
||||
input_value: str = "",
|
||||
memory: Optional[BaseMemory] = None,
|
||||
input_key: str = "query",
|
||||
output_key: str = "result",
|
||||
return_source_documents: bool = True,
|
||||
) -> Text:
|
||||
chain_type = chain_type.lower().replace(" ", "_")
|
||||
runnable = RetrievalQA.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type=chain_type,
|
||||
retriever=retriever,
|
||||
memory=memory,
|
||||
input_key=input_key,
|
||||
output_key=output_key,
|
||||
return_source_documents=return_source_documents,
|
||||
)
|
||||
if isinstance(input_value, Document):
|
||||
input_value = input_value.page_content
|
||||
if isinstance(input_value, Record):
|
||||
input_value = input_value.get_text()
|
||||
self.status = runnable
|
||||
result = runnable.invoke({input_key: input_value})
|
||||
result = result.content if hasattr(result, "content") else result
|
||||
# Result is a dict with keys "query", "result" and "source_documents"
|
||||
# for now we just return the result
|
||||
records = self.to_records(result.get("source_documents"))
|
||||
references_str = ""
|
||||
if return_source_documents:
|
||||
references_str = self.create_references_from_records(records)
|
||||
result_str = result.get("result", "")
|
||||
|
||||
final_result = "\n".join([Text(result_str), references_str])
|
||||
self.status = final_result
|
||||
return final_result # OK
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import RetrievalQAWithSourcesChain
|
||||
from langchain_core.documents import Document
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel, BaseMemory, BaseRetriever, Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class RetrievalQAWithSourcesChainComponent(CustomComponent):
|
||||
display_name = "RetrievalQAWithSourcesChain"
|
||||
description = "Question-answering with sources over an index."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"llm": {"display_name": "LLM"},
|
||||
"chain_type": {
|
||||
"display_name": "Chain Type",
|
||||
"options": ["Stuff", "Map Reduce", "Refine", "Map Rerank"],
|
||||
"info": "The type of chain to use to combined Documents.",
|
||||
},
|
||||
"memory": {"display_name": "Memory"},
|
||||
"return_source_documents": {"display_name": "Return Source Documents"},
|
||||
"retriever": {"display_name": "Retriever"},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
retriever: BaseRetriever,
|
||||
llm: BaseLanguageModel,
|
||||
chain_type: str,
|
||||
memory: Optional[BaseMemory] = None,
|
||||
return_source_documents: Optional[bool] = True,
|
||||
) -> Text:
|
||||
chain_type = chain_type.lower().replace(" ", "_")
|
||||
runnable = RetrievalQAWithSourcesChain.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type=chain_type,
|
||||
memory=memory,
|
||||
return_source_documents=return_source_documents,
|
||||
retriever=retriever,
|
||||
)
|
||||
if isinstance(input_value, Document):
|
||||
input_value = input_value.page_content
|
||||
self.status = runnable
|
||||
input_key = runnable.input_keys[0]
|
||||
result = runnable.invoke({input_key: input_value})
|
||||
result = result.content if hasattr(result, "content") else result
|
||||
# Result is a dict with keys "query", "result" and "source_documents"
|
||||
# for now we just return the result
|
||||
records = self.to_records(result.get("source_documents"))
|
||||
references_str = ""
|
||||
if return_source_documents:
|
||||
references_str = self.create_references_from_records(records)
|
||||
result_str = Text(result.get("answer", ""))
|
||||
final_result = "\n".join([result_str, references_str])
|
||||
self.status = final_result
|
||||
return final_result
|
||||
61
src/backend/base/langflow/components/chains/SQLGenerator.py
Normal file
61
src/backend/base/langflow/components/chains/SQLGenerator.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.chains import create_sql_query_chain
|
||||
from langchain_community.utilities.sql_database import SQLDatabase
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow.field_typing import BaseLanguageModel, Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class SQLGeneratorComponent(CustomComponent):
|
||||
display_name = "Natural Language to SQL"
|
||||
description = "Generate SQL from natural language."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"db": {"display_name": "Database"},
|
||||
"llm": {"display_name": "LLM"},
|
||||
"prompt": {
|
||||
"display_name": "Prompt",
|
||||
"info": "The prompt must contain `{question}`.",
|
||||
},
|
||||
"top_k": {
|
||||
"display_name": "Top K",
|
||||
"info": "The number of results per select statement to return. If 0, no limit.",
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"info": "The input value to pass to the chain.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
db: SQLDatabase,
|
||||
llm: BaseLanguageModel,
|
||||
top_k: int = 5,
|
||||
prompt: Optional[Text] = None,
|
||||
) -> Text:
|
||||
if prompt:
|
||||
prompt_template = PromptTemplate.from_template(template=prompt)
|
||||
else:
|
||||
prompt_template = None
|
||||
|
||||
if top_k < 1:
|
||||
raise ValueError("Top K must be greater than 0.")
|
||||
|
||||
if not prompt_template:
|
||||
sql_query_chain = create_sql_query_chain(llm=llm, db=db, k=top_k)
|
||||
else:
|
||||
# Check if {question} is in the prompt
|
||||
if "{question}" not in prompt_template.template or "question" not in prompt_template.input_variables:
|
||||
raise ValueError("Prompt must contain `{question}` to be used with Natural Language to SQL.")
|
||||
sql_query_chain = create_sql_query_chain(llm=llm, db=db, prompt=prompt_template, k=top_k)
|
||||
query_writer: Runnable = sql_query_chain | {"query": lambda x: x.replace("SQLQuery:", "").strip()}
|
||||
response = query_writer.invoke({"question": input_value})
|
||||
query = response.get("query")
|
||||
self.status = query
|
||||
return query
|
||||
17
src/backend/base/langflow/components/chains/__init__.py
Normal file
17
src/backend/base/langflow/components/chains/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from .ConversationChain import ConversationChainComponent
|
||||
from .LLMChain import LLMChainComponent
|
||||
from .LLMCheckerChain import LLMCheckerChainComponent
|
||||
from .LLMMathChain import LLMMathChainComponent
|
||||
from .RetrievalQA import RetrievalQAComponent
|
||||
from .RetrievalQAWithSourcesChain import RetrievalQAWithSourcesChainComponent
|
||||
from .SQLGenerator import SQLGeneratorComponent
|
||||
|
||||
__all__ = [
|
||||
"ConversationChainComponent",
|
||||
"LLMChainComponent",
|
||||
"LLMCheckerChainComponent",
|
||||
"LLMMathChainComponent",
|
||||
"RetrievalQAComponent",
|
||||
"RetrievalQAWithSourcesChainComponent",
|
||||
"SQLGeneratorComponent",
|
||||
]
|
||||
121
src/backend/base/langflow/components/data/APIRequest.py
Normal file
121
src/backend/base/langflow/components/data/APIRequest.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import asyncio
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class APIRequest(CustomComponent):
|
||||
display_name: str = "API Request"
|
||||
description: str = "Make HTTP requests given one or more URLs."
|
||||
output_types: list[str] = ["Record"]
|
||||
documentation: str = "https://docs.langflow.org/components/utilities#api-request"
|
||||
icon = "Globe"
|
||||
|
||||
field_config = {
|
||||
"urls": {"display_name": "URLs", "info": "URLs to make requests to."},
|
||||
"method": {
|
||||
"display_name": "Method",
|
||||
"info": "The HTTP method to use.",
|
||||
"field_type": "str",
|
||||
"options": ["GET", "POST", "PATCH", "PUT"],
|
||||
"value": "GET",
|
||||
},
|
||||
"headers": {
|
||||
"display_name": "Headers",
|
||||
"info": "The headers to send with the request.",
|
||||
"input_types": ["Record"],
|
||||
},
|
||||
"body": {
|
||||
"display_name": "Body",
|
||||
"info": "The body to send with the request (for POST, PATCH, PUT).",
|
||||
"input_types": ["Record"],
|
||||
},
|
||||
"timeout": {
|
||||
"display_name": "Timeout",
|
||||
"field_type": "int",
|
||||
"info": "The timeout to use for the request.",
|
||||
"value": 5,
|
||||
},
|
||||
}
|
||||
|
||||
async def make_request(
|
||||
self,
|
||||
client: httpx.AsyncClient,
|
||||
method: str,
|
||||
url: str,
|
||||
headers: Optional[Record] = None,
|
||||
body: Optional[Record] = None,
|
||||
timeout: int = 5,
|
||||
) -> Record:
|
||||
method = method.upper()
|
||||
if method not in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
data = body if body else None
|
||||
payload = json.dumps(data)
|
||||
try:
|
||||
response = await client.request(method, url, headers=headers, content=payload, timeout=timeout)
|
||||
try:
|
||||
result = response.json()
|
||||
except Exception:
|
||||
result = response.text
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": response.status_code,
|
||||
"result": result,
|
||||
},
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 408,
|
||||
"error": "Request timed out",
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
return Record(
|
||||
data={
|
||||
"source": url,
|
||||
"headers": headers,
|
||||
"status_code": 500,
|
||||
"error": str(exc),
|
||||
},
|
||||
)
|
||||
|
||||
async def build(
|
||||
self,
|
||||
method: str,
|
||||
urls: List[str],
|
||||
_headers: Optional[Record] = None,
|
||||
body: Optional[Record] = None,
|
||||
timeout: int = 5,
|
||||
) -> List[Record]:
|
||||
if _headers is None:
|
||||
headers = {}
|
||||
else:
|
||||
headers = _headers.data
|
||||
|
||||
bodies = []
|
||||
if body:
|
||||
if isinstance(body, list):
|
||||
bodies = [b.data for b in body]
|
||||
else:
|
||||
bodies = [body.data]
|
||||
|
||||
if len(urls) != len(bodies):
|
||||
# add bodies with None
|
||||
bodies += [None] * (len(urls) - len(bodies)) # type: ignore
|
||||
async with httpx.AsyncClient() as client:
|
||||
results = await asyncio.gather(
|
||||
*[self.make_request(client, method, u, headers, rec, timeout) for u, rec in zip(urls, bodies)]
|
||||
)
|
||||
self.status = results
|
||||
return results
|
||||
63
src/backend/base/langflow/components/data/Directory.py
Normal file
63
src/backend/base/langflow/components/data/Directory.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from langflow.base.data.utils import parallel_load_records, parse_text_file_to_record, retrieve_file_paths
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class DirectoryComponent(CustomComponent):
|
||||
display_name = "Directory"
|
||||
description = "Recursively load files from a directory."
|
||||
icon = "folder"
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"path": {"display_name": "Path"},
|
||||
"types": {
|
||||
"display_name": "Types",
|
||||
"info": "File types to load. Leave empty to load all types.",
|
||||
},
|
||||
"depth": {"display_name": "Depth", "info": "Depth to search for files."},
|
||||
"max_concurrency": {"display_name": "Max Concurrency", "advanced": True},
|
||||
"load_hidden": {
|
||||
"display_name": "Load Hidden",
|
||||
"advanced": True,
|
||||
"info": "If true, hidden files will be loaded.",
|
||||
},
|
||||
"recursive": {
|
||||
"display_name": "Recursive",
|
||||
"advanced": True,
|
||||
"info": "If true, the search will be recursive.",
|
||||
},
|
||||
"silent_errors": {
|
||||
"display_name": "Silent Errors",
|
||||
"advanced": True,
|
||||
"info": "If true, errors will not raise an exception.",
|
||||
},
|
||||
"use_multithreading": {
|
||||
"display_name": "Use Multithreading",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
depth: int = 0,
|
||||
max_concurrency: int = 2,
|
||||
load_hidden: bool = False,
|
||||
recursive: bool = True,
|
||||
silent_errors: bool = False,
|
||||
use_multithreading: bool = True,
|
||||
) -> List[Optional[Record]]:
|
||||
resolved_path = self.resolve_path(path)
|
||||
file_paths = retrieve_file_paths(resolved_path, load_hidden, recursive, depth)
|
||||
loaded_records = []
|
||||
|
||||
if use_multithreading:
|
||||
loaded_records = parallel_load_records(file_paths, silent_errors, max_concurrency)
|
||||
else:
|
||||
loaded_records = [parse_text_file_to_record(file_path, silent_errors) for file_path in file_paths]
|
||||
loaded_records = list(filter(None, loaded_records))
|
||||
self.status = loaded_records
|
||||
return loaded_records
|
||||
48
src/backend/base/langflow/components/data/File.py
Normal file
48
src/backend/base/langflow/components/data/File.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class FileComponent(CustomComponent):
|
||||
display_name = "File"
|
||||
description = "A generic file loader."
|
||||
icon = "file-text"
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"path": {
|
||||
"display_name": "Path",
|
||||
"field_type": "file",
|
||||
"file_types": TEXT_FILE_TYPES,
|
||||
"info": f"Supported file types: {', '.join(TEXT_FILE_TYPES)}",
|
||||
},
|
||||
"silent_errors": {
|
||||
"display_name": "Silent Errors",
|
||||
"advanced": True,
|
||||
"info": "If true, errors will not raise an exception.",
|
||||
},
|
||||
}
|
||||
|
||||
def load_file(self, path: str, silent_errors: bool = False) -> Record:
|
||||
resolved_path = self.resolve_path(path)
|
||||
path_obj = Path(resolved_path)
|
||||
extension = path_obj.suffix[1:].lower()
|
||||
if extension == "doc":
|
||||
raise ValueError("doc files are not supported. Please save as .docx")
|
||||
if extension not in TEXT_FILE_TYPES:
|
||||
raise ValueError(f"Unsupported file type: {extension}")
|
||||
record = parse_text_file_to_record(resolved_path, silent_errors)
|
||||
self.status = record if record else "No data"
|
||||
return record or Record()
|
||||
|
||||
def build(
|
||||
self,
|
||||
path: str,
|
||||
silent_errors: bool = False,
|
||||
) -> Record:
|
||||
record = self.load_file(path, silent_errors)
|
||||
self.status = record
|
||||
return record
|
||||
27
src/backend/base/langflow/components/data/URL.py
Normal file
27
src/backend/base/langflow/components/data/URL.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
from langchain_community.document_loaders.web_base import WebBaseLoader
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class URLComponent(CustomComponent):
|
||||
display_name = "URL"
|
||||
description = "Fetch content from one or more URLs."
|
||||
icon = "layout-template"
|
||||
|
||||
def build_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"urls": {"display_name": "URL"},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
urls: list[str],
|
||||
) -> list[Record]:
|
||||
loader = WebBaseLoader(web_paths=urls)
|
||||
docs = loader.load()
|
||||
records = self.to_records(docs)
|
||||
self.status = records
|
||||
return records
|
||||
7
src/backend/base/langflow/components/data/__init__.py
Normal file
7
src/backend/base/langflow/components/data/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from .APIRequest import APIRequest
|
||||
from .Directory import DirectoryComponent
|
||||
from .File import FileComponent
|
||||
|
||||
from .URL import URLComponent
|
||||
|
||||
__all__ = ["APIRequest", "DirectoryComponent", "FileComponent", "URLComponent"]
|
||||
|
|
@ -1,19 +1,15 @@
|
|||
from typing import Optional
|
||||
|
||||
from langchain.embeddings import BedrockEmbeddings
|
||||
from langchain.embeddings.base import Embeddings
|
||||
from langflow import CustomComponent
|
||||
from langchain_community.embeddings import BedrockEmbeddings
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class AmazonBedrockEmeddingsComponent(CustomComponent):
|
||||
"""
|
||||
A custom component for implementing an Embeddings Model using Amazon Bedrock.
|
||||
"""
|
||||
|
||||
display_name: str = "Amazon Bedrock Embeddings"
|
||||
description: str = "Embeddings model from Amazon Bedrock."
|
||||
description: str = "Generate embeddings using Amazon Bedrock models."
|
||||
documentation = "https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/bedrock"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
from langchain.embeddings.base import Embeddings
|
||||
from langchain_community.embeddings import AzureOpenAIEmbeddings
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class AzureOpenAIEmbeddingsComponent(CustomComponent):
|
||||
display_name: str = "AzureOpenAIEmbeddings"
|
||||
description: str = "Embeddings model from Azure OpenAI."
|
||||
display_name: str = "Azure OpenAI Embeddings"
|
||||
description: str = "Generate embeddings using Azure OpenAI models."
|
||||
documentation: str = "https://python.langchain.com/docs/integrations/text_embedding/azureopenai"
|
||||
beta = False
|
||||
icon = "Azure"
|
||||
|
||||
API_VERSION_OPTIONS = [
|
||||
"2022-12-01",
|
||||
|
|
@ -2,12 +2,12 @@ from typing import Optional
|
|||
|
||||
from langchain_community.embeddings.cohere import CohereEmbeddings
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langflow.custom import CustomComponent
|
||||
|
||||
|
||||
class CohereEmbeddingsComponent(CustomComponent):
|
||||
display_name = "CohereEmbeddings"
|
||||
description = "Cohere embedding models."
|
||||
display_name = "Cohere Embeddings"
|
||||
description = "Generate embeddings using Cohere models."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
@ -16,6 +16,7 @@ class CohereEmbeddingsComponent(CustomComponent):
|
|||
"truncate": {"display_name": "Truncate", "advanced": True},
|
||||
"max_retries": {"display_name": "Max Retries", "advanced": True},
|
||||
"user_agent": {"display_name": "User Agent", "advanced": True},
|
||||
"request_timeout": {"display_name": "Request Timeout", "advanced": True},
|
||||
}
|
||||
|
||||
def build(
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
from langflow import CustomComponent
|
||||
from typing import Optional, Dict
|
||||
from typing import Dict, Optional
|
||||
|
||||
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class HuggingFaceEmbeddingsComponent(CustomComponent):
|
||||
display_name = "HuggingFaceEmbeddings"
|
||||
description = "HuggingFace sentence_transformers embedding models."
|
||||
display_name = "Hugging Face Embeddings"
|
||||
description = "Generate embeddings using HuggingFace models."
|
||||
documentation = (
|
||||
"https://python.langchain.com/docs/modules/data_connection/text_embedding/integrations/sentence_transformers"
|
||||
)
|
||||
icon = "HuggingFace"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
from typing import Dict, Optional
|
||||
|
||||
from langchain_community.embeddings.huggingface import HuggingFaceInferenceAPIEmbeddings
|
||||
from langflow import CustomComponent
|
||||
from pydantic.v1.types import SecretStr
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class HuggingFaceInferenceAPIEmbeddingsComponent(CustomComponent):
|
||||
display_name = "HuggingFaceInferenceAPIEmbeddings"
|
||||
description = "HuggingFace sentence_transformers embedding models, API version."
|
||||
display_name = "Hugging Face API Embeddings"
|
||||
description = "Generate embeddings using Hugging Face Inference API models."
|
||||
documentation = "https://github.com/huggingface/text-embeddings-inference"
|
||||
icon = "HuggingFace"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
@ -1,19 +1,15 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow import CustomComponent
|
||||
from langchain.embeddings.base import Embeddings
|
||||
from langchain_community.embeddings import OllamaEmbeddings
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class OllamaEmbeddingsComponent(CustomComponent):
|
||||
"""
|
||||
A custom component for implementing an Embeddings Model using Ollama.
|
||||
"""
|
||||
|
||||
display_name: str = "Ollama Embeddings"
|
||||
description: str = "Embeddings model from Ollama."
|
||||
description: str = "Generate embeddings using Ollama models."
|
||||
documentation = "https://python.langchain.com/docs/integrations/text_embedding/ollama"
|
||||
beta = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from langchain_openai.embeddings.base import OpenAIEmbeddings
|
||||
from langflow import CustomComponent
|
||||
from langflow.field_typing import NestedDict
|
||||
from pydantic.v1.types import SecretStr
|
||||
|
||||
from langflow.field_typing import Embeddings, NestedDict
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class OpenAIEmbeddingsComponent(CustomComponent):
|
||||
display_name = "OpenAIEmbeddings"
|
||||
description = "OpenAI embedding models"
|
||||
display_name = "OpenAI Embeddings"
|
||||
description = "Generate embeddings using OpenAI models."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
|
|
@ -45,12 +45,24 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
"model": {
|
||||
"display_name": "Model",
|
||||
"advanced": False,
|
||||
"options": ["text-embedding-3-small", "text-embedding-3-large", "text-embedding-ada-002"],
|
||||
"options": [
|
||||
"text-embedding-3-small",
|
||||
"text-embedding-3-large",
|
||||
"text-embedding-ada-002",
|
||||
],
|
||||
},
|
||||
"model_kwargs": {"display_name": "Model Kwargs", "advanced": True},
|
||||
"openai_api_base": {"display_name": "OpenAI API Base", "password": True, "advanced": True},
|
||||
"openai_api_base": {
|
||||
"display_name": "OpenAI API Base",
|
||||
"password": True,
|
||||
"advanced": True,
|
||||
},
|
||||
"openai_api_key": {"display_name": "OpenAI API Key", "password": True},
|
||||
"openai_api_type": {"display_name": "OpenAI API Type", "advanced": True, "password": True},
|
||||
"openai_api_type": {
|
||||
"display_name": "OpenAI API Type",
|
||||
"advanced": True,
|
||||
"password": True,
|
||||
},
|
||||
"openai_api_version": {
|
||||
"display_name": "OpenAI API Version",
|
||||
"advanced": True,
|
||||
|
|
@ -66,25 +78,28 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
"advanced": True,
|
||||
},
|
||||
"skip_empty": {"display_name": "Skip Empty", "advanced": True},
|
||||
"tiktoken_model_name": {"display_name": "TikToken Model Name"},
|
||||
"tikToken_enable": {"display_name": "TikToken Enable", "advanced": True},
|
||||
"tiktoken_model_name": {
|
||||
"display_name": "TikToken Model Name",
|
||||
"advanced": True,
|
||||
},
|
||||
"tiktoken_enable": {"display_name": "TikToken Enable", "advanced": True},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
openai_api_key: str,
|
||||
default_headers: Optional[Dict[str, str]] = None,
|
||||
default_query: Optional[NestedDict] = {},
|
||||
allowed_special: List[str] = [],
|
||||
disallowed_special: List[str] = ["all"],
|
||||
chunk_size: int = 1000,
|
||||
client: Optional[Any] = None,
|
||||
deployment: str = "text-embedding-3-small",
|
||||
deployment: str = "text-embedding-ada-002",
|
||||
embedding_ctx_length: int = 8191,
|
||||
max_retries: int = 6,
|
||||
model: str = "text-embedding-3-small",
|
||||
model: str = "text-embedding-ada-002",
|
||||
model_kwargs: NestedDict = {},
|
||||
openai_api_base: Optional[str] = None,
|
||||
openai_api_key: Optional[str] = "",
|
||||
openai_api_type: Optional[str] = None,
|
||||
openai_api_version: Optional[str] = None,
|
||||
openai_organization: Optional[str] = None,
|
||||
|
|
@ -94,13 +109,11 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
skip_empty: bool = False,
|
||||
tiktoken_enable: bool = True,
|
||||
tiktoken_model_name: Optional[str] = None,
|
||||
) -> Union[OpenAIEmbeddings, Callable]:
|
||||
) -> Embeddings:
|
||||
# This is to avoid errors with Vector Stores (e.g Chroma)
|
||||
if disallowed_special == ["all"]:
|
||||
disallowed_special = "all" # type: ignore
|
||||
|
||||
api_key = SecretStr(openai_api_key) if openai_api_key else None
|
||||
|
||||
return OpenAIEmbeddings(
|
||||
tiktoken_enabled=tiktoken_enable,
|
||||
default_headers=default_headers,
|
||||
|
|
@ -115,7 +128,7 @@ class OpenAIEmbeddingsComponent(CustomComponent):
|
|||
model=model,
|
||||
model_kwargs=model_kwargs,
|
||||
base_url=openai_api_base,
|
||||
api_key=api_key,
|
||||
api_key=openai_api_key,
|
||||
openai_api_type=openai_api_type,
|
||||
api_version=openai_api_version,
|
||||
organization=openai_organization,
|
||||
|
|
@ -1,25 +1,55 @@
|
|||
from langflow import CustomComponent
|
||||
from langchain.embeddings import VertexAIEmbeddings
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
|
||||
from langchain_community.embeddings import VertexAIEmbeddings
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class VertexAIEmbeddingsComponent(CustomComponent):
|
||||
display_name = "VertexAIEmbeddings"
|
||||
description = "Google Cloud VertexAI embedding models."
|
||||
display_name = "VertexAI Embeddings"
|
||||
description = "Generate embeddings using Google Cloud VertexAI models."
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"credentials": {"display_name": "Credentials", "value": "", "file_types": [".json"], "field_type": "file"},
|
||||
"instance": {"display_name": "instance", "advanced": True, "field_type": "dict"},
|
||||
"location": {"display_name": "Location", "value": "us-central1", "advanced": True},
|
||||
"credentials": {
|
||||
"display_name": "Credentials",
|
||||
"value": "",
|
||||
"file_types": [".json"],
|
||||
"field_type": "file",
|
||||
},
|
||||
"instance": {
|
||||
"display_name": "instance",
|
||||
"advanced": True,
|
||||
"field_type": "dict",
|
||||
},
|
||||
"location": {
|
||||
"display_name": "Location",
|
||||
"value": "us-central1",
|
||||
"advanced": True,
|
||||
},
|
||||
"max_output_tokens": {"display_name": "Max Output Tokens", "value": 128},
|
||||
"max_retries": {"display_name": "Max Retries", "value": 6, "advanced": True},
|
||||
"model_name": {"display_name": "Model Name", "value": "textembedding-gecko"},
|
||||
"max_retries": {
|
||||
"display_name": "Max Retries",
|
||||
"value": 6,
|
||||
"advanced": True,
|
||||
},
|
||||
"model_name": {
|
||||
"display_name": "Model Name",
|
||||
"value": "textembedding-gecko",
|
||||
},
|
||||
"n": {"display_name": "N", "value": 1, "advanced": True},
|
||||
"project": {"display_name": "Project", "advanced": True},
|
||||
"request_parallelism": {"display_name": "Request Parallelism", "value": 5, "advanced": True},
|
||||
"request_parallelism": {
|
||||
"display_name": "Request Parallelism",
|
||||
"value": 5,
|
||||
"advanced": True,
|
||||
},
|
||||
"stop": {"display_name": "Stop", "advanced": True},
|
||||
"streaming": {"display_name": "Streaming", "value": False, "advanced": True},
|
||||
"streaming": {
|
||||
"display_name": "Streaming",
|
||||
"value": False,
|
||||
"advanced": True,
|
||||
},
|
||||
"temperature": {"display_name": "Temperature", "value": 0.0},
|
||||
"top_k": {"display_name": "Top K", "value": 40, "advanced": True},
|
||||
"top_p": {"display_name": "Top P", "value": 0.95, "advanced": True},
|
||||
19
src/backend/base/langflow/components/embeddings/__init__.py
Normal file
19
src/backend/base/langflow/components/embeddings/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from .AmazonBedrockEmbeddings import AmazonBedrockEmeddingsComponent
|
||||
from .AzureOpenAIEmbeddings import AzureOpenAIEmbeddingsComponent
|
||||
from .CohereEmbeddings import CohereEmbeddingsComponent
|
||||
from .HuggingFaceEmbeddings import HuggingFaceEmbeddingsComponent
|
||||
from .HuggingFaceInferenceAPIEmbeddings import HuggingFaceInferenceAPIEmbeddingsComponent
|
||||
from .OllamaEmbeddings import OllamaEmbeddingsComponent
|
||||
from .OpenAIEmbeddings import OpenAIEmbeddingsComponent
|
||||
from .VertexAIEmbeddings import VertexAIEmbeddingsComponent
|
||||
|
||||
__all__ = [
|
||||
"AmazonBedrockEmeddingsComponent",
|
||||
"AzureOpenAIEmbeddingsComponent",
|
||||
"CohereEmbeddingsComponent",
|
||||
"HuggingFaceEmbeddingsComponent",
|
||||
"HuggingFaceInferenceAPIEmbeddingsComponent",
|
||||
"OllamaEmbeddingsComponent",
|
||||
"OpenAIEmbeddingsComponent",
|
||||
"VertexAIEmbeddingsComponent",
|
||||
]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.memory import delete_messages, get_messages
|
||||
|
||||
|
||||
class ClearMessageHistoryComponent(CustomComponent):
|
||||
display_name = "Clear Message History"
|
||||
description = "A component to clear the message history."
|
||||
icon = "ClearMessageHistory"
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"session_id": {
|
||||
"display_name": "Session ID",
|
||||
"info": "The session ID to clear the message history.",
|
||||
}
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> None:
|
||||
delete_messages(session_id=session_id)
|
||||
records = get_messages(session_id=session_id)
|
||||
self.records = records
|
||||
return records
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ExtractKeyFromRecordComponent(CustomComponent):
|
||||
display_name = "Extract Key From Record"
|
||||
description = "Extracts a key from a record."
|
||||
beta: bool = True
|
||||
|
||||
field_config = {
|
||||
"record": {"display_name": "Record"},
|
||||
"keys": {
|
||||
"display_name": "Keys",
|
||||
"info": "The keys to extract from the record.",
|
||||
"input_types": [],
|
||||
},
|
||||
"silent_error": {
|
||||
"display_name": "Silent Errors",
|
||||
"info": "If True, errors will not be raised.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, record: Record, keys: list[str], silent_error: bool = True) -> Record:
|
||||
"""
|
||||
Extracts the keys from a record.
|
||||
|
||||
Args:
|
||||
record (Record): The record from which to extract the keys.
|
||||
keys (list[str]): The keys to extract from the record.
|
||||
silent_error (bool): If True, errors will not be raised.
|
||||
|
||||
Returns:
|
||||
dict: The extracted keys.
|
||||
"""
|
||||
extracted_keys = {}
|
||||
for key in keys:
|
||||
try:
|
||||
extracted_keys[key] = getattr(record, key)
|
||||
except AttributeError:
|
||||
if not silent_error:
|
||||
raise KeyError(f"The key '{key}' does not exist in the record.")
|
||||
return_record = Record(data=extracted_keys)
|
||||
self.status = return_record
|
||||
return return_record
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
from typing import Any, List, Optional
|
||||
|
||||
from langchain_core.tools import StructuredTool
|
||||
from loguru import logger
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import Tool
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.helpers.flow import build_function_and_schema
|
||||
from langflow.schema.dotdict import dotdict
|
||||
from langflow.schema.schema import Record
|
||||
|
||||
|
||||
class FlowToolComponent(CustomComponent):
|
||||
display_name = "Flow as Tool"
|
||||
description = "Construct a Tool from a function that runs the loaded Flow."
|
||||
field_order = ["flow_name", "name", "description", "return_direct"]
|
||||
|
||||
def get_flow_names(self) -> List[str]:
|
||||
flow_records = self.list_flows()
|
||||
return [flow_record.data["name"] for flow_record in flow_records]
|
||||
|
||||
def get_flow(self, flow_name: str) -> Optional[Record]:
|
||||
"""
|
||||
Retrieves a flow by its name.
|
||||
|
||||
Args:
|
||||
flow_name (str): The name of the flow to retrieve.
|
||||
|
||||
Returns:
|
||||
Optional[Text]: The flow record if found, None otherwise.
|
||||
"""
|
||||
flow_records = self.list_flows()
|
||||
for flow_record in flow_records:
|
||||
if flow_record.data["name"] == flow_name:
|
||||
return flow_record
|
||||
return None
|
||||
|
||||
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
|
||||
logger.debug(f"Updating build config with field value {field_value} and field name {field_name}")
|
||||
if field_name == "flow_name":
|
||||
build_config["flow_name"]["options"] = self.get_flow_names()
|
||||
|
||||
return build_config
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"flow_name": {
|
||||
"display_name": "Flow Name",
|
||||
"info": "The name of the flow to run.",
|
||||
"options": [],
|
||||
"real_time_refresh": True,
|
||||
"refresh_button": True,
|
||||
},
|
||||
"name": {
|
||||
"display_name": "Name",
|
||||
"description": "The name of the tool.",
|
||||
},
|
||||
"description": {
|
||||
"display_name": "Description",
|
||||
"description": "The description of the tool.",
|
||||
},
|
||||
"return_direct": {
|
||||
"display_name": "Return Direct",
|
||||
"description": "Return the result directly from the Tool.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
async def build(self, flow_name: str, name: str, description: str, return_direct: bool = False) -> Tool:
|
||||
flow_record = self.get_flow(flow_name)
|
||||
if not flow_record:
|
||||
raise ValueError("Flow not found.")
|
||||
graph = Graph.from_payload(flow_record.data["data"])
|
||||
dynamic_flow_function, schema = build_function_and_schema(flow_record, graph)
|
||||
tool = StructuredTool.from_function(
|
||||
coroutine=dynamic_flow_function,
|
||||
name=name,
|
||||
description=description,
|
||||
return_direct=return_direct,
|
||||
args_schema=schema,
|
||||
)
|
||||
description_repr = repr(tool.description).strip("'")
|
||||
args_str = "\n".join([f"- {arg_name}: {arg_data['description']}" for arg_name, arg_data in tool.args.items()])
|
||||
self.status = f"{description_repr}\nArguments:\n{args_str}"
|
||||
return tool # type: ignore
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from typing import List
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ListFlowsComponent(CustomComponent):
|
||||
display_name = "List Flows"
|
||||
description = "A component to list all available flows."
|
||||
icon = "ListFlows"
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {}
|
||||
|
||||
def build(
|
||||
self,
|
||||
) -> List[Record]:
|
||||
flows = self.list_flows()
|
||||
self.status = flows
|
||||
return flows
|
||||
21
src/backend/base/langflow/components/experimental/Listen.py
Normal file
21
src/backend/base/langflow/components/experimental/Listen.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class ListenComponent(CustomComponent):
|
||||
display_name = "Listen"
|
||||
description = "A component to listen for a notification."
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"name": {
|
||||
"display_name": "Name",
|
||||
"info": "The name of the notification to listen for.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, name: str) -> Record:
|
||||
state = self.get_state(name)
|
||||
self.status = state
|
||||
return state
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class MergeRecordsComponent(CustomComponent):
|
||||
display_name = "Merge Records"
|
||||
description = "Merges records."
|
||||
beta: bool = True
|
||||
|
||||
field_config = {
|
||||
"records": {"display_name": "Records"},
|
||||
}
|
||||
|
||||
def build(self, records: list[Record]) -> Record:
|
||||
if not records:
|
||||
return Record()
|
||||
if len(records) == 1:
|
||||
return records[0]
|
||||
merged_record = Record()
|
||||
for record in records:
|
||||
if merged_record is None:
|
||||
merged_record = record
|
||||
else:
|
||||
merged_record += record
|
||||
self.status = merged_record
|
||||
return merged_record
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
records = [
|
||||
Record(data={"key1": "value1"}),
|
||||
Record(data={"key2": "value2"}),
|
||||
]
|
||||
component = MergeRecordsComponent()
|
||||
result = component.build(records)
|
||||
print(result)
|
||||
41
src/backend/base/langflow/components/experimental/Notify.py
Normal file
41
src/backend/base/langflow/components/experimental/Notify.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from typing import Optional
|
||||
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.schema import Record
|
||||
|
||||
|
||||
class NotifyComponent(CustomComponent):
|
||||
display_name = "Notify"
|
||||
description = "A component to generate a notification to Get Notified component."
|
||||
icon = "Notify"
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"name": {"display_name": "Name", "info": "The name of the notification."},
|
||||
"record": {"display_name": "Record", "info": "The record to store."},
|
||||
"append": {
|
||||
"display_name": "Append",
|
||||
"info": "If True, the record will be appended to the notification.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, name: str, record: Optional[Record] = None, append: bool = False) -> Record:
|
||||
if record and not isinstance(record, Record):
|
||||
if isinstance(record, str):
|
||||
record = Record(text=record)
|
||||
elif isinstance(record, dict):
|
||||
record = Record(data=record)
|
||||
else:
|
||||
record = Record(text=str(record))
|
||||
elif not record:
|
||||
record = Record(text="")
|
||||
if record:
|
||||
if append:
|
||||
self.append_state(name, record)
|
||||
else:
|
||||
self.update_state(name, record)
|
||||
else:
|
||||
self.status = "No record provided."
|
||||
self.status = record
|
||||
return record
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from typing import Callable
|
||||
|
||||
from langflow.field_typing import Code
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.interface.custom.utils import get_function
|
||||
|
||||
|
||||
class PythonFunctionComponent(CustomComponent):
|
||||
display_name = "Python Function"
|
||||
description = "Define a Python function."
|
||||
icon = "Python"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"function_code": {
|
||||
"display_name": "Code",
|
||||
"info": "The code for the function.",
|
||||
"show": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, function_code: Code) -> Callable:
|
||||
self.status = function_code
|
||||
func = get_function(function_code)
|
||||
return func
|
||||
66
src/backend/base/langflow/components/experimental/RunFlow.py
Normal file
66
src/backend/base/langflow/components/experimental/RunFlow.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from typing import Any, List, Optional
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing import NestedDict, Text
|
||||
from langflow.graph.schema import ResultData
|
||||
from langflow.schema import Record, dotdict
|
||||
|
||||
|
||||
class RunFlowComponent(CustomComponent):
|
||||
display_name = "Run Flow"
|
||||
description = "A component to run a flow."
|
||||
beta: bool = True
|
||||
|
||||
def get_flow_names(self) -> List[str]:
|
||||
flow_records = self.list_flows()
|
||||
return [flow_record.data["name"] for flow_record in flow_records]
|
||||
|
||||
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
|
||||
if field_name == "flow_name":
|
||||
build_config["flow_name"]["options"] = self.get_flow_names()
|
||||
|
||||
return build_config
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"multiline": True,
|
||||
},
|
||||
"flow_name": {
|
||||
"display_name": "Flow Name",
|
||||
"info": "The name of the flow to run.",
|
||||
"options": [],
|
||||
"refresh_button": True,
|
||||
},
|
||||
"tweaks": {
|
||||
"display_name": "Tweaks",
|
||||
"info": "Tweaks to apply to the flow.",
|
||||
},
|
||||
}
|
||||
|
||||
def build_records_from_result_data(self, result_data: ResultData) -> List[Record]:
|
||||
messages = result_data.messages
|
||||
if not messages:
|
||||
return []
|
||||
records = []
|
||||
for message in messages:
|
||||
message_dict = message if isinstance(message, dict) else message.model_dump()
|
||||
record = Record(text=message_dict.get("text", ""), data={"result": result_data})
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
async def build(self, input_value: Text, flow_name: str, tweaks: NestedDict) -> List[Record]:
|
||||
results: List[Optional[ResultData]] = await self.run_flow(
|
||||
inputs={"input_value": input_value}, flow_name=flow_name, tweaks=tweaks
|
||||
)
|
||||
if isinstance(results, list):
|
||||
records = []
|
||||
for result in results:
|
||||
if result:
|
||||
records.extend(self.build_records_from_result_data(result))
|
||||
else:
|
||||
records = self.build_records_from_result_data(results)
|
||||
|
||||
self.status = records
|
||||
return records
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
from langchain_core.runnables import Runnable
|
||||
|
||||
from langflow.field_typing import Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class RunnableExecComponent(CustomComponent):
|
||||
description = "Execute a runnable. It will try to guess the input and output keys."
|
||||
display_name = "Runnable Executor"
|
||||
beta: bool = True
|
||||
field_order = [
|
||||
"input_key",
|
||||
"output_key",
|
||||
"input_value",
|
||||
"runnable",
|
||||
]
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_key": {
|
||||
"display_name": "Input Key",
|
||||
"info": "The key to use for the input.",
|
||||
"advanced": True,
|
||||
},
|
||||
"input_value": {
|
||||
"display_name": "Inputs",
|
||||
"info": "The inputs to pass to the runnable.",
|
||||
},
|
||||
"runnable": {
|
||||
"display_name": "Runnable",
|
||||
"info": "The runnable to execute.",
|
||||
"input_types": ["Chain", "AgentExecutor", "Agent", "Runnable"],
|
||||
},
|
||||
"output_key": {
|
||||
"display_name": "Output Key",
|
||||
"info": "The key to use for the output.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def get_output(self, result, input_key, output_key):
|
||||
"""
|
||||
Retrieves the output value from the given result dictionary based on the specified input and output keys.
|
||||
|
||||
Args:
|
||||
result (dict): The result dictionary containing the output value.
|
||||
input_key (str): The key used to retrieve the input value from the result dictionary.
|
||||
output_key (str): The key used to retrieve the output value from the result dictionary.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the output value and the status message.
|
||||
|
||||
"""
|
||||
possible_output_keys = ["answer", "response", "output", "result", "text"]
|
||||
status = ""
|
||||
result_value = None
|
||||
|
||||
if output_key in result:
|
||||
result_value = result.get(output_key)
|
||||
elif len(result) == 2 and input_key in result:
|
||||
# get the other key from the result dict
|
||||
other_key = [k for k in result if k != input_key][0]
|
||||
if other_key == output_key:
|
||||
result_value = result.get(output_key)
|
||||
else:
|
||||
status += f"Warning: The output key is not '{output_key}'. The output key is '{other_key}'."
|
||||
result_value = result.get(other_key)
|
||||
elif len(result) == 1:
|
||||
result_value = list(result.values())[0]
|
||||
elif any(k in result for k in possible_output_keys):
|
||||
for key in possible_output_keys:
|
||||
if key in result:
|
||||
result_value = result.get(key)
|
||||
status += f"Output key: '{key}'."
|
||||
break
|
||||
if result_value is None:
|
||||
result_value = result
|
||||
status += f"Warning: The output key is not '{output_key}'."
|
||||
else:
|
||||
result_value = result
|
||||
status += f"Warning: The output key is not '{output_key}'."
|
||||
|
||||
return result_value, status
|
||||
|
||||
def get_input_dict(self, runnable, input_key, input_value):
|
||||
"""
|
||||
Returns a dictionary containing the input key-value pair for the given runnable.
|
||||
|
||||
Args:
|
||||
runnable: The runnable object.
|
||||
input_key: The key for the input value.
|
||||
input_value: The value for the input key.
|
||||
|
||||
Returns:
|
||||
input_dict: A dictionary containing the input key-value pair.
|
||||
status: A status message indicating if the input key is not in the runnable's input keys.
|
||||
"""
|
||||
input_dict = {}
|
||||
status = ""
|
||||
if hasattr(runnable, "input_keys"):
|
||||
# Check if input_key is in the runnable's input_keys
|
||||
if input_key in runnable.input_keys:
|
||||
input_dict[input_key] = input_value
|
||||
else:
|
||||
input_dict = {k: input_value for k in runnable.input_keys}
|
||||
status = f"Warning: The input key is not '{input_key}'. The input key is '{runnable.input_keys}'."
|
||||
return input_dict, status
|
||||
|
||||
def build(
|
||||
self,
|
||||
input_value: Text,
|
||||
runnable: Runnable,
|
||||
input_key: str = "input",
|
||||
output_key: str = "output",
|
||||
) -> Text:
|
||||
input_dict, status = self.get_input_dict(runnable, input_key, input_value)
|
||||
result = runnable.invoke(input_dict)
|
||||
result_value, _status = self.get_output(result, input_key, output_key)
|
||||
status += _status
|
||||
status += f"\n\nOutput: {result_value}\n\nRaw Output: {result}"
|
||||
self.status = status
|
||||
return result_value
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
|
||||
from langchain_experimental.sql.base import SQLDatabase
|
||||
|
||||
from langflow.field_typing import Text
|
||||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
|
||||
|
||||
class SQLExecutorComponent(CustomComponent):
|
||||
display_name = "SQL Executor"
|
||||
description = "Execute SQL query."
|
||||
beta: bool = True
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"database_url": {
|
||||
"display_name": "Database URL",
|
||||
"info": "The URL of the database.",
|
||||
},
|
||||
"include_columns": {
|
||||
"display_name": "Include Columns",
|
||||
"info": "Include columns in the result.",
|
||||
},
|
||||
"passthrough": {
|
||||
"display_name": "Passthrough",
|
||||
"info": "If an error occurs, return the query instead of raising an exception.",
|
||||
},
|
||||
"add_error": {
|
||||
"display_name": "Add Error",
|
||||
"info": "Add the error to the result.",
|
||||
},
|
||||
}
|
||||
|
||||
def clean_up_uri(self, uri: str) -> str:
|
||||
if uri.startswith("postgresql://"):
|
||||
uri = uri.replace("postgresql://", "postgres://")
|
||||
return uri.strip()
|
||||
|
||||
def build(
|
||||
self,
|
||||
query: str,
|
||||
database_url: str,
|
||||
include_columns: bool = False,
|
||||
passthrough: bool = False,
|
||||
add_error: bool = False,
|
||||
) -> Text:
|
||||
error = None
|
||||
try:
|
||||
database = SQLDatabase.from_uri(database_url)
|
||||
except Exception as e:
|
||||
raise ValueError(f"An error occurred while connecting to the database: {e}")
|
||||
try:
|
||||
tool = QuerySQLDataBaseTool(db=database)
|
||||
result = tool.run(query, include_columns=include_columns)
|
||||
self.status = result
|
||||
except Exception as e:
|
||||
result = Text(e)
|
||||
self.status = result
|
||||
if not passthrough:
|
||||
raise e
|
||||
error = repr(e)
|
||||
|
||||
if add_error and error is not None:
|
||||
result = f"{result}\n\nError: {error}\n\nQuery: {query}"
|
||||
elif error is not None:
|
||||
# Then we won't add the error to the result
|
||||
# but since we are in passthrough mode, we will return the query
|
||||
result = query
|
||||
|
||||
return result
|
||||
119
src/backend/base/langflow/components/experimental/SubFlow.py
Normal file
119
src/backend/base/langflow/components/experimental/SubFlow.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
from typing import Any, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.graph.graph.base import Graph
|
||||
from langflow.graph.schema import ResultData, RunOutputs
|
||||
from langflow.graph.vertex.base import Vertex
|
||||
from langflow.helpers.flow import get_flow_inputs
|
||||
from langflow.schema import Record
|
||||
from langflow.schema.dotdict import dotdict
|
||||
from langflow.template.field.base import TemplateField
|
||||
|
||||
|
||||
class SubFlowComponent(CustomComponent):
|
||||
display_name = "Sub Flow"
|
||||
description = "Dynamically Generates a Component from a Flow. The output is a list of records with keys 'result' and 'message'."
|
||||
beta: bool = True
|
||||
field_order = ["flow_name"]
|
||||
|
||||
def get_flow_names(self) -> List[str]:
|
||||
flow_records = self.list_flows()
|
||||
return [flow_record.data["name"] for flow_record in flow_records]
|
||||
|
||||
def get_flow(self, flow_name: str) -> Optional[Record]:
|
||||
flow_records = self.list_flows()
|
||||
for flow_record in flow_records:
|
||||
if flow_record.data["name"] == flow_name:
|
||||
return flow_record
|
||||
return None
|
||||
|
||||
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
|
||||
logger.debug(f"Updating build config with field value {field_value} and field name {field_name}")
|
||||
if field_name == "flow_name":
|
||||
build_config["flow_name"]["options"] = self.get_flow_names()
|
||||
# Clean up the build config
|
||||
for key in list(build_config.keys()):
|
||||
if key not in self.field_order + ["code", "_type"]:
|
||||
del build_config[key]
|
||||
if field_value is not None and field_name == "flow_name":
|
||||
try:
|
||||
flow_record = self.get_flow(field_value)
|
||||
if not flow_record:
|
||||
raise ValueError(f"Flow {field_value} not found.")
|
||||
graph = Graph.from_payload(flow_record.data["data"])
|
||||
# Get all inputs from the graph
|
||||
inputs = get_flow_inputs(graph)
|
||||
# Add inputs to the build config
|
||||
build_config = self.add_inputs_to_build_config(inputs, build_config)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting flow {field_value}: {str(e)}")
|
||||
|
||||
return build_config
|
||||
|
||||
def add_inputs_to_build_config(self, inputs: List[Vertex], build_config: dotdict):
|
||||
new_fields: list[TemplateField] = []
|
||||
for vertex in inputs:
|
||||
field = TemplateField(
|
||||
display_name=vertex.display_name,
|
||||
name=vertex.id,
|
||||
info=vertex.description,
|
||||
field_type="str",
|
||||
default=None,
|
||||
)
|
||||
new_fields.append(field)
|
||||
logger.debug(new_fields)
|
||||
for field in new_fields:
|
||||
build_config[field.name] = field.to_dict()
|
||||
return build_config
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"input_value": {
|
||||
"display_name": "Input Value",
|
||||
"multiline": True,
|
||||
},
|
||||
"flow_name": {
|
||||
"display_name": "Flow Name",
|
||||
"info": "The name of the flow to run.",
|
||||
"options": [],
|
||||
"real_time_refresh": True,
|
||||
"refresh_button": True,
|
||||
},
|
||||
"tweaks": {
|
||||
"display_name": "Tweaks",
|
||||
"info": "Tweaks to apply to the flow.",
|
||||
},
|
||||
}
|
||||
|
||||
def build_records_from_result_data(self, result_data: ResultData) -> List[Record]:
|
||||
messages = result_data.messages
|
||||
if not messages:
|
||||
return []
|
||||
records = []
|
||||
for message in messages:
|
||||
message_dict = message if isinstance(message, dict) else message.model_dump()
|
||||
record = Record(data={"result": result_data.model_dump(), "message": message_dict.get("message", "")})
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
async def build(self, flow_name: str, **kwargs) -> List[Record]:
|
||||
tweaks = {key: {"input_value": value} for key, value in kwargs.items()}
|
||||
run_outputs: List[Optional[RunOutputs]] = await self.run_flow(
|
||||
tweaks=tweaks,
|
||||
flow_name=flow_name,
|
||||
)
|
||||
if not run_outputs:
|
||||
return []
|
||||
run_output = run_outputs[0]
|
||||
|
||||
records = []
|
||||
if run_output is not None:
|
||||
for output in run_output.outputs:
|
||||
if output:
|
||||
records.extend(self.build_records_from_result_data(output))
|
||||
|
||||
self.status = records
|
||||
logger.debug(records)
|
||||
return records
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
from .ClearMessageHistory import ClearMessageHistoryComponent
|
||||
from .ExtractDataFromRecord import ExtractKeyFromRecordComponent
|
||||
from .FlowTool import FlowToolComponent
|
||||
from .ListFlows import ListFlowsComponent
|
||||
from .Listen import ListenComponent
|
||||
from .MergeRecords import MergeRecordsComponent
|
||||
from .Notify import NotifyComponent
|
||||
from .PythonFunction import PythonFunctionComponent
|
||||
from .RunFlow import RunFlowComponent
|
||||
from .RunnableExecutor import RunnableExecComponent
|
||||
from .SQLExecutor import SQLExecutorComponent
|
||||
from .SubFlow import SubFlowComponent
|
||||
|
||||
__all__ = [
|
||||
"ClearMessageHistoryComponent",
|
||||
"ExtractKeyFromRecordComponent",
|
||||
"FlowToolComponent",
|
||||
"ListFlowsComponent",
|
||||
"ListenComponent",
|
||||
"MergeRecordsComponent",
|
||||
"NotifyComponent",
|
||||
"PythonFunctionComponent",
|
||||
"RunFlowComponent",
|
||||
"RunnableExecComponent",
|
||||
"SQLExecutorComponent",
|
||||
"SubFlowComponent",
|
||||
"PythonFunctionComponent",
|
||||
]
|
||||
29
src/backend/base/langflow/components/helpers/CombineText.py
Normal file
29
src/backend/base/langflow/components/helpers/CombineText.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from langflow.interface.custom.custom_component import CustomComponent
|
||||
from langflow.field_typing import Text
|
||||
|
||||
|
||||
class CombineTextComponent(CustomComponent):
|
||||
display_name = "Combine Text"
|
||||
description = "Concatenate two text sources into a single text chunk using a specified delimiter."
|
||||
icon = "merge"
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"text1": {
|
||||
"display_name": "First Text",
|
||||
"info": "The first text input to concatenate.",
|
||||
},
|
||||
"text2": {
|
||||
"display_name": "Second Text",
|
||||
"info": "The second text input to concatenate.",
|
||||
},
|
||||
"delimiter": {
|
||||
"display_name": "Delimiter",
|
||||
"info": "A string used to separate the two text inputs. Defaults to a whitespace.",
|
||||
},
|
||||
}
|
||||
|
||||
def build(self, text1: str, text2: str, delimiter: str = " ") -> Text:
|
||||
combined = delimiter.join([text1, text2])
|
||||
self.status = combined
|
||||
return combined
|
||||
81
src/backend/base/langflow/components/helpers/CreateRecord.py
Normal file
81
src/backend/base/langflow/components/helpers/CreateRecord.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from typing import Any
|
||||
|
||||
from langflow.custom import CustomComponent
|
||||
from langflow.field_typing.range_spec import RangeSpec
|
||||
from langflow.schema import Record
|
||||
from langflow.schema.dotdict import dotdict
|
||||
from langflow.template.field.base import TemplateField
|
||||
|
||||
|
||||
class CreateRecordComponent(CustomComponent):
|
||||
display_name = "Create Record"
|
||||
description = "Dynamically create a Record with a specified number of fields."
|
||||
field_order = ["number_of_fields", "text_key"]
|
||||
|
||||
def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):
|
||||
if field_name == "number_of_fields":
|
||||
default_keys = ["code", "_type", "number_of_fields", "text_key"]
|
||||
try:
|
||||
field_value_int = int(field_value)
|
||||
except TypeError:
|
||||
return build_config
|
||||
existing_fields = {}
|
||||
if field_value_int > 15:
|
||||
build_config["number_of_fields"]["value"] = 15
|
||||
raise ValueError("Number of fields cannot exceed 15. Try using a Component to combine two Records.")
|
||||
if len(build_config) > len(default_keys) + field_value_int:
|
||||
# back up the existing template fields
|
||||
for key in build_config.copy():
|
||||
if key not in default_keys:
|
||||
existing_fields[key] = build_config.pop(key)
|
||||
|
||||
for i in range(1, field_value_int + 1):
|
||||
key = f"field_{i}_key"
|
||||
if key in existing_fields:
|
||||
field = existing_fields[key]
|
||||
build_config[key] = field
|
||||
else:
|
||||
field = TemplateField(
|
||||
display_name=f"Field {i}",
|
||||
name=key,
|
||||
info=f"Key for field {i}.",
|
||||
field_type="dict",
|
||||
input_types=["Text", "Record"],
|
||||
)
|
||||
build_config[field.name] = field.to_dict()
|
||||
|
||||
build_config["number_of_fields"]["value"] = field_value_int
|
||||
return build_config
|
||||
|
||||
def build_config(self):
|
||||
return {
|
||||
"number_of_fields": {
|
||||
"display_name": "Number of Fields",
|
||||
"info": "Number of fields to be added to the record.",
|
||||
"real_time_refresh": True,
|
||||
"rangeSpec": RangeSpec(min=1, max=15, step=1, step_type="int"),
|
||||
},
|
||||
"text_key": {
|
||||
"display_name": "Text Key",
|
||||
"info": "Key to be used as text.",
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
|
||||
def build(
|
||||
self,
|
||||
number_of_fields: int = 0,
|
||||
text_key: str = "text",
|
||||
**kwargs,
|
||||
) -> Record:
|
||||
data = {}
|
||||
for value_dict in kwargs.values():
|
||||
if isinstance(value_dict, dict):
|
||||
# Check if the value of the value_dict is a Record
|
||||
value_dict = {
|
||||
key: value.get_text() if isinstance(value, Record) else value for key, value in value_dict.items()
|
||||
}
|
||||
data.update(value_dict)
|
||||
return_record = Record(data=data, text_key=text_key)
|
||||
self.status = return_record
|
||||
return return_record
|
||||
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