Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Criar Componente da Tess no Langflow #5840

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b3b474b
create execute_agent component
Jan 20, 2025
331233b
Revert "create execute_agent component"
Jan 20, 2025
6dd3d11
create execute_agent component
Jan 20, 2025
069abef
add tess icon in bundles menu
Jan 20, 2025
7cd4248
change icon
Jan 20, 2025
bd15bf8
create upload_file component
Jan 20, 2025
3840b41
create upload_file component
Jan 20, 2025
4e52ce6
create upload_file component
Jan 20, 2025
67280b8
create associate_file_to_agent component
Jan 20, 2025
0c74587
minor fixes
Jan 21, 2025
2beb0bb
Merge remote-tracking branch 'origin/main' into SIDE-60
Feb 21, 2025
8789880
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 21, 2025
7a17861
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Feb 21, 2025
fe2b67e
add dynamic fields
Feb 21, 2025
0599078
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 22, 2025
bbbfd66
improve dynamic fields
Feb 24, 2025
bb47193
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 24, 2025
dd78c60
fix
Feb 24, 2025
30cf990
refactor
Feb 24, 2025
00152a7
refactor
Feb 24, 2025
7b74cbb
refactor
Feb 24, 2025
a228245
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 24, 2025
6a7fb24
refactor
Feb 24, 2025
ea8f33e
refactor
Feb 24, 2025
9be803e
refactor
Feb 24, 2025
bd274ff
refactor
Feb 24, 2025
a700745
refactor
Feb 24, 2025
3a6bef9
refactor
Feb 25, 2025
81f81aa
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 25, 2025
2265609
add IntInput
Feb 25, 2025
b3ca429
fix chat message input
Feb 27, 2025
512fe0e
refactor
Feb 27, 2025
eca1c37
refactor
Feb 27, 2025
c0a9983
refactor
Feb 27, 2025
6cfdd25
some adjustments
Feb 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/backend/base/langflow/components/tessai/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .associate_file_to_agent import TessAIAssociateFileToAgentComponent
from .execute_agent import TessAIExecuteAgentComponent
from .upload_file import TessAIUploadFileComponent

__all__ = ["TessAIAssociateFileToAgentComponent", "TessAIExecuteAgentComponent", "TessAIUploadFileComponent"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import requests

from langflow.custom import Component
from langflow.inputs import DataInput, SecretStrInput, StrInput
from langflow.io import Output
from langflow.schema import Data


class TessAIAssociateFileToAgentComponent(Component):
display_name = "Associate File to Agent"
description = "Associates a file with an agent in the TessAI platform."
documentation = "https://docs.tess.pareto.io/"
icon = "TessAI"

inputs = [
SecretStrInput(
name="api_key",
display_name="Tess AI API Key",
info="The API key to use for TessAI.",
advanced=False,
input_types=[]
),
StrInput(
name="agent_id",
display_name="User-Owned Agent ID",
info="The ID of an agent you created in the Tess AI platform.",
required=True,
),
DataInput(
name="files",
display_name="File(s)",
info="The file(s) to associate with the agent.",
required=True,
is_list=True
),
]

outputs = [Output(display_name="Association Result", name="association_result", method="associate_file_to_agent")]

BASE_URL = "https://tess.pareto.io/api"

def associate_file_to_agent(self) -> Data:
headers = self._get_headers()
endpoint = f"{self.BASE_URL}/agents/{self.agent_id}/files?waitExecution=True"

try:
payload = {"file_ids": [int(file.data["id"]) for file in self.files]}

response = requests.post(endpoint, headers=headers, json=payload)

Check failure on line 49 in src/backend/base/langflow/components/tessai/associate_file_to_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (S113)

src/backend/base/langflow/components/tessai/associate_file_to_agent.py:49:24: S113 Probable use of `requests` call without timeout
response.raise_for_status()
result = response.json()

return Data(data=result)
except requests.RequestException as e:
raise RuntimeError(f"Error associating file to agent: {e!s}") from e

Check failure on line 55 in src/backend/base/langflow/components/tessai/associate_file_to_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (TRY003)

src/backend/base/langflow/components/tessai/associate_file_to_agent.py:55:19: TRY003 Avoid specifying long messages outside the exception class

Check failure on line 55 in src/backend/base/langflow/components/tessai/associate_file_to_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (EM102)

src/backend/base/langflow/components/tessai/associate_file_to_agent.py:55:32: EM102 Exception must not use an f-string literal, assign to variable first

def _get_headers(self) -> dict:
return {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}

Check failure on line 58 in src/backend/base/langflow/components/tessai/associate_file_to_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (W292)

src/backend/base/langflow/components/tessai/associate_file_to_agent.py:58:95: W292 No newline at end of file
189 changes: 189 additions & 0 deletions src/backend/base/langflow/components/tessai/execute_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import json
from copy import deepcopy

import requests

from langflow.custom import Component
from langflow.inputs import BoolInput, DropdownInput, IntInput, MultilineInput, MultiselectInput, SecretStrInput, StrInput

Check failure on line 7 in src/backend/base/langflow/components/tessai/execute_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (E501)

src/backend/base/langflow/components/tessai/execute_agent.py:7:121: E501 Line too long (122 > 120)
from langflow.io import Output
from langflow.schema.message import Message

Check failure on line 9 in src/backend/base/langflow/components/tessai/execute_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (I001)

src/backend/base/langflow/components/tessai/execute_agent.py:1:1: I001 Import block is un-sorted or un-formatted

class TessAIExecuteAgentComponent(Component):
display_name = "Execute Agent"
description = "Executes a TessAI agent."
documentation = "https://docs.tess.pareto.io/"
icon = "TessAI"

inputs = [
SecretStrInput(
name="api_key",
display_name="Tess AI API Key",
info="The API key to use for TessAI.",
advanced=False,
input_types=[]
),
StrInput(
name="agent_id",
display_name="Agent ID",
required=True,
info="The ID of the agent to execute.",
real_time_refresh=True,
),
]

outputs = [Output(display_name="Output", name="output", method="execute_agent")]

BASE_URL = "https://tess.pareto.io/api"
FIELD_SUFFIX = "_tess_ai_dynamic_field"
CHAT_MESSAGE_INPUT_SUFFIX = "_tess_ai_chat_message_input"

def execute_agent(self) -> Message:
headers = self._get_headers()
execute_endpoint = f"{self.BASE_URL}/agents/{self.agent_id.strip()}/execute?waitExecution=true"
attributes = self._collect_dynamic_attributes()

try:
response = requests.post(execute_endpoint, headers=headers, json=attributes)

Check failure on line 46 in src/backend/base/langflow/components/tessai/execute_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (S113)

src/backend/base/langflow/components/tessai/execute_agent.py:46:24: S113 Probable use of `requests` call without timeout
response.raise_for_status()
execution_data = response.json()

if execution_data["responses"][0]["status"] not in ["succeeded", "failed", "error"]:
raise ValueError(json.dumps(execution_data))

response_id = execution_data["responses"][0]["id"]
response = self._get_agent_response(headers, response_id)
return Message(text=response.get("output", ""))
except requests.RequestException as e:
error_json = e.response.json() if e.response is not None else {"error": str(e)}
raise RuntimeError(json.dumps(error_json)) from e

def update_build_config(self, build_config: dict, field_value: str, field_name: str|None = None) -> dict:
if field_name == "agent_id" and field_value and build_config.get("api_key", {}).get("value"):
try:
agent = self._get_agent(field_value)
old_build_config = deepcopy(dict(build_config))

Check failure on line 65 in src/backend/base/langflow/components/tessai/execute_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (W293)

src/backend/base/langflow/components/tessai/execute_agent.py:65:1: W293 Blank line contains whitespace
for key in list(build_config.keys()):
if key.endswith(self.FIELD_SUFFIX):
del build_config[key]

Check failure on line 69 in src/backend/base/langflow/components/tessai/execute_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (W293)

src/backend/base/langflow/components/tessai/execute_agent.py:69:1: W293 Blank line contains whitespace
questions = agent.get("questions", [])
for question in questions:
name = question.get("name", "")

Check failure on line 73 in src/backend/base/langflow/components/tessai/execute_agent.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.13)

Ruff (W293)

src/backend/base/langflow/components/tessai/execute_agent.py:73:1: W293 Blank line contains whitespace
if name == "messages" and agent.get("type") == "chat":
name += self.CHAT_MESSAGE_INPUT_SUFFIX

key = name + self.FIELD_SUFFIX
old_config = old_build_config.get(key, {})

field = self._create_field(key, question, old_config.get("value"))
config = field.model_dump(by_alias=True, exclude_none=True)

self.inputs.append(field)
build_config[key] = config

except requests.RequestException:
for key in list(build_config.keys()):
if key.endswith(self.FIELD_SUFFIX):
del build_config[key]

self.map_inputs(self.inputs)
self.build_inputs()

return build_config

def _get_headers(self) -> dict:
return {"Authorization": f"Bearer {self.api_key}", "accept": "*/*", "Content-Type": "application/json"}

def _get_agent(self, agent_id):
endpoint = f"{self.BASE_URL}/agents/{agent_id}"
response = requests.get(endpoint, headers=self._get_headers())

if response.status_code not in [200, 404]:
raise Exception(json.dumps(response.json()))

return response.json()

def _get_agent_response(self, headers: dict, response_id: str) -> str:
endpoint = f"{self.BASE_URL}/agent-responses/{response_id}"
try:
response = requests.get(endpoint, headers=headers)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
error_json = e.response.json() if e.response is not None else {"error": str(e)}
raise RuntimeError(json.dumps(error_json)) from e

def _create_field(self, key: str, question: dict, value: str|None = None) -> dict:
field_type = question.get("type", "text")

args = {
"name": key,
"display_name": question["name"],
"required": question.get("required", False),
"info": question.get("description", ""),
"placeholder": question.get("placeholder", ""),
}

if value:
args["value"] = value
elif question.get("default"):
args["value"] = question.get("default")

if field_type == "textarea":
input_class = MultilineInput
elif field_type == "select":
options = question.get("options", [])
if all(isinstance(option, bool) for option in options):
input_class = BoolInput
if value:
args["value"] = value
elif args["required"]:
args["value"] = args.get("default", options[0])
else:
input_class = DropdownInput
args["options"] = [str(option) for option in options]
if value and value in args["options"]:
args["value"] = value
elif args["required"]:
args["value"] = args.get("default", args["options"][0])
elif field_type == "number":
input_class = IntInput
args["input_types"] = ["Message"]
elif field_type == "multiselect":
input_class = MultiselectInput
args["options"] = question.get("description", "").split(",")
if value and isinstance(value, list):
args["value"] = [val for val in value if val in args["options"]]
else:
args["value"] = []
else:
input_class = StrInput
if field_type == "file":
args["display_name"] += " (direct URL)"
args["input_types"] = ["Message"]

return input_class(**args)

def _collect_dynamic_attributes(self) -> dict:
attributes = {}
suffix = self.FIELD_SUFFIX
suffix_length = len(suffix)

for key in self._attributes:
if key.endswith(suffix):
value = self._attributes[key]
name = key[:-suffix_length]

if isinstance(value, Message):
value = value.text

if name.endswith(self.CHAT_MESSAGE_INPUT_SUFFIX):
name = name[:-len(self.CHAT_MESSAGE_INPUT_SUFFIX)]
attributes[name] = [{"role": "user", "content": value}]
elif isinstance(value, list):
attributes[name] = ",".join(str(val) for val in value)
elif value not in ["", None]:
attributes[name] = value
return attributes
111 changes: 111 additions & 0 deletions src/backend/base/langflow/components/tessai/upload_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import time
from pathlib import Path

import requests

from langflow.custom import Component
from langflow.inputs import BoolInput, FileInput, SecretStrInput
from langflow.io import Output
from langflow.schema import Data


class TessAIUploadFileComponent(Component):
display_name = "Upload File"
description = "Uploads a file to TessAI platform."
documentation = "https://docs.tess.pareto.io/"
icon = "TessAI"

inputs = [
SecretStrInput(
name="api_key",
display_name="Tess AI API Key",
info="The API key to use for TessAI.",
advanced=False,
input_types=[]
),
FileInput(
name="file",
display_name="File",
info="The file to upload.",
required=True,
file_types=[
"pdf",
"docx",
"txt",
"csv",
"xlsx",
"xls",
"ppt",
"pptx",
"png",
"jpg",
"jpeg",
"gif",
"bmp",
"tiff",
"ico",
"webp",
"mp3",
"mp4",
"wav",
"webm",
"m4a",
"m4v",
"mov",
"avi",
"mkv",
"webm"
],
),
BoolInput(
name="process",
display_name="Process File",
info="Whether to process the file after upload.",
),
]

outputs = [Output(display_name="File Data", name="file_data", method="upload_file")]

BASE_URL = "https://tess.pareto.io/api"

def upload_file(self) -> Data:
headers = self._get_headers()
upload_endpoint = f"{self.BASE_URL}/files"

try:
files = {"file": open(Path(self.file), "rb")}
data = {"process": str(self.process).lower()}

response = requests.post(upload_endpoint, headers=headers, files=files, data=data)
response.raise_for_status()
file_data = response.json()

if file_data["status"] == "waiting":
return self._poll_file_status(headers, file_data["id"])

return Data(data=file_data)
except requests.RequestException as e:
raise RuntimeError(f"Error uploading file: {e!s}") from e

def _get_headers(self) -> dict:
return {"Authorization": f"Bearer {self.api_key}"}

def _poll_file_status(self, headers: dict, file_id: int) -> dict:
endpoint = f"{self.BASE_URL}/api/files/{file_id}"
start_time = time.time()
timeout = 300

while time.time() - start_time < timeout:
try:
response = requests.get(endpoint, headers=headers)
response.raise_for_status()
file_data = response.json()

if file_data["status"] != "waiting":
return file_data

time.sleep(2)
except requests.RequestException as e:
raise RuntimeError(f"Error polling file status: {e!s}") from e

raise TimeoutError("File processing timed out after 5 minutes")
1 change: 0 additions & 1 deletion src/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/frontend/src/icons/TessAI/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgTessAIIcon from "./tessAIIcon";

export const TessAIIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgTessAIIcon ref={ref} {...props} />;
});
Loading
Loading