Compare commits
60 Commits
dev
...
make-old-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
326554d89a | ||
|
|
5e22a1888a | ||
|
|
a4d7b0142f | ||
|
|
7d6375f59c | ||
|
|
aeec0ce509 | ||
|
|
b32bfcaac5 | ||
|
|
5373a6eb6e | ||
|
|
98cde46ccb | ||
|
|
bd10da10d9 | ||
|
|
60fdee1345 | ||
|
|
6f2783468c | ||
|
|
c1031b286d | ||
|
|
b849eafb7f | ||
|
|
572c3f5e0d | ||
|
|
89003a585d | ||
|
|
0e65785228 | ||
|
|
f07dff1cdd | ||
|
|
00e02a4696 | ||
|
|
634bff8277 | ||
|
|
d591f36c7b | ||
|
|
a347bed0b1 | ||
|
|
4eeb6ee2b0 | ||
|
|
7db962b9f9 | ||
|
|
9108b21541 | ||
|
|
ffe9325296 | ||
|
|
0a616d9267 | ||
|
|
ab95077e5b | ||
|
|
e477150979 | ||
|
|
804430e243 | ||
|
|
acb320d32d | ||
|
|
32f68d5999 | ||
|
|
49f56b4e8d | ||
|
|
bead811e73 | ||
|
|
013f728ebf | ||
|
|
cda9572acd | ||
|
|
e0784f8f6b | ||
|
|
3040f39136 | ||
|
|
515504c604 | ||
|
|
18edeaeaf4 | ||
|
|
44182aff9c | ||
|
|
864c5a7846 | ||
|
|
699fffb1a8 | ||
|
|
f0641c2d26 | ||
|
|
94b6f74c95 | ||
|
|
46aabab3ea | ||
|
|
0a65df5102 | ||
|
|
6fbd208fe3 | ||
|
|
8fc174ca87 | ||
|
|
cacc89790f | ||
|
|
b9113bee02 | ||
|
|
3f65da03e7 | ||
|
|
9e96d11b2d | ||
|
|
4c264b7ae9 | ||
|
|
0adbc0bd05 | ||
|
|
8f3291bc92 | ||
|
|
7a20de880d | ||
|
|
ef8a6d2528 | ||
|
|
fd66be2aaa | ||
|
|
ae2cc97dc4 | ||
|
|
ea521eed26 |
73
.github/workflows/classic-autogpt-ci.yml
vendored
@@ -6,11 +6,15 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- '.github/workflows/classic-autogpt-ci.yml'
|
- '.github/workflows/classic-autogpt-ci.yml'
|
||||||
- 'classic/original_autogpt/**'
|
- 'classic/original_autogpt/**'
|
||||||
|
- 'classic/direct_benchmark/**'
|
||||||
|
- 'classic/forge/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master, dev, release-* ]
|
branches: [ master, dev, release-* ]
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/classic-autogpt-ci.yml'
|
- '.github/workflows/classic-autogpt-ci.yml'
|
||||||
- 'classic/original_autogpt/**'
|
- 'classic/original_autogpt/**'
|
||||||
|
- 'classic/direct_benchmark/**'
|
||||||
|
- 'classic/forge/**'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('classic-autogpt-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
|
group: ${{ format('classic-autogpt-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
|
||||||
@@ -19,47 +23,22 @@ concurrency:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: classic/original_autogpt
|
working-directory: classic
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10"]
|
|
||||||
platform-os: [ubuntu, macos, macos-arm64, windows]
|
|
||||||
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Quite slow on macOS (2~4 minutes to set up Docker)
|
- name: Start MinIO service
|
||||||
# - name: Set up Docker (macOS)
|
|
||||||
# if: runner.os == 'macOS'
|
|
||||||
# uses: crazy-max/ghaction-setup-docker@v3
|
|
||||||
|
|
||||||
- name: Start MinIO service (Linux)
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
working-directory: '.'
|
working-directory: '.'
|
||||||
run: |
|
run: |
|
||||||
docker pull minio/minio:edge-cicd
|
docker pull minio/minio:edge-cicd
|
||||||
docker run -d -p 9000:9000 minio/minio:edge-cicd
|
docker run -d -p 9000:9000 minio/minio:edge-cicd
|
||||||
|
|
||||||
- name: Start MinIO service (macOS)
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
working-directory: ${{ runner.temp }}
|
|
||||||
run: |
|
|
||||||
brew install minio/stable/minio
|
|
||||||
mkdir data
|
|
||||||
minio server ./data &
|
|
||||||
|
|
||||||
# No MinIO on Windows:
|
|
||||||
# - Windows doesn't support running Linux Docker containers
|
|
||||||
# - It doesn't seem possible to start background processes on Windows. They are
|
|
||||||
# killed after the step returns.
|
|
||||||
# See: https://github.com/actions/runner/issues/598#issuecomment-2011890429
|
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -71,41 +50,23 @@ jobs:
|
|||||||
git config --global user.name "Auto-GPT-Bot"
|
git config --global user.name "Auto-GPT-Bot"
|
||||||
git config --global user.email "github-bot@agpt.co"
|
git config --global user.email "github-bot@agpt.co"
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python 3.12
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: "3.12"
|
||||||
|
|
||||||
- id: get_date
|
- id: get_date
|
||||||
name: Get date
|
name: Get date
|
||||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Set up Python dependency cache
|
- name: Set up Python dependency cache
|
||||||
# On Windows, unpacking cached dependencies takes longer than just installing them
|
|
||||||
if: runner.os != 'Windows'
|
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.os == 'macOS' && '~/Library/Caches/pypoetry' || '~/.cache/pypoetry' }}
|
path: ~/.cache/pypoetry
|
||||||
key: poetry-${{ runner.os }}-${{ hashFiles('classic/original_autogpt/poetry.lock') }}
|
key: poetry-${{ runner.os }}-${{ hashFiles('classic/poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Poetry (Unix)
|
- name: Install Poetry
|
||||||
if: runner.os != 'Windows'
|
run: curl -sSL https://install.python-poetry.org | python3 -
|
||||||
run: |
|
|
||||||
curl -sSL https://install.python-poetry.org | python3 -
|
|
||||||
|
|
||||||
if [ "${{ runner.os }}" = "macOS" ]; then
|
|
||||||
PATH="$HOME/.local/bin:$PATH"
|
|
||||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Poetry (Windows)
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
|
|
||||||
|
|
||||||
$env:PATH += ";$env:APPDATA\Python\Scripts"
|
|
||||||
echo "$env:APPDATA\Python\Scripts" >> $env:GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: poetry install
|
run: poetry install
|
||||||
@@ -116,12 +77,12 @@ jobs:
|
|||||||
--cov=autogpt --cov-branch --cov-report term-missing --cov-report xml \
|
--cov=autogpt --cov-branch --cov-report term-missing --cov-report xml \
|
||||||
--numprocesses=logical --durations=10 \
|
--numprocesses=logical --durations=10 \
|
||||||
--junitxml=junit.xml -o junit_family=legacy \
|
--junitxml=junit.xml -o junit_family=legacy \
|
||||||
tests/unit tests/integration
|
original_autogpt/tests/unit original_autogpt/tests/integration
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
PLAIN_OUTPUT: True
|
PLAIN_OUTPUT: True
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
S3_ENDPOINT_URL: ${{ runner.os != 'Windows' && 'http://127.0.0.1:9000' || '' }}
|
S3_ENDPOINT_URL: http://127.0.0.1:9000
|
||||||
AWS_ACCESS_KEY_ID: minioadmin
|
AWS_ACCESS_KEY_ID: minioadmin
|
||||||
AWS_SECRET_ACCESS_KEY: minioadmin
|
AWS_SECRET_ACCESS_KEY: minioadmin
|
||||||
|
|
||||||
@@ -135,11 +96,11 @@ jobs:
|
|||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
flags: autogpt-agent,${{ runner.os }}
|
flags: autogpt-agent
|
||||||
|
|
||||||
- name: Upload logs to artifact
|
- name: Upload logs to artifact
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-logs
|
name: test-logs
|
||||||
path: classic/original_autogpt/logs/
|
path: classic/logs/
|
||||||
|
|||||||
36
.github/workflows/classic-autogpts-ci.yml
vendored
@@ -11,9 +11,6 @@ on:
|
|||||||
- 'classic/original_autogpt/**'
|
- 'classic/original_autogpt/**'
|
||||||
- 'classic/forge/**'
|
- 'classic/forge/**'
|
||||||
- 'classic/benchmark/**'
|
- 'classic/benchmark/**'
|
||||||
- 'classic/run'
|
|
||||||
- 'classic/cli.py'
|
|
||||||
- 'classic/setup.py'
|
|
||||||
- '!**/*.md'
|
- '!**/*.md'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master, dev, release-* ]
|
branches: [ master, dev, release-* ]
|
||||||
@@ -22,9 +19,6 @@ on:
|
|||||||
- 'classic/original_autogpt/**'
|
- 'classic/original_autogpt/**'
|
||||||
- 'classic/forge/**'
|
- 'classic/forge/**'
|
||||||
- 'classic/benchmark/**'
|
- 'classic/benchmark/**'
|
||||||
- 'classic/run'
|
|
||||||
- 'classic/cli.py'
|
|
||||||
- 'classic/setup.py'
|
|
||||||
- '!**/*.md'
|
- '!**/*.md'
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
@@ -35,13 +29,9 @@ defaults:
|
|||||||
jobs:
|
jobs:
|
||||||
serve-agent-protocol:
|
serve-agent-protocol:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
agent-name: [ original_autogpt ]
|
|
||||||
fail-fast: false
|
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
min-python-version: '3.10'
|
min-python-version: '3.12'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -55,22 +45,22 @@ jobs:
|
|||||||
python-version: ${{ env.min-python-version }}
|
python-version: ${{ env.min-python-version }}
|
||||||
|
|
||||||
- name: Install Poetry
|
- name: Install Poetry
|
||||||
working-directory: ./classic/${{ matrix.agent-name }}/
|
|
||||||
run: |
|
run: |
|
||||||
curl -sSL https://install.python-poetry.org | python -
|
curl -sSL https://install.python-poetry.org | python -
|
||||||
|
|
||||||
- name: Run regression tests
|
- name: Install dependencies
|
||||||
|
run: poetry install
|
||||||
|
|
||||||
|
- name: Run smoke tests with direct-benchmark
|
||||||
run: |
|
run: |
|
||||||
./run agent start ${{ matrix.agent-name }}
|
poetry run direct-benchmark run \
|
||||||
cd ${{ matrix.agent-name }}
|
--strategies one_shot \
|
||||||
poetry run agbenchmark --mock --test=BasicRetrieval --test=Battleship --test=WebArenaTask_0
|
--models claude \
|
||||||
poetry run agbenchmark --test=WriteFile
|
--tests ReadFile,WriteFile \
|
||||||
|
--json
|
||||||
env:
|
env:
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
AGENT_NAME: ${{ matrix.agent-name }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
|
REQUESTS_CA_BUNDLE: /etc/ssl/certs/ca-certificates.crt
|
||||||
HELICONE_CACHE_ENABLED: false
|
NONINTERACTIVE_MODE: "true"
|
||||||
HELICONE_PROPERTY_AGENT: ${{ matrix.agent-name }}
|
CI: true
|
||||||
REPORTS_FOLDER: ${{ format('../../reports/{0}', matrix.agent-name) }}
|
|
||||||
TELEMETRY_ENVIRONMENT: autogpt-ci
|
|
||||||
TELEMETRY_OPT_IN: ${{ github.ref_name == 'master' }}
|
|
||||||
|
|||||||
189
.github/workflows/classic-benchmark-ci.yml
vendored
@@ -1,17 +1,21 @@
|
|||||||
name: Classic - AGBenchmark CI
|
name: Classic - Direct Benchmark CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master, dev, ci-test* ]
|
branches: [ master, dev, ci-test* ]
|
||||||
paths:
|
paths:
|
||||||
- 'classic/benchmark/**'
|
- 'classic/direct_benchmark/**'
|
||||||
- '!classic/benchmark/reports/**'
|
- 'classic/benchmark/agbenchmark/challenges/**'
|
||||||
|
- 'classic/original_autogpt/**'
|
||||||
|
- 'classic/forge/**'
|
||||||
- .github/workflows/classic-benchmark-ci.yml
|
- .github/workflows/classic-benchmark-ci.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master, dev, release-* ]
|
branches: [ master, dev, release-* ]
|
||||||
paths:
|
paths:
|
||||||
- 'classic/benchmark/**'
|
- 'classic/direct_benchmark/**'
|
||||||
- '!classic/benchmark/reports/**'
|
- 'classic/benchmark/agbenchmark/challenges/**'
|
||||||
|
- 'classic/original_autogpt/**'
|
||||||
|
- 'classic/forge/**'
|
||||||
- .github/workflows/classic-benchmark-ci.yml
|
- .github/workflows/classic-benchmark-ci.yml
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -23,23 +27,16 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
env:
|
env:
|
||||||
min-python-version: '3.10'
|
min-python-version: '3.12'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
benchmark-tests:
|
||||||
permissions:
|
runs-on: ubuntu-latest
|
||||||
contents: read
|
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10"]
|
|
||||||
platform-os: [ubuntu, macos, macos-arm64, windows]
|
|
||||||
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: classic/benchmark
|
working-directory: classic
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -47,71 +44,84 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ env.min-python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ env.min-python-version }}
|
||||||
|
|
||||||
- name: Set up Python dependency cache
|
- name: Set up Python dependency cache
|
||||||
# On Windows, unpacking cached dependencies takes longer than just installing them
|
|
||||||
if: runner.os != 'Windows'
|
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.os == 'macOS' && '~/Library/Caches/pypoetry' || '~/.cache/pypoetry' }}
|
path: ~/.cache/pypoetry
|
||||||
key: poetry-${{ runner.os }}-${{ hashFiles('classic/benchmark/poetry.lock') }}
|
key: poetry-${{ runner.os }}-${{ hashFiles('classic/poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Poetry (Unix)
|
- name: Install Poetry
|
||||||
if: runner.os != 'Windows'
|
|
||||||
run: |
|
run: |
|
||||||
curl -sSL https://install.python-poetry.org | python3 -
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
if [ "${{ runner.os }}" = "macOS" ]; then
|
- name: Install dependencies
|
||||||
PATH="$HOME/.local/bin:$PATH"
|
|
||||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Poetry (Windows)
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
|
|
||||||
|
|
||||||
$env:PATH += ";$env:APPDATA\Python\Scripts"
|
|
||||||
echo "$env:APPDATA\Python\Scripts" >> $env:GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
|
||||||
run: poetry install
|
run: poetry install
|
||||||
|
|
||||||
- name: Run pytest with coverage
|
- name: Run basic benchmark tests
|
||||||
run: |
|
run: |
|
||||||
poetry run pytest -vv \
|
echo "Testing ReadFile challenge with one_shot strategy..."
|
||||||
--cov=agbenchmark --cov-branch --cov-report term-missing --cov-report xml \
|
poetry run direct-benchmark run \
|
||||||
--durations=10 \
|
--strategies one_shot \
|
||||||
--junitxml=junit.xml -o junit_family=legacy \
|
--models claude \
|
||||||
tests
|
--tests ReadFile \
|
||||||
|
--json
|
||||||
|
|
||||||
|
echo "Testing WriteFile challenge..."
|
||||||
|
poetry run direct-benchmark run \
|
||||||
|
--strategies one_shot \
|
||||||
|
--models claude \
|
||||||
|
--tests WriteFile \
|
||||||
|
--json
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
NONINTERACTIVE_MODE: "true"
|
||||||
|
|
||||||
- name: Upload test results to Codecov
|
- name: Test category filtering
|
||||||
if: ${{ !cancelled() }} # Run even if tests fail
|
run: |
|
||||||
uses: codecov/test-results-action@v1
|
echo "Testing coding category..."
|
||||||
with:
|
poetry run direct-benchmark run \
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
--strategies one_shot \
|
||||||
|
--models claude \
|
||||||
|
--categories coding \
|
||||||
|
--tests ReadFile,WriteFile \
|
||||||
|
--json
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
NONINTERACTIVE_MODE: "true"
|
||||||
|
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Test multiple strategies
|
||||||
uses: codecov/codecov-action@v5
|
run: |
|
||||||
with:
|
echo "Testing multiple strategies..."
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
poetry run direct-benchmark run \
|
||||||
flags: agbenchmark,${{ runner.os }}
|
--strategies one_shot,plan_execute \
|
||||||
|
--models claude \
|
||||||
|
--tests ReadFile \
|
||||||
|
--parallel 2 \
|
||||||
|
--json
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
NONINTERACTIVE_MODE: "true"
|
||||||
|
|
||||||
self-test-with-agent:
|
# Run regression tests on maintain challenges
|
||||||
|
regression-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
timeout-minutes: 45
|
||||||
matrix:
|
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev'
|
||||||
agent-name: [forge]
|
defaults:
|
||||||
fail-fast: false
|
run:
|
||||||
timeout-minutes: 20
|
shell: bash
|
||||||
|
working-directory: classic
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -126,51 +136,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Poetry
|
- name: Install Poetry
|
||||||
run: |
|
run: |
|
||||||
curl -sSL https://install.python-poetry.org | python -
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: poetry install
|
||||||
|
|
||||||
- name: Run regression tests
|
- name: Run regression tests
|
||||||
working-directory: classic
|
|
||||||
run: |
|
run: |
|
||||||
./run agent start ${{ matrix.agent-name }}
|
echo "Running regression tests (previously beaten challenges)..."
|
||||||
cd ${{ matrix.agent-name }}
|
poetry run direct-benchmark run \
|
||||||
|
--strategies one_shot \
|
||||||
set +e # Ignore non-zero exit codes and continue execution
|
--models claude \
|
||||||
echo "Running the following command: poetry run agbenchmark --maintain --mock"
|
--maintain \
|
||||||
poetry run agbenchmark --maintain --mock
|
--parallel 4 \
|
||||||
EXIT_CODE=$?
|
--json
|
||||||
set -e # Stop ignoring non-zero exit codes
|
|
||||||
# Check if the exit code was 5, and if so, exit with 0 instead
|
|
||||||
if [ $EXIT_CODE -eq 5 ]; then
|
|
||||||
echo "regression_tests.json is empty."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Running the following command: poetry run agbenchmark --mock"
|
|
||||||
poetry run agbenchmark --mock
|
|
||||||
|
|
||||||
echo "Running the following command: poetry run agbenchmark --mock --category=data"
|
|
||||||
poetry run agbenchmark --mock --category=data
|
|
||||||
|
|
||||||
echo "Running the following command: poetry run agbenchmark --mock --category=coding"
|
|
||||||
poetry run agbenchmark --mock --category=coding
|
|
||||||
|
|
||||||
# echo "Running the following command: poetry run agbenchmark --test=WriteFile"
|
|
||||||
# poetry run agbenchmark --test=WriteFile
|
|
||||||
cd ../benchmark
|
|
||||||
poetry install
|
|
||||||
echo "Adding the BUILD_SKILL_TREE environment variable. This will attempt to add new elements in the skill tree. If new elements are added, the CI fails because they should have been pushed"
|
|
||||||
export BUILD_SKILL_TREE=true
|
|
||||||
|
|
||||||
# poetry run agbenchmark --mock
|
|
||||||
|
|
||||||
# CHANGED=$(git diff --name-only | grep -E '(agbenchmark/challenges)|(../classic/frontend/assets)') || echo "No diffs"
|
|
||||||
# if [ ! -z "$CHANGED" ]; then
|
|
||||||
# echo "There are unstaged changes please run agbenchmark and commit those changes since they are needed."
|
|
||||||
# echo "$CHANGED"
|
|
||||||
# exit 1
|
|
||||||
# else
|
|
||||||
# echo "No unstaged changes."
|
|
||||||
# fi
|
|
||||||
env:
|
env:
|
||||||
|
CI: true
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
TELEMETRY_ENVIRONMENT: autogpt-benchmark-ci
|
NONINTERACTIVE_MODE: "true"
|
||||||
TELEMETRY_OPT_IN: ${{ github.ref_name == 'master' }}
|
|
||||||
|
|||||||
182
.github/workflows/classic-forge-ci.yml
vendored
@@ -6,13 +6,11 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- '.github/workflows/classic-forge-ci.yml'
|
- '.github/workflows/classic-forge-ci.yml'
|
||||||
- 'classic/forge/**'
|
- 'classic/forge/**'
|
||||||
- '!classic/forge/tests/vcr_cassettes'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master, dev, release-* ]
|
branches: [ master, dev, release-* ]
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/classic-forge-ci.yml'
|
- '.github/workflows/classic-forge-ci.yml'
|
||||||
- 'classic/forge/**'
|
- 'classic/forge/**'
|
||||||
- '!classic/forge/tests/vcr_cassettes'
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ format('forge-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
|
group: ${{ format('forge-ci-{0}', github.head_ref && format('{0}-{1}', github.event_name, github.event.pull_request.number) || github.sha) }}
|
||||||
@@ -21,115 +19,38 @@ concurrency:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
working-directory: classic/forge
|
working-directory: classic
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10"]
|
|
||||||
platform-os: [ubuntu, macos, macos-arm64, windows]
|
|
||||||
runs-on: ${{ matrix.platform-os != 'macos-arm64' && format('{0}-latest', matrix.platform-os) || 'macos-14' }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Quite slow on macOS (2~4 minutes to set up Docker)
|
- name: Start MinIO service
|
||||||
# - name: Set up Docker (macOS)
|
|
||||||
# if: runner.os == 'macOS'
|
|
||||||
# uses: crazy-max/ghaction-setup-docker@v3
|
|
||||||
|
|
||||||
- name: Start MinIO service (Linux)
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
working-directory: '.'
|
working-directory: '.'
|
||||||
run: |
|
run: |
|
||||||
docker pull minio/minio:edge-cicd
|
docker pull minio/minio:edge-cicd
|
||||||
docker run -d -p 9000:9000 minio/minio:edge-cicd
|
docker run -d -p 9000:9000 minio/minio:edge-cicd
|
||||||
|
|
||||||
- name: Start MinIO service (macOS)
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
working-directory: ${{ runner.temp }}
|
|
||||||
run: |
|
|
||||||
brew install minio/stable/minio
|
|
||||||
mkdir data
|
|
||||||
minio server ./data &
|
|
||||||
|
|
||||||
# No MinIO on Windows:
|
|
||||||
# - Windows doesn't support running Linux Docker containers
|
|
||||||
# - It doesn't seem possible to start background processes on Windows. They are
|
|
||||||
# killed after the step returns.
|
|
||||||
# See: https://github.com/actions/runner/issues/598#issuecomment-2011890429
|
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Checkout cassettes
|
- name: Set up Python 3.12
|
||||||
if: ${{ startsWith(github.event_name, 'pull_request') }}
|
|
||||||
env:
|
|
||||||
PR_BASE: ${{ github.event.pull_request.base.ref }}
|
|
||||||
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
|
|
||||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
|
||||||
run: |
|
|
||||||
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
|
|
||||||
cassette_base_branch="${PR_BASE}"
|
|
||||||
cd tests/vcr_cassettes
|
|
||||||
|
|
||||||
if ! git ls-remote --exit-code --heads origin $cassette_base_branch ; then
|
|
||||||
cassette_base_branch="master"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git ls-remote --exit-code --heads origin $cassette_branch ; then
|
|
||||||
git fetch origin $cassette_branch
|
|
||||||
git fetch origin $cassette_base_branch
|
|
||||||
|
|
||||||
git checkout $cassette_branch
|
|
||||||
|
|
||||||
# Pick non-conflicting cassette updates from the base branch
|
|
||||||
git merge --no-commit --strategy-option=ours origin/$cassette_base_branch
|
|
||||||
echo "Using cassettes from mirror branch '$cassette_branch'," \
|
|
||||||
"synced to upstream branch '$cassette_base_branch'."
|
|
||||||
else
|
|
||||||
git checkout -b $cassette_branch
|
|
||||||
echo "Branch '$cassette_branch' does not exist in cassette submodule." \
|
|
||||||
"Using cassettes from '$cassette_base_branch'."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Set up Python dependency cache
|
- name: Set up Python dependency cache
|
||||||
# On Windows, unpacking cached dependencies takes longer than just installing them
|
|
||||||
if: runner.os != 'Windows'
|
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.os == 'macOS' && '~/Library/Caches/pypoetry' || '~/.cache/pypoetry' }}
|
path: ~/.cache/pypoetry
|
||||||
key: poetry-${{ runner.os }}-${{ hashFiles('classic/forge/poetry.lock') }}
|
key: poetry-${{ runner.os }}-${{ hashFiles('classic/poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Poetry (Unix)
|
- name: Install Poetry
|
||||||
if: runner.os != 'Windows'
|
run: curl -sSL https://install.python-poetry.org | python3 -
|
||||||
run: |
|
|
||||||
curl -sSL https://install.python-poetry.org | python3 -
|
|
||||||
|
|
||||||
if [ "${{ runner.os }}" = "macOS" ]; then
|
|
||||||
PATH="$HOME/.local/bin:$PATH"
|
|
||||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Poetry (Windows)
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
|
|
||||||
|
|
||||||
$env:PATH += ";$env:APPDATA\Python\Scripts"
|
|
||||||
echo "$env:APPDATA\Python\Scripts" >> $env:GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: poetry install
|
run: poetry install
|
||||||
@@ -140,12 +61,15 @@ jobs:
|
|||||||
--cov=forge --cov-branch --cov-report term-missing --cov-report xml \
|
--cov=forge --cov-branch --cov-report term-missing --cov-report xml \
|
||||||
--durations=10 \
|
--durations=10 \
|
||||||
--junitxml=junit.xml -o junit_family=legacy \
|
--junitxml=junit.xml -o junit_family=legacy \
|
||||||
forge
|
forge/forge forge/tests
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
PLAIN_OUTPUT: True
|
PLAIN_OUTPUT: True
|
||||||
|
# API keys - tests that need these will skip if not available
|
||||||
|
# Secrets are not available to fork PRs (GitHub security feature)
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
S3_ENDPOINT_URL: ${{ runner.os != 'Windows' && 'http://127.0.0.1:9000' || '' }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
S3_ENDPOINT_URL: http://127.0.0.1:9000
|
||||||
AWS_ACCESS_KEY_ID: minioadmin
|
AWS_ACCESS_KEY_ID: minioadmin
|
||||||
AWS_SECRET_ACCESS_KEY: minioadmin
|
AWS_SECRET_ACCESS_KEY: minioadmin
|
||||||
|
|
||||||
@@ -159,85 +83,11 @@ jobs:
|
|||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
flags: forge,${{ runner.os }}
|
flags: forge
|
||||||
|
|
||||||
- id: setup_git_auth
|
|
||||||
name: Set up git token authentication
|
|
||||||
# Cassettes may be pushed even when tests fail
|
|
||||||
if: success() || failure()
|
|
||||||
run: |
|
|
||||||
config_key="http.${{ github.server_url }}/.extraheader"
|
|
||||||
if [ "${{ runner.os }}" = 'macOS' ]; then
|
|
||||||
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64)
|
|
||||||
else
|
|
||||||
base64_pat=$(echo -n "pat:${{ secrets.PAT_REVIEW }}" | base64 -w0)
|
|
||||||
fi
|
|
||||||
|
|
||||||
git config "$config_key" \
|
|
||||||
"Authorization: Basic $base64_pat"
|
|
||||||
|
|
||||||
cd tests/vcr_cassettes
|
|
||||||
git config "$config_key" \
|
|
||||||
"Authorization: Basic $base64_pat"
|
|
||||||
|
|
||||||
echo "config_key=$config_key" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- id: push_cassettes
|
|
||||||
name: Push updated cassettes
|
|
||||||
# For pull requests, push updated cassettes even when tests fail
|
|
||||||
if: github.event_name == 'push' || (! github.event.pull_request.head.repo.fork && (success() || failure()))
|
|
||||||
env:
|
|
||||||
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
|
|
||||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
|
||||||
run: |
|
|
||||||
if [ "${{ startsWith(github.event_name, 'pull_request') }}" = "true" ]; then
|
|
||||||
is_pull_request=true
|
|
||||||
cassette_branch="${PR_AUTHOR}-${PR_BRANCH}"
|
|
||||||
else
|
|
||||||
cassette_branch="${{ github.ref_name }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd tests/vcr_cassettes
|
|
||||||
# Commit & push changes to cassettes if any
|
|
||||||
if ! git diff --quiet; then
|
|
||||||
git add .
|
|
||||||
git commit -m "Auto-update cassettes"
|
|
||||||
git push origin HEAD:$cassette_branch
|
|
||||||
if [ ! $is_pull_request ]; then
|
|
||||||
cd ../..
|
|
||||||
git add tests/vcr_cassettes
|
|
||||||
git commit -m "Update cassette submodule"
|
|
||||||
git push origin HEAD:$cassette_branch
|
|
||||||
fi
|
|
||||||
echo "updated=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "updated=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "No cassette changes to commit"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Post Set up git token auth
|
|
||||||
if: steps.setup_git_auth.outcome == 'success'
|
|
||||||
run: |
|
|
||||||
git config --unset-all '${{ steps.setup_git_auth.outputs.config_key }}'
|
|
||||||
git submodule foreach git config --unset-all '${{ steps.setup_git_auth.outputs.config_key }}'
|
|
||||||
|
|
||||||
- name: Apply "behaviour change" label and comment on PR
|
|
||||||
if: ${{ startsWith(github.event_name, 'pull_request') }}
|
|
||||||
run: |
|
|
||||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
|
||||||
TOKEN="${{ secrets.PAT_REVIEW }}"
|
|
||||||
REPO="${{ github.repository }}"
|
|
||||||
|
|
||||||
if [[ "${{ steps.push_cassettes.outputs.updated }}" == "true" ]]; then
|
|
||||||
echo "Adding label and comment..."
|
|
||||||
echo $TOKEN | gh auth login --with-token
|
|
||||||
gh issue edit $PR_NUMBER --add-label "behaviour change"
|
|
||||||
gh issue comment $PR_NUMBER --body "You changed AutoGPT's behaviour on ${{ runner.os }}. The cassettes have been updated and will be merged to the submodule when this Pull Request gets merged."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload logs to artifact
|
- name: Upload logs to artifact
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-logs
|
name: test-logs
|
||||||
path: classic/forge/logs/
|
path: classic/logs/
|
||||||
|
|||||||
60
.github/workflows/classic-frontend-ci.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Classic - Frontend CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- dev
|
|
||||||
- 'ci-test*' # This will match any branch that starts with "ci-test"
|
|
||||||
paths:
|
|
||||||
- 'classic/frontend/**'
|
|
||||||
- '.github/workflows/classic-frontend-ci.yml'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'classic/frontend/**'
|
|
||||||
- '.github/workflows/classic-frontend-ci.yml'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
BUILD_BRANCH: ${{ format('classic-frontend-build/{0}', github.ref_name) }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
flutter-version: '3.13.2'
|
|
||||||
|
|
||||||
- name: Build Flutter to Web
|
|
||||||
run: |
|
|
||||||
cd classic/frontend
|
|
||||||
flutter build web --base-href /app/
|
|
||||||
|
|
||||||
# - name: Commit and Push to ${{ env.BUILD_BRANCH }}
|
|
||||||
# if: github.event_name == 'push'
|
|
||||||
# run: |
|
|
||||||
# git config --local user.email "action@github.com"
|
|
||||||
# git config --local user.name "GitHub Action"
|
|
||||||
# git add classic/frontend/build/web
|
|
||||||
# git checkout -B ${{ env.BUILD_BRANCH }}
|
|
||||||
# git commit -m "Update frontend build to ${GITHUB_SHA:0:7}" -a
|
|
||||||
# git push -f origin ${{ env.BUILD_BRANCH }}
|
|
||||||
|
|
||||||
- name: Create PR ${{ env.BUILD_BRANCH }} -> ${{ github.ref_name }}
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
uses: peter-evans/create-pull-request@v7
|
|
||||||
with:
|
|
||||||
add-paths: classic/frontend/build/web
|
|
||||||
base: ${{ github.ref_name }}
|
|
||||||
branch: ${{ env.BUILD_BRANCH }}
|
|
||||||
delete-branch: true
|
|
||||||
title: "Update frontend build in `${{ github.ref_name }}`"
|
|
||||||
body: "This PR updates the frontend build based on commit ${{ github.sha }}."
|
|
||||||
commit-message: "Update frontend build based on commit ${{ github.sha }}"
|
|
||||||
67
.github/workflows/classic-python-checks.yml
vendored
@@ -7,7 +7,9 @@ on:
|
|||||||
- '.github/workflows/classic-python-checks-ci.yml'
|
- '.github/workflows/classic-python-checks-ci.yml'
|
||||||
- 'classic/original_autogpt/**'
|
- 'classic/original_autogpt/**'
|
||||||
- 'classic/forge/**'
|
- 'classic/forge/**'
|
||||||
- 'classic/benchmark/**'
|
- 'classic/direct_benchmark/**'
|
||||||
|
- 'classic/pyproject.toml'
|
||||||
|
- 'classic/poetry.lock'
|
||||||
- '**.py'
|
- '**.py'
|
||||||
- '!classic/forge/tests/vcr_cassettes'
|
- '!classic/forge/tests/vcr_cassettes'
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -16,7 +18,9 @@ on:
|
|||||||
- '.github/workflows/classic-python-checks-ci.yml'
|
- '.github/workflows/classic-python-checks-ci.yml'
|
||||||
- 'classic/original_autogpt/**'
|
- 'classic/original_autogpt/**'
|
||||||
- 'classic/forge/**'
|
- 'classic/forge/**'
|
||||||
- 'classic/benchmark/**'
|
- 'classic/direct_benchmark/**'
|
||||||
|
- 'classic/pyproject.toml'
|
||||||
|
- 'classic/poetry.lock'
|
||||||
- '**.py'
|
- '**.py'
|
||||||
- '!classic/forge/tests/vcr_cassettes'
|
- '!classic/forge/tests/vcr_cassettes'
|
||||||
|
|
||||||
@@ -27,44 +31,13 @@ concurrency:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
working-directory: classic
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-changed-parts:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- id: changes-in
|
|
||||||
name: Determine affected subprojects
|
|
||||||
uses: dorny/paths-filter@v3
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
original_autogpt:
|
|
||||||
- classic/original_autogpt/autogpt/**
|
|
||||||
- classic/original_autogpt/tests/**
|
|
||||||
- classic/original_autogpt/poetry.lock
|
|
||||||
forge:
|
|
||||||
- classic/forge/forge/**
|
|
||||||
- classic/forge/tests/**
|
|
||||||
- classic/forge/poetry.lock
|
|
||||||
benchmark:
|
|
||||||
- classic/benchmark/agbenchmark/**
|
|
||||||
- classic/benchmark/tests/**
|
|
||||||
- classic/benchmark/poetry.lock
|
|
||||||
outputs:
|
|
||||||
changed-parts: ${{ steps.changes-in.outputs.changes }}
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
needs: get-changed-parts
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
min-python-version: "3.10"
|
min-python-version: "3.12"
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
sub-package: ${{ fromJson(needs.get-changed-parts.outputs.changed-parts) }}
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -81,42 +54,31 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry
|
path: ~/.cache/pypoetry
|
||||||
key: ${{ runner.os }}-poetry-${{ hashFiles(format('{0}/poetry.lock', matrix.sub-package)) }}
|
key: ${{ runner.os }}-poetry-${{ hashFiles('classic/poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Poetry
|
- name: Install Poetry
|
||||||
run: curl -sSL https://install.python-poetry.org | python3 -
|
run: curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: poetry -C classic/${{ matrix.sub-package }} install
|
run: poetry install
|
||||||
|
|
||||||
# Lint
|
# Lint
|
||||||
|
|
||||||
- name: Lint (isort)
|
- name: Lint (isort)
|
||||||
run: poetry run isort --check .
|
run: poetry run isort --check .
|
||||||
working-directory: classic/${{ matrix.sub-package }}
|
|
||||||
|
|
||||||
- name: Lint (Black)
|
- name: Lint (Black)
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
run: poetry run black --check .
|
run: poetry run black --check .
|
||||||
working-directory: classic/${{ matrix.sub-package }}
|
|
||||||
|
|
||||||
- name: Lint (Flake8)
|
- name: Lint (Flake8)
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
run: poetry run flake8 .
|
run: poetry run flake8 .
|
||||||
working-directory: classic/${{ matrix.sub-package }}
|
|
||||||
|
|
||||||
types:
|
types:
|
||||||
needs: get-changed-parts
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
min-python-version: "3.10"
|
min-python-version: "3.12"
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
sub-package: ${{ fromJson(needs.get-changed-parts.outputs.changed-parts) }}
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -133,19 +95,16 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry
|
path: ~/.cache/pypoetry
|
||||||
key: ${{ runner.os }}-poetry-${{ hashFiles(format('{0}/poetry.lock', matrix.sub-package)) }}
|
key: ${{ runner.os }}-poetry-${{ hashFiles('classic/poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Poetry
|
- name: Install Poetry
|
||||||
run: curl -sSL https://install.python-poetry.org | python3 -
|
run: curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
|
|
||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: poetry -C classic/${{ matrix.sub-package }} install
|
run: poetry install
|
||||||
|
|
||||||
# Typecheck
|
# Typecheck
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
run: poetry run pyright
|
run: poetry run pyright
|
||||||
working-directory: classic/${{ matrix.sub-package }}
|
|
||||||
|
|||||||
38
.github/workflows/platform-frontend-ci.yml
vendored
@@ -128,7 +128,7 @@ jobs:
|
|||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
exitOnceUploaded: true
|
exitOnceUploaded: true
|
||||||
|
|
||||||
e2e_test:
|
test:
|
||||||
runs-on: big-boi
|
runs-on: big-boi
|
||||||
needs: setup
|
needs: setup
|
||||||
strategy:
|
strategy:
|
||||||
@@ -258,39 +258,3 @@ jobs:
|
|||||||
- name: Print Final Docker Compose logs
|
- name: Print Final Docker Compose logs
|
||||||
if: always()
|
if: always()
|
||||||
run: docker compose -f ../docker-compose.yml logs
|
run: docker compose -f ../docker-compose.yml logs
|
||||||
|
|
||||||
integration_test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: setup
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22.18.0"
|
|
||||||
|
|
||||||
- name: Enable corepack
|
|
||||||
run: corepack enable
|
|
||||||
|
|
||||||
- name: Restore dependencies cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.pnpm-store
|
|
||||||
key: ${{ needs.setup.outputs.cache-key }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-${{ hashFiles('autogpt_platform/frontend/pnpm-lock.yaml') }}
|
|
||||||
${{ runner.os }}-pnpm-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Generate API client
|
|
||||||
run: pnpm generate:api
|
|
||||||
|
|
||||||
- name: Run Integration Tests
|
|
||||||
run: pnpm test:unit
|
|
||||||
|
|||||||
10
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
classic/original_autogpt/keys.py
|
classic/original_autogpt/keys.py
|
||||||
classic/original_autogpt/*.json
|
classic/original_autogpt/*.json
|
||||||
auto_gpt_workspace/*
|
auto_gpt_workspace/*
|
||||||
|
.autogpt/
|
||||||
*.mpeg
|
*.mpeg
|
||||||
.env
|
.env
|
||||||
# Root .env files
|
# Root .env files
|
||||||
@@ -159,6 +160,10 @@ CURRENT_BULLETIN.md
|
|||||||
|
|
||||||
# AgBenchmark
|
# AgBenchmark
|
||||||
classic/benchmark/agbenchmark/reports/
|
classic/benchmark/agbenchmark/reports/
|
||||||
|
classic/reports/
|
||||||
|
classic/direct_benchmark/reports/
|
||||||
|
classic/.benchmark_workspaces/
|
||||||
|
classic/direct_benchmark/.benchmark_workspaces/
|
||||||
|
|
||||||
# Nodejs
|
# Nodejs
|
||||||
package-lock.json
|
package-lock.json
|
||||||
@@ -177,5 +182,8 @@ autogpt_platform/backend/settings.py
|
|||||||
|
|
||||||
*.ign.*
|
*.ign.*
|
||||||
.test-contents
|
.test-contents
|
||||||
.claude/settings.local.json
|
**/.claude/settings.local.json
|
||||||
/autogpt_platform/backend/logs
|
/autogpt_platform/backend/logs
|
||||||
|
|
||||||
|
# Test database
|
||||||
|
test.db
|
||||||
|
|||||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "classic/forge/tests/vcr_cassettes"]
|
|
||||||
path = classic/forge/tests/vcr_cassettes
|
|
||||||
url = https://github.com/Significant-Gravitas/Auto-GPT-test-cassettes
|
|
||||||
@@ -43,29 +43,10 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
- id: poetry-install
|
- id: poetry-install
|
||||||
name: Check & Install dependencies - Classic - AutoGPT
|
name: Check & Install dependencies - Classic
|
||||||
alias: poetry-install-classic-autogpt
|
alias: poetry-install-classic
|
||||||
entry: poetry -C classic/original_autogpt install
|
entry: poetry -C classic install
|
||||||
# include forge source (since it's a path dependency)
|
files: ^classic/poetry\.lock$
|
||||||
files: ^classic/(original_autogpt|forge)/poetry\.lock$
|
|
||||||
types: [file]
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- id: poetry-install
|
|
||||||
name: Check & Install dependencies - Classic - Forge
|
|
||||||
alias: poetry-install-classic-forge
|
|
||||||
entry: poetry -C classic/forge install
|
|
||||||
files: ^classic/forge/poetry\.lock$
|
|
||||||
types: [file]
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- id: poetry-install
|
|
||||||
name: Check & Install dependencies - Classic - Benchmark
|
|
||||||
alias: poetry-install-classic-benchmark
|
|
||||||
entry: poetry -C classic/benchmark install
|
|
||||||
files: ^classic/benchmark/poetry\.lock$
|
|
||||||
types: [file]
|
types: [file]
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
@@ -116,26 +97,10 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
|
|
||||||
- id: isort
|
- id: isort
|
||||||
name: Lint (isort) - Classic - AutoGPT
|
name: Lint (isort) - Classic
|
||||||
alias: isort-classic-autogpt
|
alias: isort-classic
|
||||||
entry: poetry -P classic/original_autogpt run isort -p autogpt
|
entry: bash -c 'cd classic && poetry run isort $(echo "$@" | sed "s|classic/||g")' --
|
||||||
files: ^classic/original_autogpt/
|
files: ^classic/(original_autogpt|forge|direct_benchmark)/
|
||||||
types: [file, python]
|
|
||||||
language: system
|
|
||||||
|
|
||||||
- id: isort
|
|
||||||
name: Lint (isort) - Classic - Forge
|
|
||||||
alias: isort-classic-forge
|
|
||||||
entry: poetry -P classic/forge run isort -p forge
|
|
||||||
files: ^classic/forge/
|
|
||||||
types: [file, python]
|
|
||||||
language: system
|
|
||||||
|
|
||||||
- id: isort
|
|
||||||
name: Lint (isort) - Classic - Benchmark
|
|
||||||
alias: isort-classic-benchmark
|
|
||||||
entry: poetry -P classic/benchmark run isort -p agbenchmark
|
|
||||||
files: ^classic/benchmark/
|
|
||||||
types: [file, python]
|
types: [file, python]
|
||||||
language: system
|
language: system
|
||||||
|
|
||||||
@@ -149,26 +114,13 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 7.0.0
|
rev: 7.0.0
|
||||||
# To have flake8 load the config of the individual subprojects, we have to call
|
# Use consolidated flake8 config at classic/.flake8
|
||||||
# them separately.
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
name: Lint (Flake8) - Classic - AutoGPT
|
name: Lint (Flake8) - Classic
|
||||||
alias: flake8-classic-autogpt
|
alias: flake8-classic
|
||||||
files: ^classic/original_autogpt/(autogpt|scripts|tests)/
|
files: ^classic/(original_autogpt|forge|direct_benchmark)/
|
||||||
args: [--config=classic/original_autogpt/.flake8]
|
args: [--config=classic/.flake8]
|
||||||
|
|
||||||
- id: flake8
|
|
||||||
name: Lint (Flake8) - Classic - Forge
|
|
||||||
alias: flake8-classic-forge
|
|
||||||
files: ^classic/forge/(forge|tests)/
|
|
||||||
args: [--config=classic/forge/.flake8]
|
|
||||||
|
|
||||||
- id: flake8
|
|
||||||
name: Lint (Flake8) - Classic - Benchmark
|
|
||||||
alias: flake8-classic-benchmark
|
|
||||||
files: ^classic/benchmark/(agbenchmark|tests)/((?!reports).)*[/.]
|
|
||||||
args: [--config=classic/benchmark/.flake8]
|
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
@@ -204,29 +156,10 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
- id: pyright
|
- id: pyright
|
||||||
name: Typecheck - Classic - AutoGPT
|
name: Typecheck - Classic
|
||||||
alias: pyright-classic-autogpt
|
alias: pyright-classic
|
||||||
entry: poetry -C classic/original_autogpt run pyright
|
entry: poetry -C classic run pyright
|
||||||
# include forge source (since it's a path dependency) but exclude *_test.py files:
|
files: ^classic/(original_autogpt|forge|direct_benchmark)/.*\.py$|^classic/poetry\.lock$
|
||||||
files: ^(classic/original_autogpt/((autogpt|scripts|tests)/|poetry\.lock$)|classic/forge/(forge/.*(?<!_test)\.py|poetry\.lock)$)
|
|
||||||
types: [file]
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- id: pyright
|
|
||||||
name: Typecheck - Classic - Forge
|
|
||||||
alias: pyright-classic-forge
|
|
||||||
entry: poetry -C classic/forge run pyright
|
|
||||||
files: ^classic/forge/(forge/|poetry\.lock$)
|
|
||||||
types: [file]
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- id: pyright
|
|
||||||
name: Typecheck - Classic - Benchmark
|
|
||||||
alias: pyright-classic-benchmark
|
|
||||||
entry: poetry -C classic/benchmark run pyright
|
|
||||||
files: ^classic/benchmark/(agbenchmark/|tests/|poetry\.lock$)
|
|
||||||
types: [file]
|
types: [file]
|
||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|||||||
26
AGENTS.md
@@ -16,32 +16,6 @@ See `docs/content/platform/getting-started.md` for setup instructions.
|
|||||||
- Format Python code with `poetry run format`.
|
- Format Python code with `poetry run format`.
|
||||||
- Format frontend code using `pnpm format`.
|
- Format frontend code using `pnpm format`.
|
||||||
|
|
||||||
|
|
||||||
## Frontend guidelines:
|
|
||||||
|
|
||||||
See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference:
|
|
||||||
|
|
||||||
1. **Pages**: Create in `src/app/(platform)/feature-name/page.tsx`
|
|
||||||
- Add `usePageName.ts` hook for logic
|
|
||||||
- Put sub-components in local `components/` folder
|
|
||||||
2. **Components**: Structure as `ComponentName/ComponentName.tsx` + `useComponentName.ts` + `helpers.ts`
|
|
||||||
- Use design system components from `src/components/` (atoms, molecules, organisms)
|
|
||||||
- Never use `src/components/__legacy__/*`
|
|
||||||
3. **Data fetching**: Use generated API hooks from `@/app/api/__generated__/endpoints/`
|
|
||||||
- Regenerate with `pnpm generate:api`
|
|
||||||
- Pattern: `use{Method}{Version}{OperationName}`
|
|
||||||
4. **Styling**: Tailwind CSS only, use design tokens, Phosphor Icons only
|
|
||||||
5. **Testing**: Add Storybook stories for new components, Playwright for E2E
|
|
||||||
6. **Code conventions**: Function declarations (not arrow functions) for components/handlers
|
|
||||||
- Component props should be `interface Props { ... }` (not exported) unless the interface needs to be used outside the component
|
|
||||||
- Separate render logic from business logic (component.tsx + useComponent.ts + helpers.ts)
|
|
||||||
- Colocate state when possible and avoid creating large components, use sub-components ( local `/components` folder next to the parent component ) when sensible
|
|
||||||
- Avoid large hooks, abstract logic into `helpers.ts` files when sensible
|
|
||||||
- Use function declarations for components, arrow functions only for callbacks
|
|
||||||
- No barrel files or `index.ts` re-exports
|
|
||||||
- Do not use `useCallback` or `useMemo` unless strictly needed
|
|
||||||
- Avoid comments at all times unless the code is very complex
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Backend: `poetry run test` (runs pytest with a docker based postgres + prisma).
|
- Backend: `poetry run test` (runs pytest with a docker based postgres + prisma).
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ If you get any pushback or hit complex block conditions check the new_blocks gui
|
|||||||
3. Write tests alongside the route file
|
3. Write tests alongside the route file
|
||||||
4. Run `poetry run test` to verify
|
4. Run `poetry run test` to verify
|
||||||
|
|
||||||
### Frontend guidelines:
|
**Frontend feature development:**
|
||||||
|
|
||||||
See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference:
|
See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference:
|
||||||
|
|
||||||
@@ -217,14 +217,6 @@ See `/frontend/CONTRIBUTING.md` for complete patterns. Quick reference:
|
|||||||
4. **Styling**: Tailwind CSS only, use design tokens, Phosphor Icons only
|
4. **Styling**: Tailwind CSS only, use design tokens, Phosphor Icons only
|
||||||
5. **Testing**: Add Storybook stories for new components, Playwright for E2E
|
5. **Testing**: Add Storybook stories for new components, Playwright for E2E
|
||||||
6. **Code conventions**: Function declarations (not arrow functions) for components/handlers
|
6. **Code conventions**: Function declarations (not arrow functions) for components/handlers
|
||||||
- Component props should be `interface Props { ... }` (not exported) unless the interface needs to be used outside the component
|
|
||||||
- Separate render logic from business logic (component.tsx + useComponent.ts + helpers.ts)
|
|
||||||
- Colocate state when possible and avoid creating large components, use sub-components ( local `/components` folder next to the parent component ) when sensible
|
|
||||||
- Avoid large hooks, abstract logic into `helpers.ts` files when sensible
|
|
||||||
- Use function declarations for components, arrow functions only for callbacks
|
|
||||||
- No barrel files or `index.ts` re-exports
|
|
||||||
- Do not use `useCallback` or `useMemo` unless strictly needed
|
|
||||||
- Avoid comments at all times unless the code is very complex
|
|
||||||
|
|
||||||
### Security Implementation
|
### Security Implementation
|
||||||
|
|
||||||
|
|||||||
@@ -290,11 +290,6 @@ async def _cache_session(session: ChatSession) -> None:
|
|||||||
await async_redis.setex(redis_key, config.session_ttl, session.model_dump_json())
|
await async_redis.setex(redis_key, config.session_ttl, session.model_dump_json())
|
||||||
|
|
||||||
|
|
||||||
async def cache_chat_session(session: ChatSession) -> None:
|
|
||||||
"""Cache a chat session without persisting to the database."""
|
|
||||||
await _cache_session(session)
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_session_from_db(session_id: str) -> ChatSession | None:
|
async def _get_session_from_db(session_id: str) -> ChatSession | None:
|
||||||
"""Get a chat session from the database."""
|
"""Get a chat session from the database."""
|
||||||
prisma_session = await chat_db.get_chat_session(session_id)
|
prisma_session = await chat_db.get_chat_session(session_id)
|
||||||
|
|||||||
@@ -172,12 +172,12 @@ async def get_session(
|
|||||||
user_id: The optional authenticated user ID, or None for anonymous access.
|
user_id: The optional authenticated user ID, or None for anonymous access.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
SessionDetailResponse: Details for the requested session, or None if not found.
|
SessionDetailResponse: Details for the requested session; raises NotFoundError if not found.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
session = await get_chat_session(session_id, user_id)
|
session = await get_chat_session(session_id, user_id)
|
||||||
if not session:
|
if not session:
|
||||||
raise NotFoundError(f"Session {session_id} not found.")
|
raise NotFoundError(f"Session {session_id} not found")
|
||||||
|
|
||||||
messages = [message.model_dump() for message in session.messages]
|
messages = [message.model_dump() for message in session.messages]
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -222,8 +222,6 @@ async def stream_chat_post(
|
|||||||
session = await _validate_and_get_session(session_id, user_id)
|
session = await _validate_and_get_session(session_id, user_id)
|
||||||
|
|
||||||
async def event_generator() -> AsyncGenerator[str, None]:
|
async def event_generator() -> AsyncGenerator[str, None]:
|
||||||
chunk_count = 0
|
|
||||||
first_chunk_type: str | None = None
|
|
||||||
async for chunk in chat_service.stream_chat_completion(
|
async for chunk in chat_service.stream_chat_completion(
|
||||||
session_id,
|
session_id,
|
||||||
request.message,
|
request.message,
|
||||||
@@ -232,26 +230,7 @@ async def stream_chat_post(
|
|||||||
session=session, # Pass pre-fetched session to avoid double-fetch
|
session=session, # Pass pre-fetched session to avoid double-fetch
|
||||||
context=request.context,
|
context=request.context,
|
||||||
):
|
):
|
||||||
if chunk_count < 3:
|
|
||||||
logger.info(
|
|
||||||
"Chat stream chunk",
|
|
||||||
extra={
|
|
||||||
"session_id": session_id,
|
|
||||||
"chunk_type": str(chunk.type),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not first_chunk_type:
|
|
||||||
first_chunk_type = str(chunk.type)
|
|
||||||
chunk_count += 1
|
|
||||||
yield chunk.to_sse()
|
yield chunk.to_sse()
|
||||||
logger.info(
|
|
||||||
"Chat stream completed",
|
|
||||||
extra={
|
|
||||||
"session_id": session_id,
|
|
||||||
"chunk_count": chunk_count,
|
|
||||||
"first_chunk_type": first_chunk_type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# AI SDK protocol termination
|
# AI SDK protocol termination
|
||||||
yield "data: [DONE]\n\n"
|
yield "data: [DONE]\n\n"
|
||||||
|
|
||||||
@@ -296,8 +275,6 @@ async def stream_chat_get(
|
|||||||
session = await _validate_and_get_session(session_id, user_id)
|
session = await _validate_and_get_session(session_id, user_id)
|
||||||
|
|
||||||
async def event_generator() -> AsyncGenerator[str, None]:
|
async def event_generator() -> AsyncGenerator[str, None]:
|
||||||
chunk_count = 0
|
|
||||||
first_chunk_type: str | None = None
|
|
||||||
async for chunk in chat_service.stream_chat_completion(
|
async for chunk in chat_service.stream_chat_completion(
|
||||||
session_id,
|
session_id,
|
||||||
message,
|
message,
|
||||||
@@ -305,26 +282,7 @@ async def stream_chat_get(
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
session=session, # Pass pre-fetched session to avoid double-fetch
|
session=session, # Pass pre-fetched session to avoid double-fetch
|
||||||
):
|
):
|
||||||
if chunk_count < 3:
|
|
||||||
logger.info(
|
|
||||||
"Chat stream chunk",
|
|
||||||
extra={
|
|
||||||
"session_id": session_id,
|
|
||||||
"chunk_type": str(chunk.type),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not first_chunk_type:
|
|
||||||
first_chunk_type = str(chunk.type)
|
|
||||||
chunk_count += 1
|
|
||||||
yield chunk.to_sse()
|
yield chunk.to_sse()
|
||||||
logger.info(
|
|
||||||
"Chat stream completed",
|
|
||||||
extra={
|
|
||||||
"session_id": session_id,
|
|
||||||
"chunk_count": chunk_count,
|
|
||||||
"first_chunk_type": first_chunk_type,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
# AI SDK protocol termination
|
# AI SDK protocol termination
|
||||||
yield "data: [DONE]\n\n"
|
yield "data: [DONE]\n\n"
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
from asyncio import CancelledError
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from langfuse import get_client, propagate_attributes
|
from langfuse import get_client, propagate_attributes
|
||||||
from langfuse.openai import openai # type: ignore
|
from langfuse.openai import openai # type: ignore
|
||||||
from openai import (
|
from openai import APIConnectionError, APIError, APIStatusError, RateLimitError
|
||||||
APIConnectionError,
|
|
||||||
APIError,
|
|
||||||
APIStatusError,
|
|
||||||
PermissionDeniedError,
|
|
||||||
RateLimitError,
|
|
||||||
)
|
|
||||||
from openai.types.chat import ChatCompletionChunk, ChatCompletionToolParam
|
from openai.types.chat import ChatCompletionChunk, ChatCompletionToolParam
|
||||||
|
|
||||||
from backend.data.understanding import (
|
from backend.data.understanding import (
|
||||||
@@ -29,7 +21,6 @@ from .model import (
|
|||||||
ChatMessage,
|
ChatMessage,
|
||||||
ChatSession,
|
ChatSession,
|
||||||
Usage,
|
Usage,
|
||||||
cache_chat_session,
|
|
||||||
get_chat_session,
|
get_chat_session,
|
||||||
update_session_title,
|
update_session_title,
|
||||||
upsert_chat_session,
|
upsert_chat_session,
|
||||||
@@ -305,10 +296,6 @@ async def stream_chat_completion(
|
|||||||
content="",
|
content="",
|
||||||
)
|
)
|
||||||
accumulated_tool_calls: list[dict[str, Any]] = []
|
accumulated_tool_calls: list[dict[str, Any]] = []
|
||||||
has_saved_assistant_message = False
|
|
||||||
has_appended_streaming_message = False
|
|
||||||
last_cache_time = 0.0
|
|
||||||
last_cache_content_len = 0
|
|
||||||
|
|
||||||
# Wrap main logic in try/finally to ensure Langfuse observations are always ended
|
# Wrap main logic in try/finally to ensure Langfuse observations are always ended
|
||||||
has_yielded_end = False
|
has_yielded_end = False
|
||||||
@@ -345,23 +332,6 @@ async def stream_chat_completion(
|
|||||||
assert assistant_response.content is not None
|
assert assistant_response.content is not None
|
||||||
assistant_response.content += delta
|
assistant_response.content += delta
|
||||||
has_received_text = True
|
has_received_text = True
|
||||||
if not has_appended_streaming_message:
|
|
||||||
session.messages.append(assistant_response)
|
|
||||||
has_appended_streaming_message = True
|
|
||||||
current_time = time.monotonic()
|
|
||||||
content_len = len(assistant_response.content)
|
|
||||||
if (
|
|
||||||
current_time - last_cache_time >= 1.0
|
|
||||||
and content_len > last_cache_content_len
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
await cache_chat_session(session)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Failed to cache partial session {session.session_id}: {e}"
|
|
||||||
)
|
|
||||||
last_cache_time = current_time
|
|
||||||
last_cache_content_len = content_len
|
|
||||||
yield chunk
|
yield chunk
|
||||||
elif isinstance(chunk, StreamTextEnd):
|
elif isinstance(chunk, StreamTextEnd):
|
||||||
# Emit text-end after text completes
|
# Emit text-end after text completes
|
||||||
@@ -420,42 +390,10 @@ async def stream_chat_completion(
|
|||||||
if has_received_text and not text_streaming_ended:
|
if has_received_text and not text_streaming_ended:
|
||||||
yield StreamTextEnd(id=text_block_id)
|
yield StreamTextEnd(id=text_block_id)
|
||||||
text_streaming_ended = True
|
text_streaming_ended = True
|
||||||
|
|
||||||
# Save assistant message before yielding finish to ensure it's persisted
|
|
||||||
# even if client disconnects immediately after receiving StreamFinish
|
|
||||||
if not has_saved_assistant_message:
|
|
||||||
messages_to_save_early: list[ChatMessage] = []
|
|
||||||
if accumulated_tool_calls:
|
|
||||||
assistant_response.tool_calls = (
|
|
||||||
accumulated_tool_calls
|
|
||||||
)
|
|
||||||
if not has_appended_streaming_message and (
|
|
||||||
assistant_response.content
|
|
||||||
or assistant_response.tool_calls
|
|
||||||
):
|
|
||||||
messages_to_save_early.append(assistant_response)
|
|
||||||
messages_to_save_early.extend(tool_response_messages)
|
|
||||||
|
|
||||||
if messages_to_save_early:
|
|
||||||
session.messages.extend(messages_to_save_early)
|
|
||||||
logger.info(
|
|
||||||
f"Saving assistant message before StreamFinish: "
|
|
||||||
f"content_len={len(assistant_response.content or '')}, "
|
|
||||||
f"tool_calls={len(assistant_response.tool_calls or [])}, "
|
|
||||||
f"tool_responses={len(tool_response_messages)}"
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
messages_to_save_early
|
|
||||||
or has_appended_streaming_message
|
|
||||||
):
|
|
||||||
await upsert_chat_session(session)
|
|
||||||
has_saved_assistant_message = True
|
|
||||||
|
|
||||||
has_yielded_end = True
|
has_yielded_end = True
|
||||||
yield chunk
|
yield chunk
|
||||||
elif isinstance(chunk, StreamError):
|
elif isinstance(chunk, StreamError):
|
||||||
has_yielded_error = True
|
has_yielded_error = True
|
||||||
yield chunk
|
|
||||||
elif isinstance(chunk, StreamUsage):
|
elif isinstance(chunk, StreamUsage):
|
||||||
session.usage.append(
|
session.usage.append(
|
||||||
Usage(
|
Usage(
|
||||||
@@ -475,27 +413,6 @@ async def stream_chat_completion(
|
|||||||
langfuse.update_current_trace(output=str(tool_response_messages))
|
langfuse.update_current_trace(output=str(tool_response_messages))
|
||||||
langfuse.update_current_span(output=str(tool_response_messages))
|
langfuse.update_current_span(output=str(tool_response_messages))
|
||||||
|
|
||||||
except CancelledError:
|
|
||||||
if not has_saved_assistant_message:
|
|
||||||
if accumulated_tool_calls:
|
|
||||||
assistant_response.tool_calls = accumulated_tool_calls
|
|
||||||
if assistant_response.content:
|
|
||||||
assistant_response.content = (
|
|
||||||
f"{assistant_response.content}\n\n[interrupted]"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
assistant_response.content = "[interrupted]"
|
|
||||||
if not has_appended_streaming_message:
|
|
||||||
session.messages.append(assistant_response)
|
|
||||||
if tool_response_messages:
|
|
||||||
session.messages.extend(tool_response_messages)
|
|
||||||
try:
|
|
||||||
await upsert_chat_session(session)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Failed to save interrupted session {session.session_id}: {e}"
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during stream: {e!s}", exc_info=True)
|
logger.error(f"Error during stream: {e!s}", exc_info=True)
|
||||||
|
|
||||||
@@ -517,19 +434,14 @@ async def stream_chat_completion(
|
|||||||
# Add assistant message if it has content or tool calls
|
# Add assistant message if it has content or tool calls
|
||||||
if accumulated_tool_calls:
|
if accumulated_tool_calls:
|
||||||
assistant_response.tool_calls = accumulated_tool_calls
|
assistant_response.tool_calls = accumulated_tool_calls
|
||||||
if not has_appended_streaming_message and (
|
if assistant_response.content or assistant_response.tool_calls:
|
||||||
assistant_response.content or assistant_response.tool_calls
|
|
||||||
):
|
|
||||||
messages_to_save.append(assistant_response)
|
messages_to_save.append(assistant_response)
|
||||||
|
|
||||||
# Add tool response messages after assistant message
|
# Add tool response messages after assistant message
|
||||||
messages_to_save.extend(tool_response_messages)
|
messages_to_save.extend(tool_response_messages)
|
||||||
|
|
||||||
if not has_saved_assistant_message:
|
session.messages.extend(messages_to_save)
|
||||||
if messages_to_save:
|
await upsert_chat_session(session)
|
||||||
session.messages.extend(messages_to_save)
|
|
||||||
if messages_to_save or has_appended_streaming_message:
|
|
||||||
await upsert_chat_session(session)
|
|
||||||
|
|
||||||
if not has_yielded_error:
|
if not has_yielded_error:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
@@ -560,49 +472,38 @@ async def stream_chat_completion(
|
|||||||
return # Exit after retry to avoid double-saving in finally block
|
return # Exit after retry to avoid double-saving in finally block
|
||||||
|
|
||||||
# Normal completion path - save session and handle tool call continuation
|
# Normal completion path - save session and handle tool call continuation
|
||||||
# Only save if we haven't already saved when StreamFinish was received
|
logger.info(
|
||||||
if not has_saved_assistant_message:
|
f"Normal completion path: session={session.session_id}, "
|
||||||
|
f"current message_count={len(session.messages)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build the messages list in the correct order
|
||||||
|
messages_to_save: list[ChatMessage] = []
|
||||||
|
|
||||||
|
# Add assistant message with tool_calls if any
|
||||||
|
if accumulated_tool_calls:
|
||||||
|
assistant_response.tool_calls = accumulated_tool_calls
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Normal completion path: session={session.session_id}, "
|
f"Added {len(accumulated_tool_calls)} tool calls to assistant message"
|
||||||
f"current message_count={len(session.messages)}"
|
)
|
||||||
|
if assistant_response.content or assistant_response.tool_calls:
|
||||||
|
messages_to_save.append(assistant_response)
|
||||||
|
logger.info(
|
||||||
|
f"Saving assistant message with content_len={len(assistant_response.content or '')}, tool_calls={len(assistant_response.tool_calls or [])}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build the messages list in the correct order
|
# Add tool response messages after assistant message
|
||||||
messages_to_save: list[ChatMessage] = []
|
messages_to_save.extend(tool_response_messages)
|
||||||
|
logger.info(
|
||||||
|
f"Saving {len(tool_response_messages)} tool response messages, "
|
||||||
|
f"total_to_save={len(messages_to_save)}"
|
||||||
|
)
|
||||||
|
|
||||||
# Add assistant message with tool_calls if any
|
session.messages.extend(messages_to_save)
|
||||||
if accumulated_tool_calls:
|
logger.info(
|
||||||
assistant_response.tool_calls = accumulated_tool_calls
|
f"Extended session messages, new message_count={len(session.messages)}"
|
||||||
logger.info(
|
)
|
||||||
f"Added {len(accumulated_tool_calls)} tool calls to assistant message"
|
await upsert_chat_session(session)
|
||||||
)
|
|
||||||
if not has_appended_streaming_message and (
|
|
||||||
assistant_response.content or assistant_response.tool_calls
|
|
||||||
):
|
|
||||||
messages_to_save.append(assistant_response)
|
|
||||||
logger.info(
|
|
||||||
f"Saving assistant message with content_len={len(assistant_response.content or '')}, tool_calls={len(assistant_response.tool_calls or [])}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add tool response messages after assistant message
|
|
||||||
messages_to_save.extend(tool_response_messages)
|
|
||||||
logger.info(
|
|
||||||
f"Saving {len(tool_response_messages)} tool response messages, "
|
|
||||||
f"total_to_save={len(messages_to_save)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if messages_to_save:
|
|
||||||
session.messages.extend(messages_to_save)
|
|
||||||
logger.info(
|
|
||||||
f"Extended session messages, new message_count={len(session.messages)}"
|
|
||||||
)
|
|
||||||
if messages_to_save or has_appended_streaming_message:
|
|
||||||
await upsert_chat_session(session)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
"Assistant message already saved when StreamFinish was received, "
|
|
||||||
"skipping duplicate save"
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we did a tool call, stream the chat completion again to get the next response
|
# If we did a tool call, stream the chat completion again to get the next response
|
||||||
if has_done_tool_call:
|
if has_done_tool_call:
|
||||||
@@ -644,12 +545,6 @@ def _is_retryable_error(error: Exception) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_region_blocked_error(error: Exception) -> bool:
|
|
||||||
if isinstance(error, PermissionDeniedError):
|
|
||||||
return "not available in your region" in str(error).lower()
|
|
||||||
return "not available in your region" in str(error).lower()
|
|
||||||
|
|
||||||
|
|
||||||
async def _stream_chat_chunks(
|
async def _stream_chat_chunks(
|
||||||
session: ChatSession,
|
session: ChatSession,
|
||||||
tools: list[ChatCompletionToolParam],
|
tools: list[ChatCompletionToolParam],
|
||||||
@@ -842,18 +737,7 @@ async def _stream_chat_chunks(
|
|||||||
f"Error in stream (not retrying): {e!s}",
|
f"Error in stream (not retrying): {e!s}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
error_code = None
|
error_response = StreamError(errorText=str(e))
|
||||||
error_text = str(e)
|
|
||||||
if _is_region_blocked_error(e):
|
|
||||||
error_code = "MODEL_NOT_AVAILABLE_REGION"
|
|
||||||
error_text = (
|
|
||||||
"This model is not available in your region. "
|
|
||||||
"Please connect via VPN and try again."
|
|
||||||
)
|
|
||||||
error_response = StreamError(
|
|
||||||
errorText=error_text,
|
|
||||||
code=error_code,
|
|
||||||
)
|
|
||||||
yield error_response
|
yield error_response
|
||||||
yield StreamFinish()
|
yield StreamFinish()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ async def save_agent_to_library(
|
|||||||
library_agents = await library_db.create_library_agent(
|
library_agents = await library_db.create_library_agent(
|
||||||
graph=created_graph,
|
graph=created_graph,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
sensitive_action_safe_mode=True,
|
|
||||||
create_library_agents_for_sub_graphs=False,
|
create_library_agents_for_sub_graphs=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from .models import (
|
|||||||
UserReadiness,
|
UserReadiness,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
build_missing_credentials_from_graph,
|
check_user_has_required_credentials,
|
||||||
extract_credentials_from_schema,
|
extract_credentials_from_schema,
|
||||||
fetch_graph_from_store_slug,
|
fetch_graph_from_store_slug,
|
||||||
get_or_create_library_agent,
|
get_or_create_library_agent,
|
||||||
@@ -237,13 +237,15 @@ class RunAgentTool(BaseTool):
|
|||||||
# Return credentials needed response with input data info
|
# Return credentials needed response with input data info
|
||||||
# The UI handles credential setup automatically, so the message
|
# The UI handles credential setup automatically, so the message
|
||||||
# focuses on asking about input data
|
# focuses on asking about input data
|
||||||
requirements_creds_dict = build_missing_credentials_from_graph(
|
credentials = extract_credentials_from_schema(
|
||||||
graph, None
|
graph.credentials_input_schema
|
||||||
)
|
)
|
||||||
missing_credentials_dict = build_missing_credentials_from_graph(
|
missing_creds_check = await check_user_has_required_credentials(
|
||||||
graph, graph_credentials
|
user_id, credentials
|
||||||
)
|
)
|
||||||
requirements_creds_list = list(requirements_creds_dict.values())
|
missing_credentials_dict = {
|
||||||
|
c.id: c.model_dump() for c in missing_creds_check
|
||||||
|
}
|
||||||
|
|
||||||
return SetupRequirementsResponse(
|
return SetupRequirementsResponse(
|
||||||
message=self._build_inputs_message(graph, MSG_WHAT_VALUES_TO_USE),
|
message=self._build_inputs_message(graph, MSG_WHAT_VALUES_TO_USE),
|
||||||
@@ -257,7 +259,7 @@ class RunAgentTool(BaseTool):
|
|||||||
ready_to_run=False,
|
ready_to_run=False,
|
||||||
),
|
),
|
||||||
requirements={
|
requirements={
|
||||||
"credentials": requirements_creds_list,
|
"credentials": [c.model_dump() for c in credentials],
|
||||||
"inputs": self._get_inputs_list(graph.input_schema),
|
"inputs": self._get_inputs_list(graph.input_schema),
|
||||||
"execution_modes": self._get_execution_modes(graph),
|
"execution_modes": self._get_execution_modes(graph),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from .models import (
|
|||||||
ToolResponseBase,
|
ToolResponseBase,
|
||||||
UserReadiness,
|
UserReadiness,
|
||||||
)
|
)
|
||||||
from .utils import build_missing_credentials_from_field_info
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -190,11 +189,7 @@ class RunBlockTool(BaseTool):
|
|||||||
|
|
||||||
if missing_credentials:
|
if missing_credentials:
|
||||||
# Return setup requirements response with missing credentials
|
# Return setup requirements response with missing credentials
|
||||||
credentials_fields_info = block.input_schema.get_credentials_fields_info()
|
missing_creds_dict = {c.id: c.model_dump() for c in missing_credentials}
|
||||||
missing_creds_dict = build_missing_credentials_from_field_info(
|
|
||||||
credentials_fields_info, set(matched_credentials.keys())
|
|
||||||
)
|
|
||||||
missing_creds_list = list(missing_creds_dict.values())
|
|
||||||
|
|
||||||
return SetupRequirementsResponse(
|
return SetupRequirementsResponse(
|
||||||
message=(
|
message=(
|
||||||
@@ -211,7 +206,7 @@ class RunBlockTool(BaseTool):
|
|||||||
ready_to_run=False,
|
ready_to_run=False,
|
||||||
),
|
),
|
||||||
requirements={
|
requirements={
|
||||||
"credentials": missing_creds_list,
|
"credentials": [c.model_dump() for c in missing_credentials],
|
||||||
"inputs": self._get_inputs_list(block),
|
"inputs": self._get_inputs_list(block),
|
||||||
"execution_modes": ["immediate"],
|
"execution_modes": ["immediate"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from backend.api.features.library import model as library_model
|
|||||||
from backend.api.features.store import db as store_db
|
from backend.api.features.store import db as store_db
|
||||||
from backend.data import graph as graph_db
|
from backend.data import graph as graph_db
|
||||||
from backend.data.graph import GraphModel
|
from backend.data.graph import GraphModel
|
||||||
from backend.data.model import CredentialsFieldInfo, CredentialsMetaInput
|
from backend.data.model import CredentialsMetaInput
|
||||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||||
from backend.util.exceptions import NotFoundError
|
from backend.util.exceptions import NotFoundError
|
||||||
|
|
||||||
@@ -89,59 +89,6 @@ def extract_credentials_from_schema(
|
|||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
def _serialize_missing_credential(
|
|
||||||
field_key: str, field_info: CredentialsFieldInfo
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Convert credential field info into a serializable dict that preserves all supported
|
|
||||||
credential types (e.g., api_key + oauth2) so the UI can offer multiple options.
|
|
||||||
"""
|
|
||||||
supported_types = sorted(field_info.supported_types)
|
|
||||||
provider = next(iter(field_info.provider), "unknown")
|
|
||||||
scopes = sorted(field_info.required_scopes or [])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": field_key,
|
|
||||||
"title": field_key.replace("_", " ").title(),
|
|
||||||
"provider": provider,
|
|
||||||
"provider_name": provider.replace("_", " ").title(),
|
|
||||||
"type": supported_types[0] if supported_types else "api_key",
|
|
||||||
"types": supported_types,
|
|
||||||
"scopes": scopes,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_missing_credentials_from_graph(
|
|
||||||
graph: GraphModel, matched_credentials: dict[str, CredentialsMetaInput] | None
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Build a missing_credentials mapping from a graph's aggregated credentials inputs,
|
|
||||||
preserving all supported credential types for each field.
|
|
||||||
"""
|
|
||||||
matched_keys = set(matched_credentials.keys()) if matched_credentials else set()
|
|
||||||
aggregated_fields = graph.aggregate_credentials_inputs()
|
|
||||||
|
|
||||||
return {
|
|
||||||
field_key: _serialize_missing_credential(field_key, field_info)
|
|
||||||
for field_key, (field_info, _node_fields) in aggregated_fields.items()
|
|
||||||
if field_key not in matched_keys
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_missing_credentials_from_field_info(
|
|
||||||
credential_fields: dict[str, CredentialsFieldInfo],
|
|
||||||
matched_keys: set[str],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Build missing_credentials mapping from a simple credentials field info dictionary.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
field_key: _serialize_missing_credential(field_key, field_info)
|
|
||||||
for field_key, field_info in credential_fields.items()
|
|
||||||
if field_key not in matched_keys
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def extract_credentials_as_dict(
|
def extract_credentials_as_dict(
|
||||||
credentials_input_schema: dict[str, Any] | None,
|
credentials_input_schema: dict[str, Any] | None,
|
||||||
) -> dict[str, CredentialsMetaInput]:
|
) -> dict[str, CredentialsMetaInput]:
|
||||||
|
|||||||
@@ -401,11 +401,27 @@ async def add_generated_agent_image(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _initialize_graph_settings(graph: graph_db.GraphModel) -> GraphSettings:
|
||||||
|
"""
|
||||||
|
Initialize GraphSettings based on graph content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
graph: The graph to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GraphSettings with appropriate human_in_the_loop_safe_mode value
|
||||||
|
"""
|
||||||
|
if graph.has_human_in_the_loop:
|
||||||
|
# Graph has HITL blocks - set safe mode to True by default
|
||||||
|
return GraphSettings(human_in_the_loop_safe_mode=True)
|
||||||
|
else:
|
||||||
|
# Graph has no HITL blocks - keep None
|
||||||
|
return GraphSettings(human_in_the_loop_safe_mode=None)
|
||||||
|
|
||||||
|
|
||||||
async def create_library_agent(
|
async def create_library_agent(
|
||||||
graph: graph_db.GraphModel,
|
graph: graph_db.GraphModel,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
hitl_safe_mode: bool = True,
|
|
||||||
sensitive_action_safe_mode: bool = False,
|
|
||||||
create_library_agents_for_sub_graphs: bool = True,
|
create_library_agents_for_sub_graphs: bool = True,
|
||||||
) -> list[library_model.LibraryAgent]:
|
) -> list[library_model.LibraryAgent]:
|
||||||
"""
|
"""
|
||||||
@@ -414,8 +430,6 @@ async def create_library_agent(
|
|||||||
Args:
|
Args:
|
||||||
agent: The agent/Graph to add to the library.
|
agent: The agent/Graph to add to the library.
|
||||||
user_id: The user to whom the agent will be added.
|
user_id: The user to whom the agent will be added.
|
||||||
hitl_safe_mode: Whether HITL blocks require manual review (default True).
|
|
||||||
sensitive_action_safe_mode: Whether sensitive action blocks require review.
|
|
||||||
create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well.
|
create_library_agents_for_sub_graphs: If True, creates LibraryAgent records for sub-graphs as well.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -451,11 +465,7 @@ async def create_library_agent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
settings=SafeJson(
|
settings=SafeJson(
|
||||||
GraphSettings.from_graph(
|
_initialize_graph_settings(graph_entry).model_dump()
|
||||||
graph_entry,
|
|
||||||
hitl_safe_mode=hitl_safe_mode,
|
|
||||||
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
|
||||||
).model_dump()
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
include=library_agent_include(
|
include=library_agent_include(
|
||||||
@@ -617,6 +627,33 @@ async def update_library_agent(
|
|||||||
raise DatabaseError("Failed to update library agent") from e
|
raise DatabaseError("Failed to update library agent") from e
|
||||||
|
|
||||||
|
|
||||||
|
async def update_library_agent_settings(
|
||||||
|
user_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
settings: GraphSettings,
|
||||||
|
) -> library_model.LibraryAgent:
|
||||||
|
"""
|
||||||
|
Updates the settings for a specific LibraryAgent.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The owner of the LibraryAgent.
|
||||||
|
agent_id: The ID of the LibraryAgent to update.
|
||||||
|
settings: New GraphSettings to apply.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated LibraryAgent.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotFoundError: If the specified LibraryAgent does not exist.
|
||||||
|
DatabaseError: If there's an error in the update operation.
|
||||||
|
"""
|
||||||
|
return await update_library_agent(
|
||||||
|
library_agent_id=agent_id,
|
||||||
|
user_id=user_id,
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def delete_library_agent(
|
async def delete_library_agent(
|
||||||
library_agent_id: str, user_id: str, soft_delete: bool = True
|
library_agent_id: str, user_id: str, soft_delete: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -801,7 +838,7 @@ async def add_store_agent_to_library(
|
|||||||
"isCreatedByUser": False,
|
"isCreatedByUser": False,
|
||||||
"useGraphIsActiveVersion": False,
|
"useGraphIsActiveVersion": False,
|
||||||
"settings": SafeJson(
|
"settings": SafeJson(
|
||||||
GraphSettings.from_graph(graph_model).model_dump()
|
_initialize_graph_settings(graph_model).model_dump()
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
include=library_agent_include(
|
include=library_agent_include(
|
||||||
@@ -1191,15 +1228,8 @@ async def fork_library_agent(
|
|||||||
)
|
)
|
||||||
new_graph = await on_graph_activate(new_graph, user_id=user_id)
|
new_graph = await on_graph_activate(new_graph, user_id=user_id)
|
||||||
|
|
||||||
# Create a library agent for the new graph, preserving safe mode settings
|
# Create a library agent for the new graph
|
||||||
return (
|
return (await create_library_agent(new_graph, user_id))[0]
|
||||||
await create_library_agent(
|
|
||||||
new_graph,
|
|
||||||
user_id,
|
|
||||||
hitl_safe_mode=original_agent.settings.human_in_the_loop_safe_mode,
|
|
||||||
sensitive_action_safe_mode=original_agent.settings.sensitive_action_safe_mode,
|
|
||||||
)
|
|
||||||
)[0]
|
|
||||||
except prisma.errors.PrismaError as e:
|
except prisma.errors.PrismaError as e:
|
||||||
logger.error(f"Database error cloning library agent: {e}")
|
logger.error(f"Database error cloning library agent: {e}")
|
||||||
raise DatabaseError("Failed to fork library agent") from e
|
raise DatabaseError("Failed to fork library agent") from e
|
||||||
|
|||||||
@@ -73,12 +73,6 @@ class LibraryAgent(pydantic.BaseModel):
|
|||||||
has_external_trigger: bool = pydantic.Field(
|
has_external_trigger: bool = pydantic.Field(
|
||||||
description="Whether the agent has an external trigger (e.g. webhook) node"
|
description="Whether the agent has an external trigger (e.g. webhook) node"
|
||||||
)
|
)
|
||||||
has_human_in_the_loop: bool = pydantic.Field(
|
|
||||||
description="Whether the agent has human-in-the-loop blocks"
|
|
||||||
)
|
|
||||||
has_sensitive_action: bool = pydantic.Field(
|
|
||||||
description="Whether the agent has sensitive action blocks"
|
|
||||||
)
|
|
||||||
trigger_setup_info: Optional[GraphTriggerInfo] = None
|
trigger_setup_info: Optional[GraphTriggerInfo] = None
|
||||||
|
|
||||||
# Indicates whether there's a new output (based on recent runs)
|
# Indicates whether there's a new output (based on recent runs)
|
||||||
@@ -186,8 +180,6 @@ class LibraryAgent(pydantic.BaseModel):
|
|||||||
graph.credentials_input_schema if sub_graphs is not None else None
|
graph.credentials_input_schema if sub_graphs is not None else None
|
||||||
),
|
),
|
||||||
has_external_trigger=graph.has_external_trigger,
|
has_external_trigger=graph.has_external_trigger,
|
||||||
has_human_in_the_loop=graph.has_human_in_the_loop,
|
|
||||||
has_sensitive_action=graph.has_sensitive_action,
|
|
||||||
trigger_setup_info=graph.trigger_setup_info,
|
trigger_setup_info=graph.trigger_setup_info,
|
||||||
new_output=new_output,
|
new_output=new_output,
|
||||||
can_access_graph=can_access_graph,
|
can_access_graph=can_access_graph,
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ async def test_get_library_agents_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
has_human_in_the_loop=False,
|
|
||||||
has_sensitive_action=False,
|
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
recommended_schedule_cron=None,
|
recommended_schedule_cron=None,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
@@ -77,8 +75,6 @@ async def test_get_library_agents_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
has_human_in_the_loop=False,
|
|
||||||
has_sensitive_action=False,
|
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
recommended_schedule_cron=None,
|
recommended_schedule_cron=None,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
@@ -154,8 +150,6 @@ async def test_get_favorite_library_agents_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
has_human_in_the_loop=False,
|
|
||||||
has_sensitive_action=False,
|
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
recommended_schedule_cron=None,
|
recommended_schedule_cron=None,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
@@ -224,8 +218,6 @@ def test_add_agent_to_library_success(
|
|||||||
output_schema={"type": "object", "properties": {}},
|
output_schema={"type": "object", "properties": {}},
|
||||||
credentials_input_schema={"type": "object", "properties": {}},
|
credentials_input_schema={"type": "object", "properties": {}},
|
||||||
has_external_trigger=False,
|
has_external_trigger=False,
|
||||||
has_human_in_the_loop=False,
|
|
||||||
has_sensitive_action=False,
|
|
||||||
status=library_model.LibraryAgentStatus.COMPLETED,
|
status=library_model.LibraryAgentStatus.COMPLETED,
|
||||||
new_output=False,
|
new_output=False,
|
||||||
can_access_graph=True,
|
can_access_graph=True,
|
||||||
|
|||||||
@@ -154,7 +154,6 @@ async def store_content_embedding(
|
|||||||
|
|
||||||
# Upsert the embedding
|
# Upsert the embedding
|
||||||
# WHERE clause in DO UPDATE prevents PostgreSQL 15 bug with NULLS NOT DISTINCT
|
# WHERE clause in DO UPDATE prevents PostgreSQL 15 bug with NULLS NOT DISTINCT
|
||||||
# Use unqualified ::vector - pgvector is in search_path on all environments
|
|
||||||
await execute_raw_with_schema(
|
await execute_raw_with_schema(
|
||||||
"""
|
"""
|
||||||
INSERT INTO {schema_prefix}"UnifiedContentEmbedding" (
|
INSERT INTO {schema_prefix}"UnifiedContentEmbedding" (
|
||||||
@@ -178,6 +177,7 @@ async def store_content_embedding(
|
|||||||
searchable_text,
|
searchable_text,
|
||||||
metadata_json,
|
metadata_json,
|
||||||
client=client,
|
client=client,
|
||||||
|
set_public_search_path=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Stored embedding for {content_type}:{content_id}")
|
logger.info(f"Stored embedding for {content_type}:{content_id}")
|
||||||
@@ -236,6 +236,7 @@ async def get_content_embedding(
|
|||||||
content_type,
|
content_type,
|
||||||
content_id,
|
content_id,
|
||||||
user_id,
|
user_id,
|
||||||
|
set_public_search_path=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result and len(result) > 0:
|
if result and len(result) > 0:
|
||||||
@@ -870,45 +871,31 @@ async def semantic_search(
|
|||||||
# Add content type parameters and build placeholders dynamically
|
# Add content type parameters and build placeholders dynamically
|
||||||
content_type_start_idx = len(params) + 1
|
content_type_start_idx = len(params) + 1
|
||||||
content_type_placeholders = ", ".join(
|
content_type_placeholders = ", ".join(
|
||||||
"$" + str(content_type_start_idx + i) + '::{schema_prefix}"ContentType"'
|
f'${content_type_start_idx + i}::{{{{schema_prefix}}}}"ContentType"'
|
||||||
for i in range(len(content_types))
|
for i in range(len(content_types))
|
||||||
)
|
)
|
||||||
params.extend([ct.value for ct in content_types])
|
params.extend([ct.value for ct in content_types])
|
||||||
|
|
||||||
# Build min_similarity param index before appending
|
sql = f"""
|
||||||
min_similarity_idx = len(params) + 1
|
|
||||||
params.append(min_similarity)
|
|
||||||
|
|
||||||
# Use unqualified ::vector and <=> operator - pgvector is in search_path on all environments
|
|
||||||
sql = (
|
|
||||||
"""
|
|
||||||
SELECT
|
SELECT
|
||||||
"contentId" as content_id,
|
"contentId" as content_id,
|
||||||
"contentType" as content_type,
|
"contentType" as content_type,
|
||||||
"searchableText" as searchable_text,
|
"searchableText" as searchable_text,
|
||||||
metadata,
|
metadata,
|
||||||
1 - (embedding <=> '"""
|
1 - (embedding <=> '{embedding_str}'::vector) as similarity
|
||||||
+ embedding_str
|
FROM {{{{schema_prefix}}}}"UnifiedContentEmbedding"
|
||||||
+ """'::vector) as similarity
|
WHERE "contentType" IN ({content_type_placeholders})
|
||||||
FROM {schema_prefix}"UnifiedContentEmbedding"
|
{user_filter}
|
||||||
WHERE "contentType" IN ("""
|
AND 1 - (embedding <=> '{embedding_str}'::vector) >= ${len(params) + 1}
|
||||||
+ content_type_placeholders
|
|
||||||
+ """)
|
|
||||||
"""
|
|
||||||
+ user_filter
|
|
||||||
+ """
|
|
||||||
AND 1 - (embedding <=> '"""
|
|
||||||
+ embedding_str
|
|
||||||
+ """'::vector) >= $"""
|
|
||||||
+ str(min_similarity_idx)
|
|
||||||
+ """
|
|
||||||
ORDER BY similarity DESC
|
ORDER BY similarity DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
"""
|
"""
|
||||||
)
|
params.append(min_similarity)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = await query_raw_with_schema(sql, *params)
|
results = await query_raw_with_schema(
|
||||||
|
sql, *params, set_public_search_path=True
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"content_id": row["content_id"],
|
"content_id": row["content_id"],
|
||||||
@@ -935,41 +922,31 @@ async def semantic_search(
|
|||||||
# Add content type parameters and build placeholders dynamically
|
# Add content type parameters and build placeholders dynamically
|
||||||
content_type_start_idx = len(params_lexical) + 1
|
content_type_start_idx = len(params_lexical) + 1
|
||||||
content_type_placeholders_lexical = ", ".join(
|
content_type_placeholders_lexical = ", ".join(
|
||||||
"$" + str(content_type_start_idx + i) + '::{schema_prefix}"ContentType"'
|
f'${content_type_start_idx + i}::{{{{schema_prefix}}}}"ContentType"'
|
||||||
for i in range(len(content_types))
|
for i in range(len(content_types))
|
||||||
)
|
)
|
||||||
params_lexical.extend([ct.value for ct in content_types])
|
params_lexical.extend([ct.value for ct in content_types])
|
||||||
|
|
||||||
# Build query param index before appending
|
sql_lexical = f"""
|
||||||
query_param_idx = len(params_lexical) + 1
|
|
||||||
params_lexical.append(f"%{query}%")
|
|
||||||
|
|
||||||
# Use regular string (not f-string) for template to preserve {schema_prefix} placeholders
|
|
||||||
sql_lexical = (
|
|
||||||
"""
|
|
||||||
SELECT
|
SELECT
|
||||||
"contentId" as content_id,
|
"contentId" as content_id,
|
||||||
"contentType" as content_type,
|
"contentType" as content_type,
|
||||||
"searchableText" as searchable_text,
|
"searchableText" as searchable_text,
|
||||||
metadata,
|
metadata,
|
||||||
0.0 as similarity
|
0.0 as similarity
|
||||||
FROM {schema_prefix}"UnifiedContentEmbedding"
|
FROM {{{{schema_prefix}}}}"UnifiedContentEmbedding"
|
||||||
WHERE "contentType" IN ("""
|
WHERE "contentType" IN ({content_type_placeholders_lexical})
|
||||||
+ content_type_placeholders_lexical
|
{user_filter}
|
||||||
+ """)
|
AND "searchableText" ILIKE ${len(params_lexical) + 1}
|
||||||
"""
|
|
||||||
+ user_filter
|
|
||||||
+ """
|
|
||||||
AND "searchableText" ILIKE $"""
|
|
||||||
+ str(query_param_idx)
|
|
||||||
+ """
|
|
||||||
ORDER BY "updatedAt" DESC
|
ORDER BY "updatedAt" DESC
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
"""
|
"""
|
||||||
)
|
params_lexical.append(f"%{query}%")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
results = await query_raw_with_schema(sql_lexical, *params_lexical)
|
results = await query_raw_with_schema(
|
||||||
|
sql_lexical, *params_lexical, set_public_search_path=True
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"content_id": row["content_id"],
|
"content_id": row["content_id"],
|
||||||
|
|||||||
@@ -155,14 +155,18 @@ async def test_store_embedding_success(mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
# execute_raw is called once for INSERT (no separate SET search_path needed)
|
# execute_raw is called twice: once for SET search_path, once for INSERT
|
||||||
assert mock_client.execute_raw.call_count == 1
|
assert mock_client.execute_raw.call_count == 2
|
||||||
|
|
||||||
# Verify the INSERT query with the actual data
|
# First call: SET search_path
|
||||||
call_args = mock_client.execute_raw.call_args_list[0][0]
|
first_call_args = mock_client.execute_raw.call_args_list[0][0]
|
||||||
assert "test-version-id" in call_args
|
assert "SET search_path" in first_call_args[0]
|
||||||
assert "[0.1,0.2,0.3]" in call_args
|
|
||||||
assert None in call_args # userId should be None for store agents
|
# Second call: INSERT query with the actual data
|
||||||
|
second_call_args = mock_client.execute_raw.call_args_list[1][0]
|
||||||
|
assert "test-version-id" in second_call_args
|
||||||
|
assert "[0.1,0.2,0.3]" in second_call_args
|
||||||
|
assert None in second_call_args # userId should be None for store agents
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(loop_scope="session")
|
@pytest.mark.asyncio(loop_scope="session")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from prisma.enums import ContentType
|
from prisma.enums import ContentType
|
||||||
from rank_bm25 import BM25Okapi # type: ignore[import-untyped]
|
from rank_bm25 import BM25Okapi
|
||||||
|
|
||||||
from backend.api.features.store.embeddings import (
|
from backend.api.features.store.embeddings import (
|
||||||
EMBEDDING_DIM,
|
EMBEDDING_DIM,
|
||||||
@@ -363,7 +363,9 @@ async def unified_hybrid_search(
|
|||||||
LIMIT {limit_param} OFFSET {offset_param}
|
LIMIT {limit_param} OFFSET {offset_param}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
results = await query_raw_with_schema(sql_query, *params)
|
results = await query_raw_with_schema(
|
||||||
|
sql_query, *params, set_public_search_path=True
|
||||||
|
)
|
||||||
|
|
||||||
total = results[0]["total_count"] if results else 0
|
total = results[0]["total_count"] if results else 0
|
||||||
# Apply BM25 reranking
|
# Apply BM25 reranking
|
||||||
@@ -686,7 +688,9 @@ async def hybrid_search(
|
|||||||
LIMIT {limit_param} OFFSET {offset_param}
|
LIMIT {limit_param} OFFSET {offset_param}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
results = await query_raw_with_schema(sql_query, *params)
|
results = await query_raw_with_schema(
|
||||||
|
sql_query, *params, set_public_search_path=True
|
||||||
|
)
|
||||||
|
|
||||||
total = results[0]["total_count"] if results else 0
|
total = results[0]["total_count"] if results else 0
|
||||||
|
|
||||||
|
|||||||
@@ -761,8 +761,10 @@ async def create_new_graph(
|
|||||||
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
graph.reassign_ids(user_id=user_id, reassign_graph_id=True)
|
||||||
graph.validate_graph(for_run=False)
|
graph.validate_graph(for_run=False)
|
||||||
|
|
||||||
|
# The return value of the create graph & library function is intentionally not used here,
|
||||||
|
# as the graph already valid and no sub-graphs are returned back.
|
||||||
await graph_db.create_graph(graph, user_id=user_id)
|
await graph_db.create_graph(graph, user_id=user_id)
|
||||||
await library_db.create_library_agent(graph, user_id)
|
await library_db.create_library_agent(graph, user_id=user_id)
|
||||||
activated_graph = await on_graph_activate(graph, user_id=user_id)
|
activated_graph = await on_graph_activate(graph, user_id=user_id)
|
||||||
|
|
||||||
if create_graph.source == "builder":
|
if create_graph.source == "builder":
|
||||||
@@ -886,19 +888,21 @@ async def set_graph_active_version(
|
|||||||
async def _update_library_agent_version_and_settings(
|
async def _update_library_agent_version_and_settings(
|
||||||
user_id: str, agent_graph: graph_db.GraphModel
|
user_id: str, agent_graph: graph_db.GraphModel
|
||||||
) -> library_model.LibraryAgent:
|
) -> library_model.LibraryAgent:
|
||||||
|
# Keep the library agent up to date with the new active version
|
||||||
library = await library_db.update_agent_version_in_library(
|
library = await library_db.update_agent_version_in_library(
|
||||||
user_id, agent_graph.id, agent_graph.version
|
user_id, agent_graph.id, agent_graph.version
|
||||||
)
|
)
|
||||||
updated_settings = GraphSettings.from_graph(
|
# If the graph has HITL node, initialize the setting if it's not already set.
|
||||||
graph=agent_graph,
|
if (
|
||||||
hitl_safe_mode=library.settings.human_in_the_loop_safe_mode,
|
agent_graph.has_human_in_the_loop
|
||||||
sensitive_action_safe_mode=library.settings.sensitive_action_safe_mode,
|
and library.settings.human_in_the_loop_safe_mode is None
|
||||||
)
|
):
|
||||||
if updated_settings != library.settings:
|
await library_db.update_library_agent_settings(
|
||||||
library = await library_db.update_library_agent(
|
|
||||||
library_agent_id=library.id,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
settings=updated_settings,
|
agent_id=library.id,
|
||||||
|
settings=library.settings.model_copy(
|
||||||
|
update={"human_in_the_loop_safe_mode": True}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return library
|
return library
|
||||||
|
|
||||||
@@ -915,18 +919,21 @@ async def update_graph_settings(
|
|||||||
user_id: Annotated[str, Security(get_user_id)],
|
user_id: Annotated[str, Security(get_user_id)],
|
||||||
) -> GraphSettings:
|
) -> GraphSettings:
|
||||||
"""Update graph settings for the user's library agent."""
|
"""Update graph settings for the user's library agent."""
|
||||||
|
# Get the library agent for this graph
|
||||||
library_agent = await library_db.get_library_agent_by_graph_id(
|
library_agent = await library_db.get_library_agent_by_graph_id(
|
||||||
graph_id=graph_id, user_id=user_id
|
graph_id=graph_id, user_id=user_id
|
||||||
)
|
)
|
||||||
if not library_agent:
|
if not library_agent:
|
||||||
raise HTTPException(404, f"Graph #{graph_id} not found in user's library")
|
raise HTTPException(404, f"Graph #{graph_id} not found in user's library")
|
||||||
|
|
||||||
updated_agent = await library_db.update_library_agent(
|
# Update the library agent settings
|
||||||
library_agent_id=library_agent.id,
|
updated_agent = await library_db.update_library_agent_settings(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
agent_id=library_agent.id,
|
||||||
settings=settings,
|
settings=settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return the updated settings
|
||||||
return GraphSettings.model_validate(updated_agent.settings)
|
return GraphSettings.model_validate(updated_agent.settings)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -680,58 +680,3 @@ class ListIsEmptyBlock(Block):
|
|||||||
|
|
||||||
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
||||||
yield "is_empty", len(input_data.list) == 0
|
yield "is_empty", len(input_data.list) == 0
|
||||||
|
|
||||||
|
|
||||||
class ConcatenateListsBlock(Block):
|
|
||||||
class Input(BlockSchemaInput):
|
|
||||||
lists: List[List[Any]] = SchemaField(
|
|
||||||
description="A list of lists to concatenate together. All lists will be combined in order into a single list.",
|
|
||||||
placeholder="e.g., [[1, 2], [3, 4], [5, 6]]",
|
|
||||||
)
|
|
||||||
|
|
||||||
class Output(BlockSchemaOutput):
|
|
||||||
concatenated_list: List[Any] = SchemaField(
|
|
||||||
description="The concatenated list containing all elements from all input lists in order."
|
|
||||||
)
|
|
||||||
error: str = SchemaField(
|
|
||||||
description="Error message if concatenation failed due to invalid input types."
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(
|
|
||||||
id="3cf9298b-5817-4141-9d80-7c2cc5199c8e",
|
|
||||||
description="Concatenates multiple lists into a single list. All elements from all input lists are combined in order.",
|
|
||||||
categories={BlockCategory.BASIC},
|
|
||||||
input_schema=ConcatenateListsBlock.Input,
|
|
||||||
output_schema=ConcatenateListsBlock.Output,
|
|
||||||
test_input=[
|
|
||||||
{"lists": [[1, 2, 3], [4, 5, 6]]},
|
|
||||||
{"lists": [["a", "b"], ["c"], ["d", "e", "f"]]},
|
|
||||||
{"lists": [[1, 2], []]},
|
|
||||||
{"lists": []},
|
|
||||||
],
|
|
||||||
test_output=[
|
|
||||||
("concatenated_list", [1, 2, 3, 4, 5, 6]),
|
|
||||||
("concatenated_list", ["a", "b", "c", "d", "e", "f"]),
|
|
||||||
("concatenated_list", [1, 2]),
|
|
||||||
("concatenated_list", []),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
|
||||||
concatenated = []
|
|
||||||
for idx, lst in enumerate(input_data.lists):
|
|
||||||
if lst is None:
|
|
||||||
# Skip None values to avoid errors
|
|
||||||
continue
|
|
||||||
if not isinstance(lst, list):
|
|
||||||
# Type validation: each item must be a list
|
|
||||||
# Strings are iterable and would cause extend() to iterate character-by-character
|
|
||||||
# Non-iterable types would raise TypeError
|
|
||||||
yield "error", (
|
|
||||||
f"Invalid input at index {idx}: expected a list, got {type(lst).__name__}. "
|
|
||||||
f"All items in 'lists' must be lists (e.g., [[1, 2], [3, 4]])."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
concatenated.extend(lst)
|
|
||||||
yield "concatenated_list", concatenated
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class HITLReviewHelper:
|
|||||||
Exception: If review creation or status update fails
|
Exception: If review creation or status update fails
|
||||||
"""
|
"""
|
||||||
# Skip review if safe mode is disabled - return auto-approved result
|
# Skip review if safe mode is disabled - return auto-approved result
|
||||||
if not execution_context.human_in_the_loop_safe_mode:
|
if not execution_context.safe_mode:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Block {block_name} skipping review for node {node_exec_id} - safe mode disabled"
|
f"Block {block_name} skipping review for node {node_exec_id} - safe mode disabled"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class HumanInTheLoopBlock(Block):
|
|||||||
execution_context: ExecutionContext,
|
execution_context: ExecutionContext,
|
||||||
**_kwargs,
|
**_kwargs,
|
||||||
) -> BlockOutput:
|
) -> BlockOutput:
|
||||||
if not execution_context.human_in_the_loop_safe_mode:
|
if not execution_context.safe_mode:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"HITL block skipping review for node {node_exec_id} - safe mode disabled"
|
f"HITL block skipping review for node {node_exec_id} - safe mode disabled"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,10 +79,6 @@ class ModelMetadata(NamedTuple):
|
|||||||
provider: str
|
provider: str
|
||||||
context_window: int
|
context_window: int
|
||||||
max_output_tokens: int | None
|
max_output_tokens: int | None
|
||||||
display_name: str
|
|
||||||
provider_name: str
|
|
||||||
creator_name: str
|
|
||||||
price_tier: Literal[1, 2, 3]
|
|
||||||
|
|
||||||
|
|
||||||
class LlmModelMeta(EnumMeta):
|
class LlmModelMeta(EnumMeta):
|
||||||
@@ -175,26 +171,6 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
|
|||||||
V0_1_5_LG = "v0-1.5-lg"
|
V0_1_5_LG = "v0-1.5-lg"
|
||||||
V0_1_0_MD = "v0-1.0-md"
|
V0_1_0_MD = "v0-1.0-md"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_pydantic_json_schema__(cls, schema, handler):
|
|
||||||
json_schema = handler(schema)
|
|
||||||
llm_model_metadata = {}
|
|
||||||
for model in cls:
|
|
||||||
model_name = model.value
|
|
||||||
metadata = model.metadata
|
|
||||||
llm_model_metadata[model_name] = {
|
|
||||||
"creator": metadata.creator_name,
|
|
||||||
"creator_name": metadata.creator_name,
|
|
||||||
"title": metadata.display_name,
|
|
||||||
"provider": metadata.provider,
|
|
||||||
"provider_name": metadata.provider_name,
|
|
||||||
"name": model_name,
|
|
||||||
"price_tier": metadata.price_tier,
|
|
||||||
}
|
|
||||||
json_schema["llm_model"] = True
|
|
||||||
json_schema["llm_model_metadata"] = llm_model_metadata
|
|
||||||
return json_schema
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self) -> ModelMetadata:
|
def metadata(self) -> ModelMetadata:
|
||||||
return MODEL_METADATA[self]
|
return MODEL_METADATA[self]
|
||||||
@@ -214,291 +190,119 @@ class LlmModel(str, Enum, metaclass=LlmModelMeta):
|
|||||||
|
|
||||||
MODEL_METADATA = {
|
MODEL_METADATA = {
|
||||||
# https://platform.openai.com/docs/models
|
# https://platform.openai.com/docs/models
|
||||||
LlmModel.O3: ModelMetadata("openai", 200000, 100000, "O3", "OpenAI", "OpenAI", 2),
|
LlmModel.O3: ModelMetadata("openai", 200000, 100000),
|
||||||
LlmModel.O3_MINI: ModelMetadata(
|
LlmModel.O3_MINI: ModelMetadata("openai", 200000, 100000), # o3-mini-2025-01-31
|
||||||
"openai", 200000, 100000, "O3 Mini", "OpenAI", "OpenAI", 1
|
LlmModel.O1: ModelMetadata("openai", 200000, 100000), # o1-2024-12-17
|
||||||
), # o3-mini-2025-01-31
|
LlmModel.O1_MINI: ModelMetadata("openai", 128000, 65536), # o1-mini-2024-09-12
|
||||||
LlmModel.O1: ModelMetadata(
|
|
||||||
"openai", 200000, 100000, "O1", "OpenAI", "OpenAI", 3
|
|
||||||
), # o1-2024-12-17
|
|
||||||
LlmModel.O1_MINI: ModelMetadata(
|
|
||||||
"openai", 128000, 65536, "O1 Mini", "OpenAI", "OpenAI", 2
|
|
||||||
), # o1-mini-2024-09-12
|
|
||||||
# GPT-5 models
|
# GPT-5 models
|
||||||
LlmModel.GPT5_2: ModelMetadata(
|
LlmModel.GPT5_2: ModelMetadata("openai", 400000, 128000),
|
||||||
"openai", 400000, 128000, "GPT-5.2", "OpenAI", "OpenAI", 3
|
LlmModel.GPT5_1: ModelMetadata("openai", 400000, 128000),
|
||||||
),
|
LlmModel.GPT5: ModelMetadata("openai", 400000, 128000),
|
||||||
LlmModel.GPT5_1: ModelMetadata(
|
LlmModel.GPT5_MINI: ModelMetadata("openai", 400000, 128000),
|
||||||
"openai", 400000, 128000, "GPT-5.1", "OpenAI", "OpenAI", 2
|
LlmModel.GPT5_NANO: ModelMetadata("openai", 400000, 128000),
|
||||||
),
|
LlmModel.GPT5_CHAT: ModelMetadata("openai", 400000, 16384),
|
||||||
LlmModel.GPT5: ModelMetadata(
|
LlmModel.GPT41: ModelMetadata("openai", 1047576, 32768),
|
||||||
"openai", 400000, 128000, "GPT-5", "OpenAI", "OpenAI", 1
|
LlmModel.GPT41_MINI: ModelMetadata("openai", 1047576, 32768),
|
||||||
),
|
|
||||||
LlmModel.GPT5_MINI: ModelMetadata(
|
|
||||||
"openai", 400000, 128000, "GPT-5 Mini", "OpenAI", "OpenAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.GPT5_NANO: ModelMetadata(
|
|
||||||
"openai", 400000, 128000, "GPT-5 Nano", "OpenAI", "OpenAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.GPT5_CHAT: ModelMetadata(
|
|
||||||
"openai", 400000, 16384, "GPT-5 Chat Latest", "OpenAI", "OpenAI", 2
|
|
||||||
),
|
|
||||||
LlmModel.GPT41: ModelMetadata(
|
|
||||||
"openai", 1047576, 32768, "GPT-4.1", "OpenAI", "OpenAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.GPT41_MINI: ModelMetadata(
|
|
||||||
"openai", 1047576, 32768, "GPT-4.1 Mini", "OpenAI", "OpenAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.GPT4O_MINI: ModelMetadata(
|
LlmModel.GPT4O_MINI: ModelMetadata(
|
||||||
"openai", 128000, 16384, "GPT-4o Mini", "OpenAI", "OpenAI", 1
|
"openai", 128000, 16384
|
||||||
), # gpt-4o-mini-2024-07-18
|
), # gpt-4o-mini-2024-07-18
|
||||||
LlmModel.GPT4O: ModelMetadata(
|
LlmModel.GPT4O: ModelMetadata("openai", 128000, 16384), # gpt-4o-2024-08-06
|
||||||
"openai", 128000, 16384, "GPT-4o", "OpenAI", "OpenAI", 2
|
|
||||||
), # gpt-4o-2024-08-06
|
|
||||||
LlmModel.GPT4_TURBO: ModelMetadata(
|
LlmModel.GPT4_TURBO: ModelMetadata(
|
||||||
"openai", 128000, 4096, "GPT-4 Turbo", "OpenAI", "OpenAI", 3
|
"openai", 128000, 4096
|
||||||
), # gpt-4-turbo-2024-04-09
|
), # gpt-4-turbo-2024-04-09
|
||||||
LlmModel.GPT3_5_TURBO: ModelMetadata(
|
LlmModel.GPT3_5_TURBO: ModelMetadata("openai", 16385, 4096), # gpt-3.5-turbo-0125
|
||||||
"openai", 16385, 4096, "GPT-3.5 Turbo", "OpenAI", "OpenAI", 1
|
|
||||||
), # gpt-3.5-turbo-0125
|
|
||||||
# https://docs.anthropic.com/en/docs/about-claude/models
|
# https://docs.anthropic.com/en/docs/about-claude/models
|
||||||
LlmModel.CLAUDE_4_1_OPUS: ModelMetadata(
|
LlmModel.CLAUDE_4_1_OPUS: ModelMetadata(
|
||||||
"anthropic", 200000, 32000, "Claude Opus 4.1", "Anthropic", "Anthropic", 3
|
"anthropic", 200000, 32000
|
||||||
), # claude-opus-4-1-20250805
|
), # claude-opus-4-1-20250805
|
||||||
LlmModel.CLAUDE_4_OPUS: ModelMetadata(
|
LlmModel.CLAUDE_4_OPUS: ModelMetadata(
|
||||||
"anthropic", 200000, 32000, "Claude Opus 4", "Anthropic", "Anthropic", 3
|
"anthropic", 200000, 32000
|
||||||
), # claude-4-opus-20250514
|
), # claude-4-opus-20250514
|
||||||
LlmModel.CLAUDE_4_SONNET: ModelMetadata(
|
LlmModel.CLAUDE_4_SONNET: ModelMetadata(
|
||||||
"anthropic", 200000, 64000, "Claude Sonnet 4", "Anthropic", "Anthropic", 2
|
"anthropic", 200000, 64000
|
||||||
), # claude-4-sonnet-20250514
|
), # claude-4-sonnet-20250514
|
||||||
LlmModel.CLAUDE_4_5_OPUS: ModelMetadata(
|
LlmModel.CLAUDE_4_5_OPUS: ModelMetadata(
|
||||||
"anthropic", 200000, 64000, "Claude Opus 4.5", "Anthropic", "Anthropic", 3
|
"anthropic", 200000, 64000
|
||||||
), # claude-opus-4-5-20251101
|
), # claude-opus-4-5-20251101
|
||||||
LlmModel.CLAUDE_4_5_SONNET: ModelMetadata(
|
LlmModel.CLAUDE_4_5_SONNET: ModelMetadata(
|
||||||
"anthropic", 200000, 64000, "Claude Sonnet 4.5", "Anthropic", "Anthropic", 3
|
"anthropic", 200000, 64000
|
||||||
), # claude-sonnet-4-5-20250929
|
), # claude-sonnet-4-5-20250929
|
||||||
LlmModel.CLAUDE_4_5_HAIKU: ModelMetadata(
|
LlmModel.CLAUDE_4_5_HAIKU: ModelMetadata(
|
||||||
"anthropic", 200000, 64000, "Claude Haiku 4.5", "Anthropic", "Anthropic", 2
|
"anthropic", 200000, 64000
|
||||||
), # claude-haiku-4-5-20251001
|
), # claude-haiku-4-5-20251001
|
||||||
LlmModel.CLAUDE_3_7_SONNET: ModelMetadata(
|
LlmModel.CLAUDE_3_7_SONNET: ModelMetadata(
|
||||||
"anthropic", 200000, 64000, "Claude 3.7 Sonnet", "Anthropic", "Anthropic", 2
|
"anthropic", 200000, 64000
|
||||||
), # claude-3-7-sonnet-20250219
|
), # claude-3-7-sonnet-20250219
|
||||||
LlmModel.CLAUDE_3_HAIKU: ModelMetadata(
|
LlmModel.CLAUDE_3_HAIKU: ModelMetadata(
|
||||||
"anthropic", 200000, 4096, "Claude 3 Haiku", "Anthropic", "Anthropic", 1
|
"anthropic", 200000, 4096
|
||||||
), # claude-3-haiku-20240307
|
), # claude-3-haiku-20240307
|
||||||
# https://docs.aimlapi.com/api-overview/model-database/text-models
|
# https://docs.aimlapi.com/api-overview/model-database/text-models
|
||||||
LlmModel.AIML_API_QWEN2_5_72B: ModelMetadata(
|
LlmModel.AIML_API_QWEN2_5_72B: ModelMetadata("aiml_api", 32000, 8000),
|
||||||
"aiml_api", 32000, 8000, "Qwen 2.5 72B Instruct Turbo", "AI/ML", "Qwen", 1
|
LlmModel.AIML_API_LLAMA3_1_70B: ModelMetadata("aiml_api", 128000, 40000),
|
||||||
),
|
LlmModel.AIML_API_LLAMA3_3_70B: ModelMetadata("aiml_api", 128000, None),
|
||||||
LlmModel.AIML_API_LLAMA3_1_70B: ModelMetadata(
|
LlmModel.AIML_API_META_LLAMA_3_1_70B: ModelMetadata("aiml_api", 131000, 2000),
|
||||||
"aiml_api",
|
LlmModel.AIML_API_LLAMA_3_2_3B: ModelMetadata("aiml_api", 128000, None),
|
||||||
128000,
|
|
||||||
40000,
|
|
||||||
"Llama 3.1 Nemotron 70B Instruct",
|
|
||||||
"AI/ML",
|
|
||||||
"Nvidia",
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
LlmModel.AIML_API_LLAMA3_3_70B: ModelMetadata(
|
|
||||||
"aiml_api", 128000, None, "Llama 3.3 70B Instruct Turbo", "AI/ML", "Meta", 1
|
|
||||||
),
|
|
||||||
LlmModel.AIML_API_META_LLAMA_3_1_70B: ModelMetadata(
|
|
||||||
"aiml_api", 131000, 2000, "Llama 3.1 70B Instruct Turbo", "AI/ML", "Meta", 1
|
|
||||||
),
|
|
||||||
LlmModel.AIML_API_LLAMA_3_2_3B: ModelMetadata(
|
|
||||||
"aiml_api", 128000, None, "Llama 3.2 3B Instruct Turbo", "AI/ML", "Meta", 1
|
|
||||||
),
|
|
||||||
# https://console.groq.com/docs/models
|
# https://console.groq.com/docs/models
|
||||||
LlmModel.LLAMA3_3_70B: ModelMetadata(
|
LlmModel.LLAMA3_3_70B: ModelMetadata("groq", 128000, 32768),
|
||||||
"groq", 128000, 32768, "Llama 3.3 70B Versatile", "Groq", "Meta", 1
|
LlmModel.LLAMA3_1_8B: ModelMetadata("groq", 128000, 8192),
|
||||||
),
|
|
||||||
LlmModel.LLAMA3_1_8B: ModelMetadata(
|
|
||||||
"groq", 128000, 8192, "Llama 3.1 8B Instant", "Groq", "Meta", 1
|
|
||||||
),
|
|
||||||
# https://ollama.com/library
|
# https://ollama.com/library
|
||||||
LlmModel.OLLAMA_LLAMA3_3: ModelMetadata(
|
LlmModel.OLLAMA_LLAMA3_3: ModelMetadata("ollama", 8192, None),
|
||||||
"ollama", 8192, None, "Llama 3.3", "Ollama", "Meta", 1
|
LlmModel.OLLAMA_LLAMA3_2: ModelMetadata("ollama", 8192, None),
|
||||||
),
|
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata("ollama", 8192, None),
|
||||||
LlmModel.OLLAMA_LLAMA3_2: ModelMetadata(
|
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata("ollama", 8192, None),
|
||||||
"ollama", 8192, None, "Llama 3.2", "Ollama", "Meta", 1
|
LlmModel.OLLAMA_DOLPHIN: ModelMetadata("ollama", 32768, None),
|
||||||
),
|
|
||||||
LlmModel.OLLAMA_LLAMA3_8B: ModelMetadata(
|
|
||||||
"ollama", 8192, None, "Llama 3", "Ollama", "Meta", 1
|
|
||||||
),
|
|
||||||
LlmModel.OLLAMA_LLAMA3_405B: ModelMetadata(
|
|
||||||
"ollama", 8192, None, "Llama 3.1 405B", "Ollama", "Meta", 1
|
|
||||||
),
|
|
||||||
LlmModel.OLLAMA_DOLPHIN: ModelMetadata(
|
|
||||||
"ollama", 32768, None, "Dolphin Mistral Latest", "Ollama", "Mistral AI", 1
|
|
||||||
),
|
|
||||||
# https://openrouter.ai/models
|
# https://openrouter.ai/models
|
||||||
LlmModel.GEMINI_2_5_PRO: ModelMetadata(
|
LlmModel.GEMINI_2_5_PRO: ModelMetadata("open_router", 1050000, 8192),
|
||||||
"open_router",
|
LlmModel.GEMINI_3_PRO_PREVIEW: ModelMetadata("open_router", 1048576, 65535),
|
||||||
1050000,
|
LlmModel.GEMINI_2_5_FLASH: ModelMetadata("open_router", 1048576, 65535),
|
||||||
8192,
|
LlmModel.GEMINI_2_0_FLASH: ModelMetadata("open_router", 1048576, 8192),
|
||||||
"Gemini 2.5 Pro Preview 03.25",
|
|
||||||
"OpenRouter",
|
|
||||||
"Google",
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
LlmModel.GEMINI_3_PRO_PREVIEW: ModelMetadata(
|
|
||||||
"open_router", 1048576, 65535, "Gemini 3 Pro Preview", "OpenRouter", "Google", 2
|
|
||||||
),
|
|
||||||
LlmModel.GEMINI_2_5_FLASH: ModelMetadata(
|
|
||||||
"open_router", 1048576, 65535, "Gemini 2.5 Flash", "OpenRouter", "Google", 1
|
|
||||||
),
|
|
||||||
LlmModel.GEMINI_2_0_FLASH: ModelMetadata(
|
|
||||||
"open_router", 1048576, 8192, "Gemini 2.0 Flash 001", "OpenRouter", "Google", 1
|
|
||||||
),
|
|
||||||
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: ModelMetadata(
|
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: ModelMetadata(
|
||||||
"open_router",
|
"open_router", 1048576, 65535
|
||||||
1048576,
|
|
||||||
65535,
|
|
||||||
"Gemini 2.5 Flash Lite Preview 06.17",
|
|
||||||
"OpenRouter",
|
|
||||||
"Google",
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
LlmModel.GEMINI_2_0_FLASH_LITE: ModelMetadata(
|
|
||||||
"open_router",
|
|
||||||
1048576,
|
|
||||||
8192,
|
|
||||||
"Gemini 2.0 Flash Lite 001",
|
|
||||||
"OpenRouter",
|
|
||||||
"Google",
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
LlmModel.MISTRAL_NEMO: ModelMetadata(
|
|
||||||
"open_router", 128000, 4096, "Mistral Nemo", "OpenRouter", "Mistral AI", 1
|
|
||||||
),
|
|
||||||
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata(
|
|
||||||
"open_router", 128000, 4096, "Command R 08.2024", "OpenRouter", "Cohere", 1
|
|
||||||
),
|
|
||||||
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata(
|
|
||||||
"open_router", 128000, 4096, "Command R Plus 08.2024", "OpenRouter", "Cohere", 2
|
|
||||||
),
|
|
||||||
LlmModel.DEEPSEEK_CHAT: ModelMetadata(
|
|
||||||
"open_router", 64000, 2048, "DeepSeek Chat", "OpenRouter", "DeepSeek", 1
|
|
||||||
),
|
|
||||||
LlmModel.DEEPSEEK_R1_0528: ModelMetadata(
|
|
||||||
"open_router", 163840, 163840, "DeepSeek R1 0528", "OpenRouter", "DeepSeek", 1
|
|
||||||
),
|
|
||||||
LlmModel.PERPLEXITY_SONAR: ModelMetadata(
|
|
||||||
"open_router", 127000, 8000, "Sonar", "OpenRouter", "Perplexity", 1
|
|
||||||
),
|
|
||||||
LlmModel.PERPLEXITY_SONAR_PRO: ModelMetadata(
|
|
||||||
"open_router", 200000, 8000, "Sonar Pro", "OpenRouter", "Perplexity", 2
|
|
||||||
),
|
),
|
||||||
|
LlmModel.GEMINI_2_0_FLASH_LITE: ModelMetadata("open_router", 1048576, 8192),
|
||||||
|
LlmModel.MISTRAL_NEMO: ModelMetadata("open_router", 128000, 4096),
|
||||||
|
LlmModel.COHERE_COMMAND_R_08_2024: ModelMetadata("open_router", 128000, 4096),
|
||||||
|
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: ModelMetadata("open_router", 128000, 4096),
|
||||||
|
LlmModel.DEEPSEEK_CHAT: ModelMetadata("open_router", 64000, 2048),
|
||||||
|
LlmModel.DEEPSEEK_R1_0528: ModelMetadata("open_router", 163840, 163840),
|
||||||
|
LlmModel.PERPLEXITY_SONAR: ModelMetadata("open_router", 127000, 8000),
|
||||||
|
LlmModel.PERPLEXITY_SONAR_PRO: ModelMetadata("open_router", 200000, 8000),
|
||||||
LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: ModelMetadata(
|
LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: ModelMetadata(
|
||||||
"open_router",
|
"open_router",
|
||||||
128000,
|
128000,
|
||||||
16000,
|
16000,
|
||||||
"Sonar Deep Research",
|
|
||||||
"OpenRouter",
|
|
||||||
"Perplexity",
|
|
||||||
3,
|
|
||||||
),
|
),
|
||||||
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: ModelMetadata(
|
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_405B: ModelMetadata(
|
||||||
"open_router",
|
"open_router", 131000, 4096
|
||||||
131000,
|
|
||||||
4096,
|
|
||||||
"Hermes 3 Llama 3.1 405B",
|
|
||||||
"OpenRouter",
|
|
||||||
"Nous Research",
|
|
||||||
1,
|
|
||||||
),
|
),
|
||||||
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: ModelMetadata(
|
LlmModel.NOUSRESEARCH_HERMES_3_LLAMA_3_1_70B: ModelMetadata(
|
||||||
"open_router",
|
"open_router", 12288, 12288
|
||||||
12288,
|
|
||||||
12288,
|
|
||||||
"Hermes 3 Llama 3.1 70B",
|
|
||||||
"OpenRouter",
|
|
||||||
"Nous Research",
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
LlmModel.OPENAI_GPT_OSS_120B: ModelMetadata(
|
|
||||||
"open_router", 131072, 131072, "GPT-OSS 120B", "OpenRouter", "OpenAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.OPENAI_GPT_OSS_20B: ModelMetadata(
|
|
||||||
"open_router", 131072, 32768, "GPT-OSS 20B", "OpenRouter", "OpenAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata(
|
|
||||||
"open_router", 300000, 5120, "Nova Lite V1", "OpenRouter", "Amazon", 1
|
|
||||||
),
|
|
||||||
LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata(
|
|
||||||
"open_router", 128000, 5120, "Nova Micro V1", "OpenRouter", "Amazon", 1
|
|
||||||
),
|
|
||||||
LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata(
|
|
||||||
"open_router", 300000, 5120, "Nova Pro V1", "OpenRouter", "Amazon", 1
|
|
||||||
),
|
|
||||||
LlmModel.MICROSOFT_WIZARDLM_2_8X22B: ModelMetadata(
|
|
||||||
"open_router", 65536, 4096, "WizardLM 2 8x22B", "OpenRouter", "Microsoft", 1
|
|
||||||
),
|
|
||||||
LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata(
|
|
||||||
"open_router", 4096, 4096, "MythoMax L2 13B", "OpenRouter", "Gryphe", 1
|
|
||||||
),
|
|
||||||
LlmModel.META_LLAMA_4_SCOUT: ModelMetadata(
|
|
||||||
"open_router", 131072, 131072, "Llama 4 Scout", "OpenRouter", "Meta", 1
|
|
||||||
),
|
|
||||||
LlmModel.META_LLAMA_4_MAVERICK: ModelMetadata(
|
|
||||||
"open_router", 1048576, 1000000, "Llama 4 Maverick", "OpenRouter", "Meta", 1
|
|
||||||
),
|
|
||||||
LlmModel.GROK_4: ModelMetadata(
|
|
||||||
"open_router", 256000, 256000, "Grok 4", "OpenRouter", "xAI", 3
|
|
||||||
),
|
|
||||||
LlmModel.GROK_4_FAST: ModelMetadata(
|
|
||||||
"open_router", 2000000, 30000, "Grok 4 Fast", "OpenRouter", "xAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.GROK_4_1_FAST: ModelMetadata(
|
|
||||||
"open_router", 2000000, 30000, "Grok 4.1 Fast", "OpenRouter", "xAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.GROK_CODE_FAST_1: ModelMetadata(
|
|
||||||
"open_router", 256000, 10000, "Grok Code Fast 1", "OpenRouter", "xAI", 1
|
|
||||||
),
|
|
||||||
LlmModel.KIMI_K2: ModelMetadata(
|
|
||||||
"open_router", 131000, 131000, "Kimi K2", "OpenRouter", "Moonshot AI", 1
|
|
||||||
),
|
|
||||||
LlmModel.QWEN3_235B_A22B_THINKING: ModelMetadata(
|
|
||||||
"open_router",
|
|
||||||
262144,
|
|
||||||
262144,
|
|
||||||
"Qwen 3 235B A22B Thinking 2507",
|
|
||||||
"OpenRouter",
|
|
||||||
"Qwen",
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
LlmModel.QWEN3_CODER: ModelMetadata(
|
|
||||||
"open_router", 262144, 262144, "Qwen 3 Coder", "OpenRouter", "Qwen", 3
|
|
||||||
),
|
),
|
||||||
|
LlmModel.OPENAI_GPT_OSS_120B: ModelMetadata("open_router", 131072, 131072),
|
||||||
|
LlmModel.OPENAI_GPT_OSS_20B: ModelMetadata("open_router", 131072, 32768),
|
||||||
|
LlmModel.AMAZON_NOVA_LITE_V1: ModelMetadata("open_router", 300000, 5120),
|
||||||
|
LlmModel.AMAZON_NOVA_MICRO_V1: ModelMetadata("open_router", 128000, 5120),
|
||||||
|
LlmModel.AMAZON_NOVA_PRO_V1: ModelMetadata("open_router", 300000, 5120),
|
||||||
|
LlmModel.MICROSOFT_WIZARDLM_2_8X22B: ModelMetadata("open_router", 65536, 4096),
|
||||||
|
LlmModel.GRYPHE_MYTHOMAX_L2_13B: ModelMetadata("open_router", 4096, 4096),
|
||||||
|
LlmModel.META_LLAMA_4_SCOUT: ModelMetadata("open_router", 131072, 131072),
|
||||||
|
LlmModel.META_LLAMA_4_MAVERICK: ModelMetadata("open_router", 1048576, 1000000),
|
||||||
|
LlmModel.GROK_4: ModelMetadata("open_router", 256000, 256000),
|
||||||
|
LlmModel.GROK_4_FAST: ModelMetadata("open_router", 2000000, 30000),
|
||||||
|
LlmModel.GROK_4_1_FAST: ModelMetadata("open_router", 2000000, 30000),
|
||||||
|
LlmModel.GROK_CODE_FAST_1: ModelMetadata("open_router", 256000, 10000),
|
||||||
|
LlmModel.KIMI_K2: ModelMetadata("open_router", 131000, 131000),
|
||||||
|
LlmModel.QWEN3_235B_A22B_THINKING: ModelMetadata("open_router", 262144, 262144),
|
||||||
|
LlmModel.QWEN3_CODER: ModelMetadata("open_router", 262144, 262144),
|
||||||
# Llama API models
|
# Llama API models
|
||||||
LlmModel.LLAMA_API_LLAMA_4_SCOUT: ModelMetadata(
|
LlmModel.LLAMA_API_LLAMA_4_SCOUT: ModelMetadata("llama_api", 128000, 4028),
|
||||||
"llama_api",
|
LlmModel.LLAMA_API_LLAMA4_MAVERICK: ModelMetadata("llama_api", 128000, 4028),
|
||||||
128000,
|
LlmModel.LLAMA_API_LLAMA3_3_8B: ModelMetadata("llama_api", 128000, 4028),
|
||||||
4028,
|
LlmModel.LLAMA_API_LLAMA3_3_70B: ModelMetadata("llama_api", 128000, 4028),
|
||||||
"Llama 4 Scout 17B 16E Instruct FP8",
|
|
||||||
"Llama API",
|
|
||||||
"Meta",
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
LlmModel.LLAMA_API_LLAMA4_MAVERICK: ModelMetadata(
|
|
||||||
"llama_api",
|
|
||||||
128000,
|
|
||||||
4028,
|
|
||||||
"Llama 4 Maverick 17B 128E Instruct FP8",
|
|
||||||
"Llama API",
|
|
||||||
"Meta",
|
|
||||||
1,
|
|
||||||
),
|
|
||||||
LlmModel.LLAMA_API_LLAMA3_3_8B: ModelMetadata(
|
|
||||||
"llama_api", 128000, 4028, "Llama 3.3 8B Instruct", "Llama API", "Meta", 1
|
|
||||||
),
|
|
||||||
LlmModel.LLAMA_API_LLAMA3_3_70B: ModelMetadata(
|
|
||||||
"llama_api", 128000, 4028, "Llama 3.3 70B Instruct", "Llama API", "Meta", 1
|
|
||||||
),
|
|
||||||
# v0 by Vercel models
|
# v0 by Vercel models
|
||||||
LlmModel.V0_1_5_MD: ModelMetadata("v0", 128000, 64000, "v0 1.5 MD", "V0", "V0", 1),
|
LlmModel.V0_1_5_MD: ModelMetadata("v0", 128000, 64000),
|
||||||
LlmModel.V0_1_5_LG: ModelMetadata("v0", 512000, 64000, "v0 1.5 LG", "V0", "V0", 1),
|
LlmModel.V0_1_5_LG: ModelMetadata("v0", 512000, 64000),
|
||||||
LlmModel.V0_1_0_MD: ModelMetadata("v0", 128000, 64000, "v0 1.0 MD", "V0", "V0", 1),
|
LlmModel.V0_1_0_MD: ModelMetadata("v0", 128000, 64000),
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_LLM_MODEL = LlmModel.GPT5_2
|
DEFAULT_LLM_MODEL = LlmModel.GPT5_2
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ async def test_smart_decision_maker_tracks_llm_stats():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
|
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -409,7 +409,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
|
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -535,7 +535,7 @@ async def test_smart_decision_maker_parameter_validation():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -658,7 +658,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -730,7 +730,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -786,7 +786,7 @@ async def test_smart_decision_maker_raw_response_conversion():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
@@ -905,7 +905,7 @@ async def test_smart_decision_maker_agent_mode():
|
|||||||
# Create a mock execution context
|
# Create a mock execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(
|
mock_execution_context = ExecutionContext(
|
||||||
human_in_the_loop_safe_mode=False,
|
safe_mode=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a mock execution processor for agent mode tests
|
# Create a mock execution processor for agent mode tests
|
||||||
@@ -1027,7 +1027,7 @@ async def test_smart_decision_maker_traditional_mode_default():
|
|||||||
|
|
||||||
# Create execution context
|
# Create execution context
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
|
|
||||||
# Create a mock execution processor for tests
|
# Create a mock execution processor for tests
|
||||||
|
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ async def test_output_yielding_with_dynamic_fields():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
from backend.data.execution import ExecutionContext
|
from backend.data.execution import ExecutionContext
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(human_in_the_loop_safe_mode=False)
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
mock_execution_processor = MagicMock()
|
mock_execution_processor = MagicMock()
|
||||||
|
|
||||||
async for output_name, output_value in block.run(
|
async for output_name, output_value in block.run(
|
||||||
@@ -609,9 +609,7 @@ async def test_validation_errors_dont_pollute_conversation():
|
|||||||
outputs = {}
|
outputs = {}
|
||||||
from backend.data.execution import ExecutionContext
|
from backend.data.execution import ExecutionContext
|
||||||
|
|
||||||
mock_execution_context = ExecutionContext(
|
mock_execution_context = ExecutionContext(safe_mode=False)
|
||||||
human_in_the_loop_safe_mode=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a proper mock execution processor for agent mode
|
# Create a proper mock execution processor for agent mode
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|||||||
@@ -474,7 +474,7 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
|||||||
self.block_type = block_type
|
self.block_type = block_type
|
||||||
self.webhook_config = webhook_config
|
self.webhook_config = webhook_config
|
||||||
self.execution_stats: NodeExecutionStats = NodeExecutionStats()
|
self.execution_stats: NodeExecutionStats = NodeExecutionStats()
|
||||||
self.is_sensitive_action: bool = False
|
self.requires_human_review: bool = False
|
||||||
|
|
||||||
if self.webhook_config:
|
if self.webhook_config:
|
||||||
if isinstance(self.webhook_config, BlockWebhookConfig):
|
if isinstance(self.webhook_config, BlockWebhookConfig):
|
||||||
@@ -637,9 +637,8 @@ class Block(ABC, Generic[BlockSchemaInputType, BlockSchemaOutputType]):
|
|||||||
- should_pause: True if execution should be paused for review
|
- should_pause: True if execution should be paused for review
|
||||||
- input_data_to_use: The input data to use (may be modified by reviewer)
|
- input_data_to_use: The input data to use (may be modified by reviewer)
|
||||||
"""
|
"""
|
||||||
if not (
|
# Skip review if not required or safe mode is disabled
|
||||||
self.is_sensitive_action and execution_context.sensitive_action_safe_mode
|
if not self.requires_human_review or not execution_context.safe_mode:
|
||||||
):
|
|
||||||
return False, input_data
|
return False, input_data
|
||||||
|
|
||||||
from backend.blocks.helpers.review import HITLReviewHelper
|
from backend.blocks.helpers.review import HITLReviewHelper
|
||||||
|
|||||||
@@ -99,15 +99,10 @@ MODEL_COST: dict[LlmModel, int] = {
|
|||||||
LlmModel.OPENAI_GPT_OSS_20B: 1,
|
LlmModel.OPENAI_GPT_OSS_20B: 1,
|
||||||
LlmModel.GEMINI_2_5_PRO: 4,
|
LlmModel.GEMINI_2_5_PRO: 4,
|
||||||
LlmModel.GEMINI_3_PRO_PREVIEW: 5,
|
LlmModel.GEMINI_3_PRO_PREVIEW: 5,
|
||||||
LlmModel.GEMINI_2_5_FLASH: 1,
|
|
||||||
LlmModel.GEMINI_2_0_FLASH: 1,
|
|
||||||
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: 1,
|
|
||||||
LlmModel.GEMINI_2_0_FLASH_LITE: 1,
|
|
||||||
LlmModel.MISTRAL_NEMO: 1,
|
LlmModel.MISTRAL_NEMO: 1,
|
||||||
LlmModel.COHERE_COMMAND_R_08_2024: 1,
|
LlmModel.COHERE_COMMAND_R_08_2024: 1,
|
||||||
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: 3,
|
LlmModel.COHERE_COMMAND_R_PLUS_08_2024: 3,
|
||||||
LlmModel.DEEPSEEK_CHAT: 2,
|
LlmModel.DEEPSEEK_CHAT: 2,
|
||||||
LlmModel.DEEPSEEK_R1_0528: 1,
|
|
||||||
LlmModel.PERPLEXITY_SONAR: 1,
|
LlmModel.PERPLEXITY_SONAR: 1,
|
||||||
LlmModel.PERPLEXITY_SONAR_PRO: 5,
|
LlmModel.PERPLEXITY_SONAR_PRO: 5,
|
||||||
LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: 10,
|
LlmModel.PERPLEXITY_SONAR_DEEP_RESEARCH: 10,
|
||||||
@@ -131,6 +126,11 @@ MODEL_COST: dict[LlmModel, int] = {
|
|||||||
LlmModel.KIMI_K2: 1,
|
LlmModel.KIMI_K2: 1,
|
||||||
LlmModel.QWEN3_235B_A22B_THINKING: 1,
|
LlmModel.QWEN3_235B_A22B_THINKING: 1,
|
||||||
LlmModel.QWEN3_CODER: 9,
|
LlmModel.QWEN3_CODER: 9,
|
||||||
|
LlmModel.GEMINI_2_5_FLASH: 1,
|
||||||
|
LlmModel.GEMINI_2_0_FLASH: 1,
|
||||||
|
LlmModel.GEMINI_2_5_FLASH_LITE_PREVIEW: 1,
|
||||||
|
LlmModel.GEMINI_2_0_FLASH_LITE: 1,
|
||||||
|
LlmModel.DEEPSEEK_R1_0528: 1,
|
||||||
# v0 by Vercel models
|
# v0 by Vercel models
|
||||||
LlmModel.V0_1_5_MD: 1,
|
LlmModel.V0_1_5_MD: 1,
|
||||||
LlmModel.V0_1_5_LG: 2,
|
LlmModel.V0_1_5_LG: 2,
|
||||||
|
|||||||
@@ -38,6 +38,20 @@ POOL_TIMEOUT = os.getenv("DB_POOL_TIMEOUT")
|
|||||||
if POOL_TIMEOUT:
|
if POOL_TIMEOUT:
|
||||||
DATABASE_URL = add_param(DATABASE_URL, "pool_timeout", POOL_TIMEOUT)
|
DATABASE_URL = add_param(DATABASE_URL, "pool_timeout", POOL_TIMEOUT)
|
||||||
|
|
||||||
|
# Add public schema to search_path for pgvector type access
|
||||||
|
# The vector extension is in public schema, but search_path is determined by schema parameter
|
||||||
|
# Extract the schema from DATABASE_URL or default to 'public' (matching get_database_schema())
|
||||||
|
parsed_url = urlparse(DATABASE_URL)
|
||||||
|
url_params = dict(parse_qsl(parsed_url.query))
|
||||||
|
db_schema = url_params.get("schema", "public")
|
||||||
|
# Build search_path, avoiding duplicates if db_schema is already 'public'
|
||||||
|
search_path_schemas = list(
|
||||||
|
dict.fromkeys([db_schema, "public"])
|
||||||
|
) # Preserves order, removes duplicates
|
||||||
|
search_path = ",".join(search_path_schemas)
|
||||||
|
# This allows using ::vector without schema qualification
|
||||||
|
DATABASE_URL = add_param(DATABASE_URL, "options", f"-c search_path={search_path}")
|
||||||
|
|
||||||
HTTP_TIMEOUT = int(POOL_TIMEOUT) if POOL_TIMEOUT else None
|
HTTP_TIMEOUT = int(POOL_TIMEOUT) if POOL_TIMEOUT else None
|
||||||
|
|
||||||
prisma = Prisma(
|
prisma = Prisma(
|
||||||
@@ -113,48 +127,38 @@ async def _raw_with_schema(
|
|||||||
*args,
|
*args,
|
||||||
execute: bool = False,
|
execute: bool = False,
|
||||||
client: Prisma | None = None,
|
client: Prisma | None = None,
|
||||||
|
set_public_search_path: bool = False,
|
||||||
) -> list[dict] | int:
|
) -> list[dict] | int:
|
||||||
"""Internal: Execute raw SQL with proper schema handling.
|
"""Internal: Execute raw SQL with proper schema handling.
|
||||||
|
|
||||||
Use query_raw_with_schema() or execute_raw_with_schema() instead.
|
Use query_raw_with_schema() or execute_raw_with_schema() instead.
|
||||||
|
|
||||||
Supports placeholders:
|
|
||||||
- {schema_prefix}: Table/type prefix (e.g., "platform".)
|
|
||||||
- {schema}: Raw schema name for application tables (e.g., platform)
|
|
||||||
|
|
||||||
Note on pgvector types:
|
|
||||||
Use unqualified ::vector and <=> operator in queries. PostgreSQL resolves
|
|
||||||
these via search_path, which includes the schema where pgvector is installed
|
|
||||||
on all environments (local, CI, dev).
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_template: SQL query with {schema_prefix} and/or {schema} placeholders
|
query_template: SQL query with {schema_prefix} placeholder
|
||||||
*args: Query parameters
|
*args: Query parameters
|
||||||
execute: If False, executes SELECT query. If True, executes INSERT/UPDATE/DELETE.
|
execute: If False, executes SELECT query. If True, executes INSERT/UPDATE/DELETE.
|
||||||
client: Optional Prisma client for transactions (only used when execute=True).
|
client: Optional Prisma client for transactions (only used when execute=True).
|
||||||
|
set_public_search_path: If True, sets search_path to include public schema.
|
||||||
|
Needed for pgvector types and other public schema objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- list[dict] if execute=False (query results)
|
- list[dict] if execute=False (query results)
|
||||||
- int if execute=True (number of affected rows)
|
- int if execute=True (number of affected rows)
|
||||||
|
|
||||||
Example with vector type:
|
|
||||||
await execute_raw_with_schema(
|
|
||||||
'INSERT INTO {schema_prefix}"Embedding" (vec) VALUES ($1::vector)',
|
|
||||||
embedding_data
|
|
||||||
)
|
|
||||||
"""
|
"""
|
||||||
schema = get_database_schema()
|
schema = get_database_schema()
|
||||||
schema_prefix = f'"{schema}".' if schema != "public" else ""
|
schema_prefix = f'"{schema}".' if schema != "public" else ""
|
||||||
|
formatted_query = query_template.format(schema_prefix=schema_prefix)
|
||||||
formatted_query = query_template.format(
|
|
||||||
schema_prefix=schema_prefix,
|
|
||||||
schema=schema,
|
|
||||||
)
|
|
||||||
|
|
||||||
import prisma as prisma_module
|
import prisma as prisma_module
|
||||||
|
|
||||||
db_client = client if client else prisma_module.get_client()
|
db_client = client if client else prisma_module.get_client()
|
||||||
|
|
||||||
|
# Set search_path to include public schema if requested
|
||||||
|
# Prisma doesn't support the 'options' connection parameter, so we set it per-session
|
||||||
|
# This is idempotent and safe to call multiple times
|
||||||
|
if set_public_search_path:
|
||||||
|
await db_client.execute_raw(f"SET search_path = {schema}, public") # type: ignore
|
||||||
|
|
||||||
if execute:
|
if execute:
|
||||||
result = await db_client.execute_raw(formatted_query, *args) # type: ignore
|
result = await db_client.execute_raw(formatted_query, *args) # type: ignore
|
||||||
else:
|
else:
|
||||||
@@ -163,12 +167,16 @@ async def _raw_with_schema(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def query_raw_with_schema(query_template: str, *args) -> list[dict]:
|
async def query_raw_with_schema(
|
||||||
|
query_template: str, *args, set_public_search_path: bool = False
|
||||||
|
) -> list[dict]:
|
||||||
"""Execute raw SQL SELECT query with proper schema handling.
|
"""Execute raw SQL SELECT query with proper schema handling.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_template: SQL query with {schema_prefix} and/or {schema} placeholders
|
query_template: SQL query with {schema_prefix} placeholder
|
||||||
*args: Query parameters
|
*args: Query parameters
|
||||||
|
set_public_search_path: If True, sets search_path to include public schema.
|
||||||
|
Needed for pgvector types and other public schema objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of result rows as dictionaries
|
List of result rows as dictionaries
|
||||||
@@ -179,20 +187,23 @@ async def query_raw_with_schema(query_template: str, *args) -> list[dict]:
|
|||||||
user_id
|
user_id
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
return await _raw_with_schema(query_template, *args, execute=False) # type: ignore
|
return await _raw_with_schema(query_template, *args, execute=False, set_public_search_path=set_public_search_path) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def execute_raw_with_schema(
|
async def execute_raw_with_schema(
|
||||||
query_template: str,
|
query_template: str,
|
||||||
*args,
|
*args,
|
||||||
client: Prisma | None = None,
|
client: Prisma | None = None,
|
||||||
|
set_public_search_path: bool = False,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Execute raw SQL command (INSERT/UPDATE/DELETE) with proper schema handling.
|
"""Execute raw SQL command (INSERT/UPDATE/DELETE) with proper schema handling.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_template: SQL query with {schema_prefix} and/or {schema} placeholders
|
query_template: SQL query with {schema_prefix} placeholder
|
||||||
*args: Query parameters
|
*args: Query parameters
|
||||||
client: Optional Prisma client for transactions
|
client: Optional Prisma client for transactions
|
||||||
|
set_public_search_path: If True, sets search_path to include public schema.
|
||||||
|
Needed for pgvector types and other public schema objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of affected rows
|
Number of affected rows
|
||||||
@@ -204,7 +215,7 @@ async def execute_raw_with_schema(
|
|||||||
client=tx # Optional transaction client
|
client=tx # Optional transaction client
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
return await _raw_with_schema(query_template, *args, execute=True, client=client) # type: ignore
|
return await _raw_with_schema(query_template, *args, execute=True, client=client, set_public_search_path=set_public_search_path) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class BaseDbModel(BaseModel):
|
class BaseDbModel(BaseModel):
|
||||||
|
|||||||
@@ -103,18 +103,8 @@ class RedisEventBus(BaseRedisEventBus[M], ABC):
|
|||||||
return redis.get_redis()
|
return redis.get_redis()
|
||||||
|
|
||||||
def publish_event(self, event: M, channel_key: str):
|
def publish_event(self, event: M, channel_key: str):
|
||||||
"""
|
message, full_channel_name = self._serialize_message(event, channel_key)
|
||||||
Publish an event to Redis. Gracefully handles connection failures
|
self.connection.publish(full_channel_name, message)
|
||||||
by logging the error instead of raising exceptions.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
message, full_channel_name = self._serialize_message(event, channel_key)
|
|
||||||
self.connection.publish(full_channel_name, message)
|
|
||||||
except Exception:
|
|
||||||
logger.exception(
|
|
||||||
f"Failed to publish event to Redis channel {channel_key}. "
|
|
||||||
"Event bus operation will continue without Redis connectivity."
|
|
||||||
)
|
|
||||||
|
|
||||||
def listen_events(self, channel_key: str) -> Generator[M, None, None]:
|
def listen_events(self, channel_key: str) -> Generator[M, None, None]:
|
||||||
pubsub, full_channel_name = self._get_pubsub_channel(
|
pubsub, full_channel_name = self._get_pubsub_channel(
|
||||||
@@ -138,19 +128,9 @@ class AsyncRedisEventBus(BaseRedisEventBus[M], ABC):
|
|||||||
return await redis.get_redis_async()
|
return await redis.get_redis_async()
|
||||||
|
|
||||||
async def publish_event(self, event: M, channel_key: str):
|
async def publish_event(self, event: M, channel_key: str):
|
||||||
"""
|
message, full_channel_name = self._serialize_message(event, channel_key)
|
||||||
Publish an event to Redis. Gracefully handles connection failures
|
connection = await self.connection
|
||||||
by logging the error instead of raising exceptions.
|
await connection.publish(full_channel_name, message)
|
||||||
"""
|
|
||||||
try:
|
|
||||||
message, full_channel_name = self._serialize_message(event, channel_key)
|
|
||||||
connection = await self.connection
|
|
||||||
await connection.publish(full_channel_name, message)
|
|
||||||
except Exception:
|
|
||||||
logger.exception(
|
|
||||||
f"Failed to publish event to Redis channel {channel_key}. "
|
|
||||||
"Event bus operation will continue without Redis connectivity."
|
|
||||||
)
|
|
||||||
|
|
||||||
async def listen_events(self, channel_key: str) -> AsyncGenerator[M, None]:
|
async def listen_events(self, channel_key: str) -> AsyncGenerator[M, None]:
|
||||||
pubsub, full_channel_name = self._get_pubsub_channel(
|
pubsub, full_channel_name = self._get_pubsub_channel(
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
"""
|
|
||||||
Tests for event_bus graceful degradation when Redis is unavailable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from backend.data.event_bus import AsyncRedisEventBus
|
|
||||||
|
|
||||||
|
|
||||||
class TestEvent(BaseModel):
|
|
||||||
"""Test event model."""
|
|
||||||
|
|
||||||
message: str
|
|
||||||
|
|
||||||
|
|
||||||
class TestNotificationBus(AsyncRedisEventBus[TestEvent]):
|
|
||||||
"""Test implementation of AsyncRedisEventBus."""
|
|
||||||
|
|
||||||
Model = TestEvent
|
|
||||||
|
|
||||||
@property
|
|
||||||
def event_bus_name(self) -> str:
|
|
||||||
return "test_event_bus"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_publish_event_handles_connection_failure_gracefully():
|
|
||||||
"""Test that publish_event logs exception instead of raising when Redis is unavailable."""
|
|
||||||
bus = TestNotificationBus()
|
|
||||||
event = TestEvent(message="test message")
|
|
||||||
|
|
||||||
# Mock get_redis_async to raise connection error
|
|
||||||
with patch(
|
|
||||||
"backend.data.event_bus.redis.get_redis_async",
|
|
||||||
side_effect=ConnectionError("Authentication required."),
|
|
||||||
):
|
|
||||||
# Should not raise exception
|
|
||||||
await bus.publish_event(event, "test_channel")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_publish_event_works_with_redis_available():
|
|
||||||
"""Test that publish_event works normally when Redis is available."""
|
|
||||||
bus = TestNotificationBus()
|
|
||||||
event = TestEvent(message="test message")
|
|
||||||
|
|
||||||
# Mock successful Redis connection
|
|
||||||
mock_redis = AsyncMock()
|
|
||||||
mock_redis.publish = AsyncMock()
|
|
||||||
|
|
||||||
with patch("backend.data.event_bus.redis.get_redis_async", return_value=mock_redis):
|
|
||||||
await bus.publish_event(event, "test_channel")
|
|
||||||
mock_redis.publish.assert_called_once()
|
|
||||||
@@ -81,10 +81,7 @@ class ExecutionContext(BaseModel):
|
|||||||
This includes information needed by blocks, sub-graphs, and execution management.
|
This includes information needed by blocks, sub-graphs, and execution management.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model_config = {"extra": "ignore"}
|
safe_mode: bool = True
|
||||||
|
|
||||||
human_in_the_loop_safe_mode: bool = True
|
|
||||||
sensitive_action_safe_mode: bool = False
|
|
||||||
user_timezone: str = "UTC"
|
user_timezone: str = "UTC"
|
||||||
root_execution_id: Optional[str] = None
|
root_execution_id: Optional[str] = None
|
||||||
parent_execution_id: Optional[str] = None
|
parent_execution_id: Optional[str] = None
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, cast
|
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
|
||||||
|
|
||||||
from prisma.enums import SubmissionStatus
|
from prisma.enums import SubmissionStatus
|
||||||
from prisma.models import (
|
from prisma.models import (
|
||||||
@@ -20,7 +20,7 @@ from prisma.types import (
|
|||||||
AgentNodeLinkCreateInput,
|
AgentNodeLinkCreateInput,
|
||||||
StoreListingVersionWhereInput,
|
StoreListingVersionWhereInput,
|
||||||
)
|
)
|
||||||
from pydantic import BaseModel, BeforeValidator, Field, create_model
|
from pydantic import BaseModel, Field, create_model
|
||||||
from pydantic.fields import computed_field
|
from pydantic.fields import computed_field
|
||||||
|
|
||||||
from backend.blocks.agent import AgentExecutorBlock
|
from backend.blocks.agent import AgentExecutorBlock
|
||||||
@@ -62,31 +62,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class GraphSettings(BaseModel):
|
class GraphSettings(BaseModel):
|
||||||
# Use Annotated with BeforeValidator to coerce None to default values.
|
human_in_the_loop_safe_mode: bool | None = None
|
||||||
# This handles cases where the database has null values for these fields.
|
|
||||||
model_config = {"extra": "ignore"}
|
|
||||||
|
|
||||||
human_in_the_loop_safe_mode: Annotated[
|
|
||||||
bool, BeforeValidator(lambda v: v if v is not None else True)
|
|
||||||
] = True
|
|
||||||
sensitive_action_safe_mode: Annotated[
|
|
||||||
bool, BeforeValidator(lambda v: v if v is not None else False)
|
|
||||||
] = False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_graph(
|
|
||||||
cls,
|
|
||||||
graph: "GraphModel",
|
|
||||||
hitl_safe_mode: bool | None = None,
|
|
||||||
sensitive_action_safe_mode: bool = False,
|
|
||||||
) -> "GraphSettings":
|
|
||||||
# Default to True if not explicitly set
|
|
||||||
if hitl_safe_mode is None:
|
|
||||||
hitl_safe_mode = True
|
|
||||||
return cls(
|
|
||||||
human_in_the_loop_safe_mode=hitl_safe_mode,
|
|
||||||
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Link(BaseDbModel):
|
class Link(BaseDbModel):
|
||||||
@@ -268,14 +244,10 @@ class BaseGraph(BaseDbModel):
|
|||||||
return any(
|
return any(
|
||||||
node.block_id
|
node.block_id
|
||||||
for node in self.nodes
|
for node in self.nodes
|
||||||
if node.block.block_type == BlockType.HUMAN_IN_THE_LOOP
|
if (
|
||||||
)
|
node.block.block_type == BlockType.HUMAN_IN_THE_LOOP
|
||||||
|
or node.block.requires_human_review
|
||||||
@computed_field
|
)
|
||||||
@property
|
|
||||||
def has_sensitive_action(self) -> bool:
|
|
||||||
return any(
|
|
||||||
node.block_id for node in self.nodes if node.block.is_sensitive_action
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ def ensure_embeddings_coverage():
|
|||||||
|
|
||||||
# Process in batches until no more missing embeddings
|
# Process in batches until no more missing embeddings
|
||||||
while True:
|
while True:
|
||||||
result = db_client.backfill_missing_embeddings(batch_size=100)
|
result = db_client.backfill_missing_embeddings(batch_size=10)
|
||||||
|
|
||||||
total_processed += result["processed"]
|
total_processed += result["processed"]
|
||||||
total_success += result["success"]
|
total_success += result["success"]
|
||||||
|
|||||||
@@ -873,8 +873,11 @@ async def add_graph_execution(
|
|||||||
settings = await gdb.get_graph_settings(user_id=user_id, graph_id=graph_id)
|
settings = await gdb.get_graph_settings(user_id=user_id, graph_id=graph_id)
|
||||||
|
|
||||||
execution_context = ExecutionContext(
|
execution_context = ExecutionContext(
|
||||||
human_in_the_loop_safe_mode=settings.human_in_the_loop_safe_mode,
|
safe_mode=(
|
||||||
sensitive_action_safe_mode=settings.sensitive_action_safe_mode,
|
settings.human_in_the_loop_safe_mode
|
||||||
|
if settings.human_in_the_loop_safe_mode is not None
|
||||||
|
else True
|
||||||
|
),
|
||||||
user_timezone=(
|
user_timezone=(
|
||||||
user.timezone if user.timezone != USER_TIMEZONE_NOT_SET else "UTC"
|
user.timezone if user.timezone != USER_TIMEZONE_NOT_SET else "UTC"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -386,7 +386,6 @@ async def test_add_graph_execution_is_repeatable(mocker: MockerFixture):
|
|||||||
mock_user.timezone = "UTC"
|
mock_user.timezone = "UTC"
|
||||||
mock_settings = mocker.MagicMock()
|
mock_settings = mocker.MagicMock()
|
||||||
mock_settings.human_in_the_loop_safe_mode = True
|
mock_settings.human_in_the_loop_safe_mode = True
|
||||||
mock_settings.sensitive_action_safe_mode = False
|
|
||||||
|
|
||||||
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
||||||
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
||||||
@@ -652,7 +651,6 @@ async def test_add_graph_execution_with_nodes_to_skip(mocker: MockerFixture):
|
|||||||
mock_user.timezone = "UTC"
|
mock_user.timezone = "UTC"
|
||||||
mock_settings = mocker.MagicMock()
|
mock_settings = mocker.MagicMock()
|
||||||
mock_settings.human_in_the_loop_safe_mode = True
|
mock_settings.human_in_the_loop_safe_mode = True
|
||||||
mock_settings.sensitive_action_safe_mode = False
|
|
||||||
|
|
||||||
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
mock_udb.get_user_by_id = mocker.AsyncMock(return_value=mock_user)
|
||||||
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
mock_gdb.get_graph_settings = mocker.AsyncMock(return_value=mock_settings)
|
||||||
|
|||||||
@@ -1,37 +1,11 @@
|
|||||||
-- CreateExtension
|
-- CreateExtension
|
||||||
-- Supabase: pgvector must be enabled via Dashboard → Database → Extensions first
|
-- Supabase: pgvector must be enabled via Dashboard → Database → Extensions first
|
||||||
-- Ensures vector extension is in the current schema (from DATABASE_URL ?schema= param)
|
-- Create in public schema so vector type is available across all schemas
|
||||||
-- If it exists in a different schema (e.g., public), we drop and recreate it in the current schema
|
|
||||||
-- This ensures vector type is in the same schema as tables, making ::vector work without explicit qualification
|
|
||||||
DO $$
|
DO $$
|
||||||
DECLARE
|
|
||||||
current_schema_name text;
|
|
||||||
vector_schema text;
|
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Get the current schema from search_path
|
CREATE EXTENSION IF NOT EXISTS "vector" WITH SCHEMA "public";
|
||||||
SELECT current_schema() INTO current_schema_name;
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'vector extension not available or already exists, skipping';
|
||||||
-- Check if vector extension exists and which schema it's in
|
|
||||||
SELECT n.nspname INTO vector_schema
|
|
||||||
FROM pg_extension e
|
|
||||||
JOIN pg_namespace n ON e.extnamespace = n.oid
|
|
||||||
WHERE e.extname = 'vector';
|
|
||||||
|
|
||||||
-- Handle removal if in wrong schema
|
|
||||||
IF vector_schema IS NOT NULL AND vector_schema != current_schema_name THEN
|
|
||||||
BEGIN
|
|
||||||
-- Vector exists in a different schema, drop it first
|
|
||||||
RAISE WARNING 'pgvector found in schema "%" but need it in "%". Dropping and reinstalling...',
|
|
||||||
vector_schema, current_schema_name;
|
|
||||||
EXECUTE 'DROP EXTENSION IF EXISTS vector CASCADE';
|
|
||||||
EXCEPTION WHEN OTHERS THEN
|
|
||||||
RAISE EXCEPTION 'Failed to drop pgvector from schema "%": %. You may need to drop it manually.',
|
|
||||||
vector_schema, SQLERRM;
|
|
||||||
END;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Create extension in current schema (let it fail naturally if not available)
|
|
||||||
EXECUTE format('CREATE EXTENSION IF NOT EXISTS vector SCHEMA %I', current_schema_name);
|
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
-- CreateEnum
|
-- CreateEnum
|
||||||
@@ -45,7 +19,7 @@ CREATE TABLE "UnifiedContentEmbedding" (
|
|||||||
"contentType" "ContentType" NOT NULL,
|
"contentType" "ContentType" NOT NULL,
|
||||||
"contentId" TEXT NOT NULL,
|
"contentId" TEXT NOT NULL,
|
||||||
"userId" TEXT,
|
"userId" TEXT,
|
||||||
"embedding" vector(1536) NOT NULL,
|
"embedding" public.vector(1536) NOT NULL,
|
||||||
"searchableText" TEXT NOT NULL,
|
"searchableText" TEXT NOT NULL,
|
||||||
"metadata" JSONB NOT NULL DEFAULT '{}',
|
"metadata" JSONB NOT NULL DEFAULT '{}',
|
||||||
|
|
||||||
@@ -71,4 +45,4 @@ CREATE UNIQUE INDEX "UnifiedContentEmbedding_contentType_contentId_userId_key" O
|
|||||||
-- Uses cosine distance operator (<=>), which matches the query in hybrid_search.py
|
-- Uses cosine distance operator (<=>), which matches the query in hybrid_search.py
|
||||||
-- Note: Drop first in case Prisma created a btree index (Prisma doesn't support HNSW)
|
-- Note: Drop first in case Prisma created a btree index (Prisma doesn't support HNSW)
|
||||||
DROP INDEX IF EXISTS "UnifiedContentEmbedding_embedding_idx";
|
DROP INDEX IF EXISTS "UnifiedContentEmbedding_embedding_idx";
|
||||||
CREATE INDEX "UnifiedContentEmbedding_embedding_idx" ON "UnifiedContentEmbedding" USING hnsw ("embedding" vector_cosine_ops);
|
CREATE INDEX "UnifiedContentEmbedding_embedding_idx" ON "UnifiedContentEmbedding" USING hnsw ("embedding" public.vector_cosine_ops);
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
-- Acknowledge Supabase-managed extensions to prevent drift warnings
|
||||||
|
-- These extensions are pre-installed by Supabase in specific schemas
|
||||||
|
-- This migration ensures they exist where available (Supabase) or skips gracefully (CI)
|
||||||
|
|
||||||
|
-- Create schemas (safe in both CI and Supabase)
|
||||||
|
CREATE SCHEMA IF NOT EXISTS "extensions";
|
||||||
|
|
||||||
|
-- Extensions that exist in both CI and Supabase
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'pgcrypto extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'uuid-ossp extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Supabase-specific extensions (skip gracefully in CI)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'pg_stat_statements extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'pg_net extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'pgjwt extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE SCHEMA IF NOT EXISTS "graphql";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'pg_graphql extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE SCHEMA IF NOT EXISTS "pgsodium";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'pgsodium extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE SCHEMA IF NOT EXISTS "vault";
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'supabase_vault extension not available, skipping';
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
|
||||||
|
-- Return to platform
|
||||||
|
CREATE SCHEMA IF NOT EXISTS "platform";
|
||||||
@@ -366,12 +366,12 @@ def generate_block_markdown(
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# What it is (full description)
|
# What it is (full description)
|
||||||
lines.append("### What it is")
|
lines.append(f"### What it is")
|
||||||
lines.append(block.description or "No description available.")
|
lines.append(block.description or "No description available.")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# How it works (manual section)
|
# How it works (manual section)
|
||||||
lines.append("### How it works")
|
lines.append(f"### How it works")
|
||||||
how_it_works = manual_content.get(
|
how_it_works = manual_content.get(
|
||||||
"how_it_works", "_Add technical explanation here._"
|
"how_it_works", "_Add technical explanation here._"
|
||||||
)
|
)
|
||||||
@@ -383,7 +383,7 @@ def generate_block_markdown(
|
|||||||
# Inputs table (auto-generated)
|
# Inputs table (auto-generated)
|
||||||
visible_inputs = [f for f in block.inputs if not f.hidden]
|
visible_inputs = [f for f in block.inputs if not f.hidden]
|
||||||
if visible_inputs:
|
if visible_inputs:
|
||||||
lines.append("### Inputs")
|
lines.append(f"### Inputs")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("| Input | Description | Type | Required |")
|
lines.append("| Input | Description | Type | Required |")
|
||||||
lines.append("|-------|-------------|------|----------|")
|
lines.append("|-------|-------------|------|----------|")
|
||||||
@@ -400,7 +400,7 @@ def generate_block_markdown(
|
|||||||
# Outputs table (auto-generated)
|
# Outputs table (auto-generated)
|
||||||
visible_outputs = [f for f in block.outputs if not f.hidden]
|
visible_outputs = [f for f in block.outputs if not f.hidden]
|
||||||
if visible_outputs:
|
if visible_outputs:
|
||||||
lines.append("### Outputs")
|
lines.append(f"### Outputs")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("| Output | Description | Type |")
|
lines.append("| Output | Description | Type |")
|
||||||
lines.append("|--------|-------------|------|")
|
lines.append("|--------|-------------|------|")
|
||||||
@@ -414,7 +414,7 @@ def generate_block_markdown(
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Possible use case (manual section)
|
# Possible use case (manual section)
|
||||||
lines.append("### Possible use case")
|
lines.append(f"### Possible use case")
|
||||||
use_case = manual_content.get("use_case", "_Add practical use case examples here._")
|
use_case = manual_content.get("use_case", "_Add practical use case examples here._")
|
||||||
lines.append("<!-- MANUAL: use_case -->")
|
lines.append("<!-- MANUAL: use_case -->")
|
||||||
lines.append(use_case)
|
lines.append(use_case)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"forked_from_version": null,
|
"forked_from_version": null,
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
"has_human_in_the_loop": false,
|
"has_human_in_the_loop": false,
|
||||||
"has_sensitive_action": false,
|
|
||||||
"id": "graph-123",
|
"id": "graph-123",
|
||||||
"input_schema": {
|
"input_schema": {
|
||||||
"properties": {},
|
"properties": {},
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"forked_from_version": null,
|
"forked_from_version": null,
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
"has_human_in_the_loop": false,
|
"has_human_in_the_loop": false,
|
||||||
"has_sensitive_action": false,
|
|
||||||
"id": "graph-123",
|
"id": "graph-123",
|
||||||
"input_schema": {
|
"input_schema": {
|
||||||
"properties": {},
|
"properties": {},
|
||||||
|
|||||||
@@ -27,8 +27,6 @@
|
|||||||
"properties": {}
|
"properties": {}
|
||||||
},
|
},
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
"has_human_in_the_loop": false,
|
|
||||||
"has_sensitive_action": false,
|
|
||||||
"trigger_setup_info": null,
|
"trigger_setup_info": null,
|
||||||
"new_output": false,
|
"new_output": false,
|
||||||
"can_access_graph": true,
|
"can_access_graph": true,
|
||||||
@@ -36,8 +34,7 @@
|
|||||||
"is_favorite": false,
|
"is_favorite": false,
|
||||||
"recommended_schedule_cron": null,
|
"recommended_schedule_cron": null,
|
||||||
"settings": {
|
"settings": {
|
||||||
"human_in_the_loop_safe_mode": true,
|
"human_in_the_loop_safe_mode": null
|
||||||
"sensitive_action_safe_mode": false
|
|
||||||
},
|
},
|
||||||
"marketplace_listing": null
|
"marketplace_listing": null
|
||||||
},
|
},
|
||||||
@@ -68,8 +65,6 @@
|
|||||||
"properties": {}
|
"properties": {}
|
||||||
},
|
},
|
||||||
"has_external_trigger": false,
|
"has_external_trigger": false,
|
||||||
"has_human_in_the_loop": false,
|
|
||||||
"has_sensitive_action": false,
|
|
||||||
"trigger_setup_info": null,
|
"trigger_setup_info": null,
|
||||||
"new_output": false,
|
"new_output": false,
|
||||||
"can_access_graph": false,
|
"can_access_graph": false,
|
||||||
@@ -77,8 +72,7 @@
|
|||||||
"is_favorite": false,
|
"is_favorite": false,
|
||||||
"recommended_schedule_cron": null,
|
"recommended_schedule_cron": null,
|
||||||
"settings": {
|
"settings": {
|
||||||
"human_in_the_loop_safe_mode": true,
|
"human_in_the_loop_safe_mode": null
|
||||||
"sensitive_action_safe_mode": false
|
|
||||||
},
|
},
|
||||||
"marketplace_listing": null
|
"marketplace_listing": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY=
|
|||||||
NEXT_PUBLIC_TURNSTILE=disabled
|
NEXT_PUBLIC_TURNSTILE=disabled
|
||||||
|
|
||||||
# PR previews
|
# PR previews
|
||||||
NEXT_PUBLIC_PREVIEW_STEALING_DEV=
|
NEXT_PUBLIC_PREVIEW_STEALING_DEV=
|
||||||
@@ -175,8 +175,6 @@ While server components and actions are cool and cutting-edge, they introduce a
|
|||||||
|
|
||||||
- Prefer [React Query](https://tanstack.com/query/latest/docs/framework/react/overview) for server state, colocated near consumers (see [state colocation](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster))
|
- Prefer [React Query](https://tanstack.com/query/latest/docs/framework/react/overview) for server state, colocated near consumers (see [state colocation](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster))
|
||||||
- Co-locate UI state inside components/hooks; keep global state minimal
|
- Co-locate UI state inside components/hooks; keep global state minimal
|
||||||
- Avoid `useMemo` and `useCallback` unless you have a measured performance issue
|
|
||||||
- Do not abuse `useEffect`; prefer state colocation and derive values directly when possible
|
|
||||||
|
|
||||||
### Styling and components
|
### Styling and components
|
||||||
|
|
||||||
@@ -551,48 +549,9 @@ Files:
|
|||||||
Types:
|
Types:
|
||||||
|
|
||||||
- Prefer `interface` for object shapes
|
- Prefer `interface` for object shapes
|
||||||
- Component props should be `interface Props { ... }` (not exported)
|
- Component props should be `interface Props { ... }`
|
||||||
- Only use specific exported names (e.g., `export interface MyComponentProps`) when the interface needs to be used outside the component
|
|
||||||
- Keep type definitions inline with the component - do not create separate `types.ts` files unless types are shared across multiple files
|
|
||||||
- Use precise types; avoid `any` and unsafe casts
|
- Use precise types; avoid `any` and unsafe casts
|
||||||
|
|
||||||
**Props naming examples:**
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// ✅ Good - internal props, not exported
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Modal({ title, onClose }: Props) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Good - exported when needed externally
|
|
||||||
export interface ModalProps {
|
|
||||||
title: string;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Modal({ title, onClose }: ModalProps) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ Bad - unnecessarily specific name for internal use
|
|
||||||
interface ModalComponentProps {
|
|
||||||
title: string;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ Bad - separate types.ts file for single component
|
|
||||||
// types.ts
|
|
||||||
export interface ModalProps { ... }
|
|
||||||
|
|
||||||
// Modal.tsx
|
|
||||||
import type { ModalProps } from './types';
|
|
||||||
```
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
||||||
- If more than one parameter is needed, pass a single `Args` object for clarity
|
- If more than one parameter is needed, pass a single `Args` object for clarity
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ export default defineConfig({
|
|||||||
client: "react-query",
|
client: "react-query",
|
||||||
httpClient: "fetch",
|
httpClient: "fetch",
|
||||||
indexFiles: false,
|
indexFiles: false,
|
||||||
mock: {
|
|
||||||
type: "msw",
|
|
||||||
baseUrl: "http://localhost:3000/api/proxy",
|
|
||||||
generateEachHttpStatus: true,
|
|
||||||
delay: 0,
|
|
||||||
},
|
|
||||||
override: {
|
override: {
|
||||||
mutator: {
|
mutator: {
|
||||||
path: "./mutators/custom-mutator.ts",
|
path: "./mutators/custom-mutator.ts",
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
"types": "tsc --noEmit",
|
"types": "tsc --noEmit",
|
||||||
"test": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test",
|
"test": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test",
|
||||||
"test-ui": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test --ui",
|
"test-ui": "NEXT_PUBLIC_PW_TEST=true next build --turbo && playwright test --ui",
|
||||||
"test:unit": "vitest run",
|
|
||||||
"test:unit:watch": "vitest",
|
|
||||||
"test:no-build": "playwright test",
|
"test:no-build": "playwright test",
|
||||||
"gentests": "playwright codegen http://localhost:3000",
|
"gentests": "playwright codegen http://localhost:3000",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
@@ -120,7 +118,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "4.1.2",
|
"@chromatic-com/storybook": "4.1.2",
|
||||||
"happy-dom": "20.3.4",
|
|
||||||
"@opentelemetry/instrumentation": "0.209.0",
|
"@opentelemetry/instrumentation": "0.209.0",
|
||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
"@storybook/addon-a11y": "9.1.5",
|
"@storybook/addon-a11y": "9.1.5",
|
||||||
@@ -130,8 +127,6 @@
|
|||||||
"@storybook/nextjs": "9.1.5",
|
"@storybook/nextjs": "9.1.5",
|
||||||
"@tanstack/eslint-plugin-query": "5.91.2",
|
"@tanstack/eslint-plugin-query": "5.91.2",
|
||||||
"@tanstack/react-query-devtools": "5.90.2",
|
"@tanstack/react-query-devtools": "5.90.2",
|
||||||
"@testing-library/dom": "10.4.1",
|
|
||||||
"@testing-library/react": "16.3.2",
|
|
||||||
"@types/canvas-confetti": "1.9.0",
|
"@types/canvas-confetti": "1.9.0",
|
||||||
"@types/lodash": "4.17.20",
|
"@types/lodash": "4.17.20",
|
||||||
"@types/negotiator": "0.6.4",
|
"@types/negotiator": "0.6.4",
|
||||||
@@ -140,7 +135,6 @@
|
|||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/react-modal": "3.16.3",
|
"@types/react-modal": "3.16.3",
|
||||||
"@types/react-window": "1.8.8",
|
"@types/react-window": "1.8.8",
|
||||||
"@vitejs/plugin-react": "5.1.2",
|
|
||||||
"axe-playwright": "2.2.2",
|
"axe-playwright": "2.2.2",
|
||||||
"chromatic": "13.3.3",
|
"chromatic": "13.3.3",
|
||||||
"concurrently": "9.2.1",
|
"concurrently": "9.2.1",
|
||||||
@@ -159,9 +153,7 @@
|
|||||||
"require-in-the-middle": "8.0.1",
|
"require-in-the-middle": "8.0.1",
|
||||||
"storybook": "9.1.5",
|
"storybook": "9.1.5",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3"
|
||||||
"vite-tsconfig-paths": "6.0.4",
|
|
||||||
"vitest": "4.0.17"
|
|
||||||
},
|
},
|
||||||
"msw": {
|
"msw": {
|
||||||
"workerDirectory": [
|
"workerDirectory": [
|
||||||
|
|||||||
1118
autogpt_platform/frontend/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 374 B |
|
Before Width: | Height: | Size: 663 B |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,58 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
|
||||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
|
||||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
const LOGOUT_REDIRECT_DELAY_MS = 400;
|
|
||||||
|
|
||||||
function wait(ms: number): Promise<void> {
|
|
||||||
return new Promise(function resolveAfterDelay(resolve) {
|
|
||||||
setTimeout(resolve, ms);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LogoutPage() {
|
|
||||||
const { logOut } = useSupabase();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
const hasStartedRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
function handleLogoutEffect() {
|
|
||||||
if (hasStartedRef.current) return;
|
|
||||||
hasStartedRef.current = true;
|
|
||||||
|
|
||||||
async function runLogout() {
|
|
||||||
try {
|
|
||||||
await logOut();
|
|
||||||
} catch {
|
|
||||||
toast({
|
|
||||||
title: "Failed to log out. Redirecting to login.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
await wait(LOGOUT_REDIRECT_DELAY_MS);
|
|
||||||
router.replace("/login");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void runLogout();
|
|
||||||
},
|
|
||||||
[logOut, router, toast],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center px-4">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-4 py-8">
|
|
||||||
<LoadingSpinner size="large" />
|
|
||||||
<Text variant="body" className="text-center">
|
|
||||||
Logging you out...
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ export async function GET(request: Request) {
|
|||||||
const { searchParams, origin } = new URL(request.url);
|
const { searchParams, origin } = new URL(request.url);
|
||||||
const code = searchParams.get("code");
|
const code = searchParams.get("code");
|
||||||
|
|
||||||
let next = "/";
|
let next = "/marketplace";
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
const supabase = await getServerSupabase();
|
const supabase = await getServerSupabase();
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||||
import { CircleNotchIcon, PlayIcon, StopIcon } from "@phosphor-icons/react";
|
import { PlayIcon, StopIcon } from "@phosphor-icons/react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
|
import { RunInputDialog } from "../RunInputDialog/RunInputDialog";
|
||||||
import { useRunGraph } from "./useRunGraph";
|
import { useRunGraph } from "./useRunGraph";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
||||||
const {
|
const {
|
||||||
@@ -25,31 +24,6 @@ export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
|||||||
useShallow((state) => state.isGraphRunning),
|
useShallow((state) => state.isGraphRunning),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading = isExecutingGraph || isTerminatingGraph || isSaving;
|
|
||||||
|
|
||||||
// Determine which icon to show with proper animation
|
|
||||||
const renderIcon = () => {
|
|
||||||
const iconClass = cn(
|
|
||||||
"size-4 transition-transform duration-200 ease-out",
|
|
||||||
!isLoading && "group-hover:scale-110",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<CircleNotchIcon
|
|
||||||
className={cn(iconClass, "animate-spin")}
|
|
||||||
weight="bold"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGraphRunning) {
|
|
||||||
return <StopIcon className={iconClass} weight="fill" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PlayIcon className={iconClass} weight="fill" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -59,18 +33,18 @@ export const RunGraph = ({ flowID }: { flowID: string | null }) => {
|
|||||||
variant={isGraphRunning ? "destructive" : "primary"}
|
variant={isGraphRunning ? "destructive" : "primary"}
|
||||||
data-id={isGraphRunning ? "stop-graph-button" : "run-graph-button"}
|
data-id={isGraphRunning ? "stop-graph-button" : "run-graph-button"}
|
||||||
onClick={isGraphRunning ? handleStopGraph : handleRunGraph}
|
onClick={isGraphRunning ? handleStopGraph : handleRunGraph}
|
||||||
disabled={!flowID || isLoading}
|
disabled={!flowID || isExecutingGraph || isTerminatingGraph}
|
||||||
className="group"
|
loading={isExecutingGraph || isTerminatingGraph || isSaving}
|
||||||
>
|
>
|
||||||
{renderIcon()}
|
{!isGraphRunning ? (
|
||||||
|
<PlayIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<StopIcon className="size-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{isLoading
|
{isGraphRunning ? "Stop agent" : "Run agent"}
|
||||||
? "Processing..."
|
|
||||||
: isGraphRunning
|
|
||||||
? "Stop agent"
|
|
||||||
: "Run agent"}
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<RunInputDialog
|
<RunInputDialog
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { useRunInputDialog } from "./useRunInputDialog";
|
|||||||
import { CronSchedulerDialog } from "../CronSchedulerDialog/CronSchedulerDialog";
|
import { CronSchedulerDialog } from "../CronSchedulerDialog/CronSchedulerDialog";
|
||||||
import { useTutorialStore } from "@/app/(platform)/build/stores/tutorialStore";
|
import { useTutorialStore } from "@/app/(platform)/build/stores/tutorialStore";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
|
||||||
|
|
||||||
export const RunInputDialog = ({
|
export const RunInputDialog = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -24,17 +23,19 @@ export const RunInputDialog = ({
|
|||||||
const hasInputs = useGraphStore((state) => state.hasInputs);
|
const hasInputs = useGraphStore((state) => state.hasInputs);
|
||||||
const hasCredentials = useGraphStore((state) => state.hasCredentials);
|
const hasCredentials = useGraphStore((state) => state.hasCredentials);
|
||||||
const inputSchema = useGraphStore((state) => state.inputSchema);
|
const inputSchema = useGraphStore((state) => state.inputSchema);
|
||||||
|
const credentialsSchema = useGraphStore(
|
||||||
|
(state) => state.credentialsInputSchema,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
credentialFields,
|
credentialsUiSchema,
|
||||||
requiredCredentials,
|
|
||||||
handleManualRun,
|
handleManualRun,
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
openCronSchedulerDialog,
|
openCronSchedulerDialog,
|
||||||
setOpenCronSchedulerDialog,
|
setOpenCronSchedulerDialog,
|
||||||
inputValues,
|
inputValues,
|
||||||
credentialValues,
|
credentialValues,
|
||||||
handleCredentialFieldChange,
|
handleCredentialChange,
|
||||||
isExecutingGraph,
|
isExecutingGraph,
|
||||||
} = useRunInputDialog({ setIsOpen });
|
} = useRunInputDialog({ setIsOpen });
|
||||||
|
|
||||||
@@ -61,67 +62,67 @@ export const RunInputDialog = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
set: setIsOpen,
|
set: setIsOpen,
|
||||||
}}
|
}}
|
||||||
styling={{ maxWidth: "700px", minWidth: "700px" }}
|
styling={{ maxWidth: "600px", minWidth: "600px" }}
|
||||||
>
|
>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<div
|
<div className="space-y-6 p-1" data-id="run-input-dialog-content">
|
||||||
className="grid grid-cols-[1fr_auto] gap-10 p-1"
|
{/* Credentials Section */}
|
||||||
data-id="run-input-dialog-content"
|
{hasCredentials() && (
|
||||||
>
|
<div data-id="run-input-credentials-section">
|
||||||
<div className="space-y-6">
|
<div className="mb-4">
|
||||||
{/* Credentials Section */}
|
<Text variant="h4" className="text-gray-900">
|
||||||
{hasCredentials() && credentialFields.length > 0 && (
|
Credentials
|
||||||
<div data-id="run-input-credentials-section">
|
</Text>
|
||||||
<div className="mb-4">
|
|
||||||
<Text variant="h4" className="text-gray-900">
|
|
||||||
Credentials
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="px-2" data-id="run-input-credentials-form">
|
|
||||||
<CredentialsGroupedView
|
|
||||||
credentialFields={credentialFields}
|
|
||||||
requiredCredentials={requiredCredentials}
|
|
||||||
inputCredentials={credentialValues}
|
|
||||||
inputValues={inputValues}
|
|
||||||
onCredentialChange={handleCredentialFieldChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="px-2" data-id="run-input-credentials-form">
|
||||||
|
<FormRenderer
|
||||||
{/* Inputs Section */}
|
jsonSchema={credentialsSchema as RJSFSchema}
|
||||||
{hasInputs() && (
|
handleChange={(v) => handleCredentialChange(v.formData)}
|
||||||
<div data-id="run-input-inputs-section">
|
uiSchema={credentialsUiSchema}
|
||||||
<div className="mb-4">
|
initialValues={{}}
|
||||||
<Text variant="h4" className="text-gray-900">
|
formContext={{
|
||||||
Inputs
|
showHandles: false,
|
||||||
</Text>
|
size: "large",
|
||||||
</div>
|
showOptionalToggle: false,
|
||||||
<div data-id="run-input-inputs-form">
|
}}
|
||||||
<FormRenderer
|
/>
|
||||||
jsonSchema={inputSchema as RJSFSchema}
|
|
||||||
handleChange={(v) => handleInputChange(v.formData)}
|
|
||||||
uiSchema={uiSchema}
|
|
||||||
initialValues={{}}
|
|
||||||
formContext={{
|
|
||||||
showHandles: false,
|
|
||||||
size: "large",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* Inputs Section */}
|
||||||
|
{hasInputs() && (
|
||||||
|
<div data-id="run-input-inputs-section">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Text variant="h4" className="text-gray-900">
|
||||||
|
Inputs
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div data-id="run-input-inputs-form">
|
||||||
|
<FormRenderer
|
||||||
|
jsonSchema={inputSchema as RJSFSchema}
|
||||||
|
handleChange={(v) => handleInputChange(v.formData)}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
initialValues={{}}
|
||||||
|
formContext={{
|
||||||
|
showHandles: false,
|
||||||
|
size: "large",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
<div
|
<div
|
||||||
className="flex flex-col items-end justify-start"
|
className="flex justify-end pt-2"
|
||||||
data-id="run-input-actions-section"
|
data-id="run-input-actions-section"
|
||||||
>
|
>
|
||||||
{purpose === "run" && (
|
{purpose === "run" && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="large"
|
size="large"
|
||||||
className="group h-fit min-w-0 gap-2 px-10"
|
className="group h-fit min-w-0 gap-2"
|
||||||
onClick={handleManualRun}
|
onClick={handleManualRun}
|
||||||
loading={isExecutingGraph}
|
loading={isExecutingGraph}
|
||||||
data-id="run-input-manual-run-button"
|
data-id="run-input-manual-run-button"
|
||||||
@@ -136,7 +137,7 @@ export const RunInputDialog = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="large"
|
size="large"
|
||||||
className="group h-fit min-w-0 gap-2 px-10"
|
className="group h-fit min-w-0 gap-2"
|
||||||
onClick={() => setOpenCronSchedulerDialog(true)}
|
onClick={() => setOpenCronSchedulerDialog(true)}
|
||||||
data-id="run-input-schedule-button"
|
data-id="run-input-schedule-button"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
GraphExecutionMeta,
|
GraphExecutionMeta,
|
||||||
} from "@/lib/autogpt-server-api";
|
} from "@/lib/autogpt-server-api";
|
||||||
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { uiSchema } from "../../../FlowEditor/nodes/uiSchema";
|
||||||
|
import { isCredentialFieldSchema } from "@/components/renderers/InputRenderer/custom/CredentialField/helpers";
|
||||||
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
import { useNodeStore } from "@/app/(platform)/build/stores/nodeStore";
|
||||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||||
import { useReactFlow } from "@xyflow/react";
|
import { useReactFlow } from "@xyflow/react";
|
||||||
import type { CredentialField } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers";
|
|
||||||
|
|
||||||
export const useRunInputDialog = ({
|
export const useRunInputDialog = ({
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
@@ -119,32 +120,27 @@ export const useRunInputDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert credentials schema to credential fields array for CredentialsGroupedView
|
// We are rendering the credentials field differently compared to other fields.
|
||||||
const credentialFields: CredentialField[] = useMemo(() => {
|
// In the node, we have the field name as "credential" - so our library catches it and renders it differently.
|
||||||
if (!credentialsSchema?.properties) return [];
|
// But here we have a different name, something like `Firecrawl credentials`, so here we are telling the library that this field is a credential field type.
|
||||||
return Object.entries(credentialsSchema.properties);
|
|
||||||
}, [credentialsSchema]);
|
|
||||||
|
|
||||||
// Get required credentials as a Set
|
const credentialsUiSchema = useMemo(() => {
|
||||||
const requiredCredentials = useMemo(() => {
|
const dynamicUiSchema: any = { ...uiSchema };
|
||||||
return new Set<string>(credentialsSchema?.required || []);
|
|
||||||
}, [credentialsSchema]);
|
|
||||||
|
|
||||||
// Handler for individual credential changes
|
if (credentialsSchema?.properties) {
|
||||||
const handleCredentialFieldChange = useCallback(
|
Object.keys(credentialsSchema.properties).forEach((fieldName) => {
|
||||||
(key: string, value?: CredentialsMetaInput) => {
|
const fieldSchema = credentialsSchema.properties[fieldName];
|
||||||
setCredentialValues((prev) => {
|
if (isCredentialFieldSchema(fieldSchema)) {
|
||||||
if (value) {
|
dynamicUiSchema[fieldName] = {
|
||||||
return { ...prev, [key]: value };
|
...dynamicUiSchema[fieldName],
|
||||||
} else {
|
"ui:field": "custom/credential_field",
|
||||||
const next = { ...prev };
|
};
|
||||||
delete next[key];
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
[],
|
|
||||||
);
|
return dynamicUiSchema;
|
||||||
|
}, [credentialsSchema]);
|
||||||
|
|
||||||
const handleManualRun = async () => {
|
const handleManualRun = async () => {
|
||||||
// Filter out incomplete credentials (those without a valid id)
|
// Filter out incomplete credentials (those without a valid id)
|
||||||
@@ -177,14 +173,12 @@ export const useRunInputDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credentialFields,
|
credentialsUiSchema,
|
||||||
requiredCredentials,
|
|
||||||
inputValues,
|
inputValues,
|
||||||
credentialValues,
|
credentialValues,
|
||||||
isExecutingGraph,
|
isExecutingGraph,
|
||||||
handleInputChange,
|
handleInputChange,
|
||||||
handleCredentialChange,
|
handleCredentialChange,
|
||||||
handleCredentialFieldChange,
|
|
||||||
handleManualRun,
|
handleManualRun,
|
||||||
openCronSchedulerDialog,
|
openCronSchedulerDialog,
|
||||||
setOpenCronSchedulerDialog,
|
setOpenCronSchedulerDialog,
|
||||||
|
|||||||
@@ -18,118 +18,69 @@ interface Props {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SafeModeButtonProps {
|
|
||||||
isEnabled: boolean;
|
|
||||||
label: string;
|
|
||||||
tooltipEnabled: string;
|
|
||||||
tooltipDisabled: string;
|
|
||||||
onToggle: () => void;
|
|
||||||
isPending: boolean;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SafeModeButton({
|
|
||||||
isEnabled,
|
|
||||||
label,
|
|
||||||
tooltipEnabled,
|
|
||||||
tooltipDisabled,
|
|
||||||
onToggle,
|
|
||||||
isPending,
|
|
||||||
fullWidth = false,
|
|
||||||
}: SafeModeButtonProps) {
|
|
||||||
return (
|
|
||||||
<Tooltip delayDuration={100}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={isEnabled ? "primary" : "outline"}
|
|
||||||
size="small"
|
|
||||||
onClick={onToggle}
|
|
||||||
disabled={isPending}
|
|
||||||
className={cn("justify-start", fullWidth ? "w-full" : "")}
|
|
||||||
>
|
|
||||||
{isEnabled ? (
|
|
||||||
<>
|
|
||||||
<ShieldCheckIcon weight="bold" size={16} />
|
|
||||||
<Text variant="body" className="text-zinc-200">
|
|
||||||
{label}: ON
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ShieldIcon weight="bold" size={16} />
|
|
||||||
<Text variant="body" className="text-zinc-600">
|
|
||||||
{label}: OFF
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="font-medium">
|
|
||||||
{label}: {isEnabled ? "ON" : "OFF"}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
|
||||||
{isEnabled ? tooltipEnabled : tooltipDisabled}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FloatingSafeModeToggle({
|
export function FloatingSafeModeToggle({
|
||||||
graph,
|
graph,
|
||||||
className,
|
className,
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const {
|
const {
|
||||||
currentHITLSafeMode,
|
currentSafeMode,
|
||||||
showHITLToggle,
|
|
||||||
isHITLStateUndetermined,
|
|
||||||
handleHITLToggle,
|
|
||||||
currentSensitiveActionSafeMode,
|
|
||||||
showSensitiveActionToggle,
|
|
||||||
handleSensitiveActionToggle,
|
|
||||||
isPending,
|
isPending,
|
||||||
shouldShowToggle,
|
shouldShowToggle,
|
||||||
|
isStateUndetermined,
|
||||||
|
handleToggle,
|
||||||
} = useAgentSafeMode(graph);
|
} = useAgentSafeMode(graph);
|
||||||
|
|
||||||
if (!shouldShowToggle || isPending) {
|
if (!shouldShowToggle || isStateUndetermined || isPending) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showHITL = showHITLToggle && !isHITLStateUndetermined;
|
|
||||||
const showSensitive = showSensitiveActionToggle;
|
|
||||||
|
|
||||||
if (!showHITL && !showSensitive) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("fixed z-50 flex flex-col gap-2", className)}>
|
<div className={cn("fixed z-50", className)}>
|
||||||
{showHITL && (
|
<Tooltip delayDuration={100}>
|
||||||
<SafeModeButton
|
<TooltipTrigger asChild>
|
||||||
isEnabled={currentHITLSafeMode}
|
<Button
|
||||||
label="Human in the loop block approval"
|
variant={currentSafeMode! ? "primary" : "outline"}
|
||||||
tooltipEnabled="The agent will pause at human-in-the-loop blocks and wait for your approval"
|
key={graph.id}
|
||||||
tooltipDisabled="Human in the loop blocks will proceed automatically"
|
size="small"
|
||||||
onToggle={handleHITLToggle}
|
title={
|
||||||
isPending={isPending}
|
currentSafeMode!
|
||||||
fullWidth={fullWidth}
|
? "Safe Mode: ON. Human in the loop blocks require manual review"
|
||||||
/>
|
: "Safe Mode: OFF. Human in the loop blocks proceed automatically"
|
||||||
)}
|
}
|
||||||
{showSensitive && (
|
onClick={handleToggle}
|
||||||
<SafeModeButton
|
className={cn(fullWidth ? "w-full" : "")}
|
||||||
isEnabled={currentSensitiveActionSafeMode}
|
>
|
||||||
label="Sensitive actions blocks approval"
|
{currentSafeMode! ? (
|
||||||
tooltipEnabled="The agent will pause at sensitive action blocks and wait for your approval"
|
<>
|
||||||
tooltipDisabled="Sensitive action blocks will proceed automatically"
|
<ShieldCheckIcon weight="bold" size={16} />
|
||||||
onToggle={handleSensitiveActionToggle}
|
<Text variant="body" className="text-zinc-200">
|
||||||
isPending={isPending}
|
Safe Mode: ON
|
||||||
fullWidth={fullWidth}
|
</Text>
|
||||||
/>
|
</>
|
||||||
)}
|
) : (
|
||||||
|
<>
|
||||||
|
<ShieldIcon weight="bold" size={16} />
|
||||||
|
<Text variant="body" className="text-zinc-600">
|
||||||
|
Safe Mode: OFF
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-medium">
|
||||||
|
Safe Mode: {currentSafeMode! ? "ON" : "OFF"}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{currentSafeMode!
|
||||||
|
? "Human in the loop blocks require manual review"
|
||||||
|
: "Human in the loop blocks proceed automatically"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ export const CustomControls = memo(
|
|||||||
const controls = [
|
const controls = [
|
||||||
{
|
{
|
||||||
id: "zoom-in-button",
|
id: "zoom-in-button",
|
||||||
icon: <PlusIcon className="size-3.5 text-zinc-600" />,
|
icon: <PlusIcon className="size-4" />,
|
||||||
label: "Zoom In",
|
label: "Zoom In",
|
||||||
onClick: () => zoomIn(),
|
onClick: () => zoomIn(),
|
||||||
className: "h-10 w-10 border-none",
|
className: "h-10 w-10 border-none",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "zoom-out-button",
|
id: "zoom-out-button",
|
||||||
icon: <MinusIcon className="size-3.5 text-zinc-600" />,
|
icon: <MinusIcon className="size-4" />,
|
||||||
label: "Zoom Out",
|
label: "Zoom Out",
|
||||||
onClick: () => zoomOut(),
|
onClick: () => zoomOut(),
|
||||||
className: "h-10 w-10 border-none",
|
className: "h-10 w-10 border-none",
|
||||||
@@ -68,9 +68,9 @@ export const CustomControls = memo(
|
|||||||
{
|
{
|
||||||
id: "tutorial-button",
|
id: "tutorial-button",
|
||||||
icon: isTutorialLoading ? (
|
icon: isTutorialLoading ? (
|
||||||
<CircleNotchIcon className="size-3.5 animate-spin text-zinc-600" />
|
<CircleNotchIcon className="size-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<ChalkboardIcon className="size-3.5 text-zinc-600" />
|
<ChalkboardIcon className="size-4" />
|
||||||
),
|
),
|
||||||
label: isTutorialLoading ? "Loading Tutorial..." : "Start Tutorial",
|
label: isTutorialLoading ? "Loading Tutorial..." : "Start Tutorial",
|
||||||
onClick: handleTutorialClick,
|
onClick: handleTutorialClick,
|
||||||
@@ -79,7 +79,7 @@ export const CustomControls = memo(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fit-view-button",
|
id: "fit-view-button",
|
||||||
icon: <FrameCornersIcon className="size-3.5 text-zinc-600" />,
|
icon: <FrameCornersIcon className="size-4" />,
|
||||||
label: "Fit View",
|
label: "Fit View",
|
||||||
onClick: () => fitView({ padding: 0.2, duration: 800, maxZoom: 1 }),
|
onClick: () => fitView({ padding: 0.2, duration: 800, maxZoom: 1 }),
|
||||||
className: "h-10 w-10 border-none",
|
className: "h-10 w-10 border-none",
|
||||||
@@ -87,9 +87,9 @@ export const CustomControls = memo(
|
|||||||
{
|
{
|
||||||
id: "lock-button",
|
id: "lock-button",
|
||||||
icon: !isLocked ? (
|
icon: !isLocked ? (
|
||||||
<LockOpenIcon className="size-3.5 text-zinc-600" />
|
<LockOpenIcon className="size-4" />
|
||||||
) : (
|
) : (
|
||||||
<LockIcon className="size-3.5 text-zinc-600" />
|
<LockIcon className="size-4" />
|
||||||
),
|
),
|
||||||
label: "Toggle Lock",
|
label: "Toggle Lock",
|
||||||
onClick: () => setIsLocked(!isLocked),
|
onClick: () => setIsLocked(!isLocked),
|
||||||
|
|||||||
@@ -139,6 +139,14 @@ export const useFlow = () => {
|
|||||||
useNodeStore.getState().setNodes([]);
|
useNodeStore.getState().setNodes([]);
|
||||||
useNodeStore.getState().clearResolutionState();
|
useNodeStore.getState().clearResolutionState();
|
||||||
addNodes(customNodes);
|
addNodes(customNodes);
|
||||||
|
|
||||||
|
// Sync hardcoded values with handle IDs.
|
||||||
|
// If a key–value field has a key without a value, the backend omits it from hardcoded values.
|
||||||
|
// But if a handleId exists for that key, it causes inconsistency.
|
||||||
|
// This ensures hardcoded values stay in sync with handle IDs.
|
||||||
|
customNodes.forEach((node) => {
|
||||||
|
useNodeStore.getState().syncHardcodedValuesWithHandleIds(node.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [customNodes, addNodes]);
|
}, [customNodes, addNodes]);
|
||||||
|
|
||||||
@@ -150,14 +158,6 @@ export const useFlow = () => {
|
|||||||
}
|
}
|
||||||
}, [graph?.links, addLinks]);
|
}, [graph?.links, addLinks]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (customNodes.length > 0 && graph?.links) {
|
|
||||||
customNodes.forEach((node) => {
|
|
||||||
useNodeStore.getState().syncHardcodedValuesWithHandleIds(node.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [customNodes, graph?.links]);
|
|
||||||
|
|
||||||
// update node execution status in nodes
|
// update node execution status in nodes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export type CustomEdgeData = {
|
|||||||
beadUp?: number;
|
beadUp?: number;
|
||||||
beadDown?: number;
|
beadDown?: number;
|
||||||
beadData?: Map<string, NodeExecutionResult["status"]>;
|
beadData?: Map<string, NodeExecutionResult["status"]>;
|
||||||
edgeColorClass?: string;
|
|
||||||
edgeHexColor?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomEdge = XYEdge<CustomEdgeData, "custom">;
|
export type CustomEdge = XYEdge<CustomEdgeData, "custom">;
|
||||||
@@ -38,6 +36,7 @@ const CustomEdge = ({
|
|||||||
selected,
|
selected,
|
||||||
}: EdgeProps<CustomEdge>) => {
|
}: EdgeProps<CustomEdge>) => {
|
||||||
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
const removeConnection = useEdgeStore((state) => state.removeEdge);
|
||||||
|
// Subscribe to the brokenEdgeIDs map and check if this edge is broken across any node
|
||||||
const isBroken = useNodeStore((state) => state.isEdgeBroken(id));
|
const isBroken = useNodeStore((state) => state.isEdgeBroken(id));
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@@ -53,7 +52,6 @@ const CustomEdge = ({
|
|||||||
const isStatic = data?.isStatic ?? false;
|
const isStatic = data?.isStatic ?? false;
|
||||||
const beadUp = data?.beadUp ?? 0;
|
const beadUp = data?.beadUp ?? 0;
|
||||||
const beadDown = data?.beadDown ?? 0;
|
const beadDown = data?.beadDown ?? 0;
|
||||||
const edgeColorClass = data?.edgeColorClass;
|
|
||||||
|
|
||||||
const handleRemoveEdge = () => {
|
const handleRemoveEdge = () => {
|
||||||
removeConnection(id);
|
removeConnection(id);
|
||||||
@@ -72,9 +70,7 @@ const CustomEdge = ({
|
|||||||
? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
|
? "!stroke-red-500 !stroke-[2px] [stroke-dasharray:4]"
|
||||||
: selected
|
: selected
|
||||||
? "stroke-zinc-800"
|
? "stroke-zinc-800"
|
||||||
: edgeColorClass
|
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
||||||
? cn(edgeColorClass, "opacity-70 hover:opacity-100")
|
|
||||||
: "stroke-zinc-500/50 hover:stroke-zinc-500",
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<JSBeads
|
<JSBeads
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useCallback } from "react";
|
|||||||
import { useNodeStore } from "../../../stores/nodeStore";
|
import { useNodeStore } from "../../../stores/nodeStore";
|
||||||
import { useHistoryStore } from "../../../stores/historyStore";
|
import { useHistoryStore } from "../../../stores/historyStore";
|
||||||
import { CustomEdge } from "./CustomEdge";
|
import { CustomEdge } from "./CustomEdge";
|
||||||
import { getEdgeColorFromOutputType } from "../nodes/helpers";
|
|
||||||
|
|
||||||
export const useCustomEdge = () => {
|
export const useCustomEdge = () => {
|
||||||
const edges = useEdgeStore((s) => s.edges);
|
const edges = useEdgeStore((s) => s.edges);
|
||||||
@@ -35,13 +34,8 @@ export const useCustomEdge = () => {
|
|||||||
if (exists) return;
|
if (exists) return;
|
||||||
|
|
||||||
const nodes = useNodeStore.getState().nodes;
|
const nodes = useNodeStore.getState().nodes;
|
||||||
const sourceNode = nodes.find((n) => n.id === conn.source);
|
const isStatic = nodes.find((n) => n.id === conn.source)?.data
|
||||||
const isStatic = sourceNode?.data?.staticOutput;
|
?.staticOutput;
|
||||||
|
|
||||||
const { colorClass, hexColor } = getEdgeColorFromOutputType(
|
|
||||||
sourceNode?.data?.outputSchema,
|
|
||||||
conn.sourceHandle,
|
|
||||||
);
|
|
||||||
|
|
||||||
addEdge({
|
addEdge({
|
||||||
source: conn.source,
|
source: conn.source,
|
||||||
@@ -50,8 +44,6 @@ export const useCustomEdge = () => {
|
|||||||
targetHandle: conn.targetHandle,
|
targetHandle: conn.targetHandle,
|
||||||
data: {
|
data: {
|
||||||
isStatic,
|
isStatic,
|
||||||
edgeColorClass: colorClass,
|
|
||||||
edgeHexColor: hexColor,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { Text } from "@/components/atoms/Text/Text";
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/molecules/Accordion/Accordion";
|
|
||||||
import { beautifyString, cn } from "@/lib/utils";
|
import { beautifyString, cn } from "@/lib/utils";
|
||||||
import { CopyIcon, CheckIcon } from "@phosphor-icons/react";
|
import { CaretDownIcon, CopyIcon, CheckIcon } from "@phosphor-icons/react";
|
||||||
import { NodeDataViewer } from "./components/NodeDataViewer/NodeDataViewer";
|
import { NodeDataViewer } from "./components/NodeDataViewer/NodeDataViewer";
|
||||||
import { ContentRenderer } from "./components/ContentRenderer";
|
import { ContentRenderer } from "./components/ContentRenderer";
|
||||||
import { useNodeOutput } from "./useNodeOutput";
|
import { useNodeOutput } from "./useNodeOutput";
|
||||||
import { ViewMoreData } from "./components/ViewMoreData";
|
import { ViewMoreData } from "./components/ViewMoreData";
|
||||||
|
|
||||||
export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
||||||
const { outputData, copiedKey, handleCopy, executionResultId, inputData } =
|
const {
|
||||||
useNodeOutput(nodeId);
|
outputData,
|
||||||
|
isExpanded,
|
||||||
|
setIsExpanded,
|
||||||
|
copiedKey,
|
||||||
|
handleCopy,
|
||||||
|
executionResultId,
|
||||||
|
inputData,
|
||||||
|
} = useNodeOutput(nodeId);
|
||||||
|
|
||||||
if (Object.keys(outputData).length === 0) {
|
if (Object.keys(outputData).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -24,117 +25,122 @@ export const NodeDataRenderer = ({ nodeId }: { nodeId: string }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-tutorial-id={`node-output`}
|
data-tutorial-id={`node-output`}
|
||||||
className="rounded-b-xl border-t border-zinc-200 px-4 py-2"
|
className="flex flex-col gap-3 rounded-b-xl border-t border-zinc-200 px-4 py-4"
|
||||||
>
|
>
|
||||||
<Accordion type="single" collapsible defaultValue="node-output">
|
<div className="flex items-center justify-between">
|
||||||
<AccordionItem value="node-output" className="border-none">
|
<Text variant="body-medium" className="!font-semibold text-slate-700">
|
||||||
<AccordionTrigger className="py-2 hover:no-underline">
|
Node Output
|
||||||
<Text
|
</Text>
|
||||||
variant="body-medium"
|
<Button
|
||||||
className="!font-semibold text-slate-700"
|
variant="ghost"
|
||||||
>
|
size="small"
|
||||||
Node Output
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
</Text>
|
className="h-fit min-w-0 p-1 text-slate-600 hover:text-slate-900"
|
||||||
</AccordionTrigger>
|
>
|
||||||
<AccordionContent className="pt-2">
|
<CaretDownIcon
|
||||||
<div className="flex max-w-[350px] flex-col gap-4">
|
size={16}
|
||||||
<div className="space-y-2">
|
weight="bold"
|
||||||
<Text variant="small-medium">Input</Text>
|
className={`transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ContentRenderer value={inputData} shortContent={false} />
|
{isExpanded && (
|
||||||
|
<>
|
||||||
|
<div className="flex max-w-[350px] flex-col gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Text variant="small-medium">Input</Text>
|
||||||
|
|
||||||
<div className="mt-1 flex justify-end gap-1">
|
<ContentRenderer value={inputData} shortContent={false} />
|
||||||
<NodeDataViewer
|
|
||||||
data={inputData}
|
<div className="mt-1 flex justify-end gap-1">
|
||||||
pinName="Input"
|
<NodeDataViewer
|
||||||
execId={executionResultId}
|
data={inputData}
|
||||||
/>
|
pinName="Input"
|
||||||
<Button
|
execId={executionResultId}
|
||||||
variant="secondary"
|
/>
|
||||||
size="small"
|
<Button
|
||||||
onClick={() => handleCopy("input", inputData)}
|
variant="secondary"
|
||||||
className={cn(
|
size="small"
|
||||||
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
onClick={() => handleCopy("input", inputData)}
|
||||||
copiedKey === "input" &&
|
className={cn(
|
||||||
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
||||||
)}
|
copiedKey === "input" &&
|
||||||
>
|
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
||||||
{copiedKey === "input" ? (
|
)}
|
||||||
<CheckIcon size={12} className="text-green-600" />
|
>
|
||||||
) : (
|
{copiedKey === "input" ? (
|
||||||
<CopyIcon size={12} />
|
<CheckIcon size={12} className="text-green-600" />
|
||||||
)}
|
) : (
|
||||||
</Button>
|
<CopyIcon size={12} />
|
||||||
</div>
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{Object.entries(outputData)
|
{Object.entries(outputData)
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map(([key, value]) => (
|
.map(([key, value]) => (
|
||||||
<div key={key} className="flex flex-col gap-2">
|
<div key={key} className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Text
|
<Text
|
||||||
variant="small-medium"
|
variant="small-medium"
|
||||||
className="!font-semibold text-slate-600"
|
className="!font-semibold text-slate-600"
|
||||||
>
|
>
|
||||||
Pin:
|
Pin:
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="small" className="text-slate-700">
|
<Text variant="small" className="text-slate-700">
|
||||||
{beautifyString(key)}
|
{beautifyString(key)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-2">
|
<div className="w-full space-y-2">
|
||||||
<Text
|
<Text
|
||||||
variant="small"
|
variant="small"
|
||||||
className="!font-semibold text-slate-600"
|
className="!font-semibold text-slate-600"
|
||||||
>
|
>
|
||||||
Data:
|
Data:
|
||||||
</Text>
|
</Text>
|
||||||
<div className="relative space-y-2">
|
<div className="relative space-y-2">
|
||||||
{value.map((item, index) => (
|
{value.map((item, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<ContentRenderer value={item} shortContent={true} />
|
<ContentRenderer value={item} shortContent={true} />
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="mt-1 flex justify-end gap-1">
|
|
||||||
<NodeDataViewer
|
|
||||||
data={value}
|
|
||||||
pinName={key}
|
|
||||||
execId={executionResultId}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleCopy(key, value)}
|
|
||||||
className={cn(
|
|
||||||
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
|
||||||
copiedKey === key &&
|
|
||||||
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{copiedKey === key ? (
|
|
||||||
<CheckIcon size={12} className="text-green-600" />
|
|
||||||
) : (
|
|
||||||
<CopyIcon size={12} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="mt-1 flex justify-end gap-1">
|
||||||
|
<NodeDataViewer
|
||||||
|
data={value}
|
||||||
|
pinName={key}
|
||||||
|
execId={executionResultId}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleCopy(key, value)}
|
||||||
|
className={cn(
|
||||||
|
"h-fit min-w-0 gap-1.5 border border-zinc-200 p-2 text-black hover:text-slate-900",
|
||||||
|
copiedKey === key &&
|
||||||
|
"border-green-400 bg-green-100 hover:border-green-400 hover:bg-green-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copiedKey === key ? (
|
||||||
|
<CheckIcon size={12} className="text-green-600" />
|
||||||
|
) : (
|
||||||
|
<CopyIcon size={12} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{Object.keys(outputData).length > 2 && (
|
{Object.keys(outputData).length > 2 && (
|
||||||
<ViewMoreData
|
<ViewMoreData outputData={outputData} execId={executionResultId} />
|
||||||
outputData={outputData}
|
)}
|
||||||
execId={executionResultId}
|
</>
|
||||||
/>
|
)}
|
||||||
)}
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useShallow } from "zustand/react/shallow";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const useNodeOutput = (nodeId: string) => {
|
export const useNodeOutput = (nodeId: string) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@@ -36,10 +37,13 @@ export const useNodeOutput = (nodeId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
outputData,
|
outputData: outputData,
|
||||||
inputData,
|
inputData: inputData,
|
||||||
copiedKey,
|
isExpanded: isExpanded,
|
||||||
handleCopy,
|
setIsExpanded: setIsExpanded,
|
||||||
|
copiedKey: copiedKey,
|
||||||
|
setCopiedKey: setCopiedKey,
|
||||||
|
handleCopy: handleCopy,
|
||||||
executionResultId: nodeExecutionResult?.node_exec_id,
|
executionResultId: nodeExecutionResult?.node_exec_id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -187,38 +187,3 @@ export const getTypeDisplayInfo = (schema: any) => {
|
|||||||
hexColor,
|
hexColor,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getEdgeColorFromOutputType(
|
|
||||||
outputSchema: RJSFSchema | undefined,
|
|
||||||
sourceHandle: string,
|
|
||||||
): { colorClass: string; hexColor: string } {
|
|
||||||
const defaultColor = {
|
|
||||||
colorClass: "stroke-zinc-500/50",
|
|
||||||
hexColor: "#6b7280",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!outputSchema?.properties) return defaultColor;
|
|
||||||
|
|
||||||
const properties = outputSchema.properties as Record<string, unknown>;
|
|
||||||
const handleParts = sourceHandle.split("_#_");
|
|
||||||
let currentSchema: Record<string, unknown> = properties;
|
|
||||||
|
|
||||||
for (let i = 0; i < handleParts.length; i++) {
|
|
||||||
const part = handleParts[i];
|
|
||||||
const fieldSchema = currentSchema[part] as Record<string, unknown>;
|
|
||||||
if (!fieldSchema) return defaultColor;
|
|
||||||
|
|
||||||
if (i === handleParts.length - 1) {
|
|
||||||
const { hexColor, colorClass } = getTypeDisplayInfo(fieldSchema);
|
|
||||||
return { colorClass: colorClass.replace("!text-", "stroke-"), hexColor };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldSchema.properties) {
|
|
||||||
currentSchema = fieldSchema.properties as Record<string, unknown>;
|
|
||||||
} else {
|
|
||||||
return defaultColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultColor;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,32 +1,7 @@
|
|||||||
type IconOptions = {
|
// These are SVG Phosphor icons
|
||||||
size?: number;
|
|
||||||
color?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_SIZE = 16;
|
|
||||||
const DEFAULT_COLOR = "#52525b"; // zinc-600
|
|
||||||
|
|
||||||
const iconPaths = {
|
|
||||||
ClickIcon: `M88,24V16a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0ZM16,104h8a8,8,0,0,0,0-16H16a8,8,0,0,0,0,16ZM124.42,39.16a8,8,0,0,0,10.74-3.58l8-16a8,8,0,0,0-14.31-7.16l-8,16A8,8,0,0,0,124.42,39.16Zm-96,81.69-16,8a8,8,0,0,0,7.16,14.31l16-8a8,8,0,1,0-7.16-14.31ZM219.31,184a16,16,0,0,1,0,22.63l-12.68,12.68a16,16,0,0,1-22.63,0L132.7,168,115,214.09c0,.1-.08.21-.13.32a15.83,15.83,0,0,1-14.6,9.59l-.79,0a15.83,15.83,0,0,1-14.41-11L32.8,52.92A16,16,0,0,1,52.92,32.8L213,85.07a16,16,0,0,1,1.41,29.8l-.32.13L168,132.69ZM208,195.31,156.69,144h0a16,16,0,0,1,4.93-26l.32-.14,45.95-17.64L48,48l52.2,159.86,17.65-46c0-.11.08-.22.13-.33a16,16,0,0,1,11.69-9.34,16.72,16.72,0,0,1,3-.28,16,16,0,0,1,11.3,4.69L195.31,208Z`,
|
|
||||||
Keyboard: `M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z`,
|
|
||||||
Drag: `M188,80a27.79,27.79,0,0,0-13.36,3.4,28,28,0,0,0-46.64-11A28,28,0,0,0,80,92v20H68a28,28,0,0,0-28,28v12a88,88,0,0,0,176,0V108A28,28,0,0,0,188,80Zm12,72a72,72,0,0,1-144,0V140a12,12,0,0,1,12-12H80v24a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V108a12,12,0,0,1,24,0Z`,
|
|
||||||
};
|
|
||||||
|
|
||||||
function createIcon(path: string, options: IconOptions = {}): string {
|
|
||||||
const size = options.size ?? DEFAULT_SIZE;
|
|
||||||
const color = options.color ?? DEFAULT_COLOR;
|
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="${color}" viewBox="0 0 256 256"><path d="${path}"></path></svg>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ICONS = {
|
export const ICONS = {
|
||||||
ClickIcon: createIcon(iconPaths.ClickIcon),
|
ClickIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256"><path d="M88,24V16a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0ZM16,104h8a8,8,0,0,0,0-16H16a8,8,0,0,0,0,16ZM124.42,39.16a8,8,0,0,0,10.74-3.58l8-16a8,8,0,0,0-14.31-7.16l-8,16A8,8,0,0,0,124.42,39.16Zm-96,81.69-16,8a8,8,0,0,0,7.16,14.31l16-8a8,8,0,1,0-7.16-14.31ZM219.31,184a16,16,0,0,1,0,22.63l-12.68,12.68a16,16,0,0,1-22.63,0L132.7,168,115,214.09c0,.1-.08.21-.13.32a15.83,15.83,0,0,1-14.6,9.59l-.79,0a15.83,15.83,0,0,1-14.41-11L32.8,52.92A16,16,0,0,1,52.92,32.8L213,85.07a16,16,0,0,1,1.41,29.8l-.32.13L168,132.69ZM208,195.31,156.69,144h0a16,16,0,0,1,4.93-26l.32-.14,45.95-17.64L48,48l52.2,159.86,17.65-46c0-.11.08-.22.13-.33a16,16,0,0,1,11.69-9.34,16.72,16.72,0,0,1,3-.28,16,16,0,0,1,11.3,4.69L195.31,208Z"></path></svg>`,
|
||||||
Keyboard: createIcon(iconPaths.Keyboard),
|
Keyboard: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256"><path d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z"></path></svg>`,
|
||||||
Drag: createIcon(iconPaths.Drag),
|
Drag: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256"><path d="M188,80a27.79,27.79,0,0,0-13.36,3.4,28,28,0,0,0-46.64-11A28,28,0,0,0,80,92v20H68a28,28,0,0,0-28,28v12a88,88,0,0,0,176,0V108A28,28,0,0,0,188,80Zm12,72a72,72,0,0,1-144,0V140a12,12,0,0,1,12-12H80v24a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V92a12,12,0,0,1,24,0v28a8,8,0,0,0,16,0V108a12,12,0,0,1,24,0Z"></path></svg>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getIcon(
|
|
||||||
name: keyof typeof iconPaths,
|
|
||||||
options?: IconOptions,
|
|
||||||
): string {
|
|
||||||
return createIcon(iconPaths[name], options);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import { useNodeStore } from "../../../stores/nodeStore";
|
import { useNodeStore } from "../../../stores/nodeStore";
|
||||||
import { useEdgeStore } from "../../../stores/edgeStore";
|
import { useEdgeStore } from "../../../stores/edgeStore";
|
||||||
import { useTutorialStore } from "../../../stores/tutorialStore";
|
|
||||||
|
|
||||||
let isTutorialLoading = false;
|
let isTutorialLoading = false;
|
||||||
let tutorialLoadingCallback: ((loading: boolean) => void) | null = null;
|
let tutorialLoadingCallback: ((loading: boolean) => void) | null = null;
|
||||||
@@ -61,14 +60,12 @@ export const startTutorial = async () => {
|
|||||||
handleTutorialComplete();
|
handleTutorialComplete();
|
||||||
removeTutorialStyles();
|
removeTutorialStyles();
|
||||||
clearPrefetchedBlocks();
|
clearPrefetchedBlocks();
|
||||||
useTutorialStore.getState().setIsTutorialRunning(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tour.on("cancel", () => {
|
tour.on("cancel", () => {
|
||||||
handleTutorialCancel(tour);
|
handleTutorialCancel(tour);
|
||||||
removeTutorialStyles();
|
removeTutorialStyles();
|
||||||
clearPrefetchedBlocks();
|
clearPrefetchedBlocks();
|
||||||
useTutorialStore.getState().setIsTutorialRunning(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const step of tour.steps) {
|
for (const step of tour.steps) {
|
||||||
|
|||||||
@@ -61,18 +61,12 @@ export const convertNodesPlusBlockInfoIntoCustomNodes = (
|
|||||||
return customNode;
|
return customNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isToolSourceName = (sourceName: string): boolean =>
|
|
||||||
sourceName.startsWith("tools_^_");
|
|
||||||
|
|
||||||
const cleanupSourceName = (sourceName: string): string =>
|
|
||||||
isToolSourceName(sourceName) ? "tools" : sourceName;
|
|
||||||
|
|
||||||
export const linkToCustomEdge = (link: Link): CustomEdge => ({
|
export const linkToCustomEdge = (link: Link): CustomEdge => ({
|
||||||
id: link.id ?? "",
|
id: link.id ?? "",
|
||||||
type: "custom" as const,
|
type: "custom" as const,
|
||||||
source: link.source_id,
|
source: link.source_id,
|
||||||
target: link.sink_id,
|
target: link.sink_id,
|
||||||
sourceHandle: cleanupSourceName(link.source_name),
|
sourceHandle: link.source_name,
|
||||||
targetHandle: link.sink_name,
|
targetHandle: link.sink_name,
|
||||||
data: {
|
data: {
|
||||||
isStatic: link.is_static,
|
isStatic: link.is_static,
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
|
import { Text } from "@/components/atoms/Text/Text";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { List } from "@phosphor-icons/react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
|
||||||
|
import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
|
||||||
|
import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState";
|
||||||
|
import { SessionsDrawer } from "./components/SessionsDrawer/SessionsDrawer";
|
||||||
|
import { useChat } from "./useChat";
|
||||||
|
|
||||||
|
export interface ChatProps {
|
||||||
|
className?: string;
|
||||||
|
headerTitle?: React.ReactNode;
|
||||||
|
showHeader?: boolean;
|
||||||
|
showSessionInfo?: boolean;
|
||||||
|
showNewChatButton?: boolean;
|
||||||
|
onNewChat?: () => void;
|
||||||
|
headerActions?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Chat({
|
||||||
|
className,
|
||||||
|
headerTitle = "AutoGPT Copilot",
|
||||||
|
showHeader = true,
|
||||||
|
showSessionInfo = true,
|
||||||
|
showNewChatButton = true,
|
||||||
|
onNewChat,
|
||||||
|
headerActions,
|
||||||
|
}: ChatProps) {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
isLoading,
|
||||||
|
isCreating,
|
||||||
|
error,
|
||||||
|
sessionId,
|
||||||
|
createSession,
|
||||||
|
clearSession,
|
||||||
|
loadSession,
|
||||||
|
} = useChat();
|
||||||
|
|
||||||
|
const [isSessionsDrawerOpen, setIsSessionsDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleNewChat = () => {
|
||||||
|
clearSession();
|
||||||
|
onNewChat?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectSession = async (sessionId: string) => {
|
||||||
|
try {
|
||||||
|
await loadSession(sessionId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load session:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex h-full flex-col", className)}>
|
||||||
|
{/* Header */}
|
||||||
|
{showHeader && (
|
||||||
|
<header className="shrink-0 border-t border-zinc-200 bg-white p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
aria-label="View sessions"
|
||||||
|
onClick={() => setIsSessionsDrawerOpen(true)}
|
||||||
|
className="flex size-8 items-center justify-center rounded hover:bg-zinc-100"
|
||||||
|
>
|
||||||
|
<List width="1.25rem" height="1.25rem" />
|
||||||
|
</button>
|
||||||
|
{typeof headerTitle === "string" ? (
|
||||||
|
<Text variant="h2" className="text-lg font-semibold">
|
||||||
|
{headerTitle}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
headerTitle
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{showSessionInfo && sessionId && (
|
||||||
|
<>
|
||||||
|
{showNewChatButton && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="small"
|
||||||
|
onClick={handleNewChat}
|
||||||
|
>
|
||||||
|
New Chat
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{headerActions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
|
{/* Loading State - show when explicitly loading/creating OR when we don't have a session yet and no error */}
|
||||||
|
{(isLoading || isCreating || (!sessionId && !error)) && (
|
||||||
|
<ChatLoadingState
|
||||||
|
message={isCreating ? "Creating session..." : "Loading..."}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
{error && !isLoading && (
|
||||||
|
<ChatErrorState error={error} onRetry={createSession} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Session Content */}
|
||||||
|
{sessionId && !isLoading && !error && (
|
||||||
|
<ChatContainer
|
||||||
|
sessionId={sessionId}
|
||||||
|
initialMessages={messages}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Sessions Drawer */}
|
||||||
|
<SessionsDrawer
|
||||||
|
isOpen={isSessionsDrawerOpen}
|
||||||
|
onClose={() => setIsSessionsDrawerOpen(false)}
|
||||||
|
onSelectSession={handleSelectSession}
|
||||||
|
currentSessionId={sessionId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export function AuthPromptWidget({
|
|||||||
message,
|
message,
|
||||||
sessionId,
|
sessionId,
|
||||||
agentInfo,
|
agentInfo,
|
||||||
returnUrl = "/copilot/chat",
|
returnUrl = "/chat",
|
||||||
className,
|
className,
|
||||||
}: AuthPromptWidgetProps) {
|
}: AuthPromptWidgetProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { usePageContext } from "../../usePageContext";
|
||||||
|
import { ChatInput } from "../ChatInput/ChatInput";
|
||||||
|
import { MessageList } from "../MessageList/MessageList";
|
||||||
|
import { QuickActionsWelcome } from "../QuickActionsWelcome/QuickActionsWelcome";
|
||||||
|
import { useChatContainer } from "./useChatContainer";
|
||||||
|
|
||||||
|
export interface ChatContainerProps {
|
||||||
|
sessionId: string | null;
|
||||||
|
initialMessages: SessionDetailResponse["messages"];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatContainer({
|
||||||
|
sessionId,
|
||||||
|
initialMessages,
|
||||||
|
className,
|
||||||
|
}: ChatContainerProps) {
|
||||||
|
const { messages, streamingChunks, isStreaming, sendMessage } =
|
||||||
|
useChatContainer({
|
||||||
|
sessionId,
|
||||||
|
initialMessages,
|
||||||
|
});
|
||||||
|
const { capturePageContext } = usePageContext();
|
||||||
|
|
||||||
|
// Wrap sendMessage to automatically capture page context
|
||||||
|
const sendMessageWithContext = useCallback(
|
||||||
|
async (content: string, isUserMessage: boolean = true) => {
|
||||||
|
const context = capturePageContext();
|
||||||
|
await sendMessage(content, isUserMessage, context);
|
||||||
|
},
|
||||||
|
[sendMessage, capturePageContext],
|
||||||
|
);
|
||||||
|
|
||||||
|
const quickActions = [
|
||||||
|
"Find agents for social media management",
|
||||||
|
"Show me agents for content creation",
|
||||||
|
"Help me automate my business",
|
||||||
|
"What can you help me with?",
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex h-full min-h-0 flex-col", className)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
backgroundImage:
|
||||||
|
"radial-gradient(#e5e5e5 0.5px, transparent 0.5px), radial-gradient(#e5e5e5 0.5px, #ffffff 0.5px)",
|
||||||
|
backgroundSize: "20px 20px",
|
||||||
|
backgroundPosition: "0 0, 10px 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Messages or Welcome Screen */}
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden pb-24">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<QuickActionsWelcome
|
||||||
|
title="Welcome to AutoGPT Copilot"
|
||||||
|
description="Start a conversation to discover and run AI agents."
|
||||||
|
actions={quickActions}
|
||||||
|
onActionClick={sendMessageWithContext}
|
||||||
|
disabled={isStreaming || !sessionId}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MessageList
|
||||||
|
messages={messages}
|
||||||
|
streamingChunks={streamingChunks}
|
||||||
|
isStreaming={isStreaming}
|
||||||
|
onSendMessage={sendMessageWithContext}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input - Always visible */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-50 border-t border-zinc-200 bg-white p-4">
|
||||||
|
<ChatInput
|
||||||
|
onSend={sendMessageWithContext}
|
||||||
|
disabled={isStreaming || !sessionId}
|
||||||
|
placeholder={
|
||||||
|
sessionId ? "Type your message..." : "Creating session..."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { StreamChunk } from "../../useChatStream";
|
import { StreamChunk } from "../../useChatStream";
|
||||||
import type { HandlerDependencies } from "./handlers";
|
import type { HandlerDependencies } from "./useChatContainer.handlers";
|
||||||
import {
|
import {
|
||||||
handleError,
|
handleError,
|
||||||
handleLoginNeeded,
|
handleLoginNeeded,
|
||||||
@@ -9,30 +9,12 @@ import {
|
|||||||
handleTextEnded,
|
handleTextEnded,
|
||||||
handleToolCallStart,
|
handleToolCallStart,
|
||||||
handleToolResponse,
|
handleToolResponse,
|
||||||
isRegionBlockedError,
|
} from "./useChatContainer.handlers";
|
||||||
} from "./handlers";
|
|
||||||
|
|
||||||
export function createStreamEventDispatcher(
|
export function createStreamEventDispatcher(
|
||||||
deps: HandlerDependencies,
|
deps: HandlerDependencies,
|
||||||
): (chunk: StreamChunk) => void {
|
): (chunk: StreamChunk) => void {
|
||||||
return function dispatchStreamEvent(chunk: StreamChunk): void {
|
return function dispatchStreamEvent(chunk: StreamChunk): void {
|
||||||
if (
|
|
||||||
chunk.type === "text_chunk" ||
|
|
||||||
chunk.type === "tool_call_start" ||
|
|
||||||
chunk.type === "tool_response" ||
|
|
||||||
chunk.type === "login_needed" ||
|
|
||||||
chunk.type === "need_login" ||
|
|
||||||
chunk.type === "error"
|
|
||||||
) {
|
|
||||||
if (!deps.hasResponseRef.current) {
|
|
||||||
console.info("[ChatStream] First response chunk:", {
|
|
||||||
type: chunk.type,
|
|
||||||
sessionId: deps.sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
deps.hasResponseRef.current = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (chunk.type) {
|
switch (chunk.type) {
|
||||||
case "text_chunk":
|
case "text_chunk":
|
||||||
handleTextChunk(chunk, deps);
|
handleTextChunk(chunk, deps);
|
||||||
@@ -56,23 +38,15 @@ export function createStreamEventDispatcher(
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "stream_end":
|
case "stream_end":
|
||||||
console.info("[ChatStream] Stream ended:", {
|
|
||||||
sessionId: deps.sessionId,
|
|
||||||
hasResponse: deps.hasResponseRef.current,
|
|
||||||
chunkCount: deps.streamingChunksRef.current.length,
|
|
||||||
});
|
|
||||||
handleStreamEnd(chunk, deps);
|
handleStreamEnd(chunk, deps);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "error":
|
case "error":
|
||||||
const isRegionBlocked = isRegionBlockedError(chunk);
|
|
||||||
handleError(chunk, deps);
|
handleError(chunk, deps);
|
||||||
// Show toast at dispatcher level to avoid circular dependencies
|
// Show toast at dispatcher level to avoid circular dependencies
|
||||||
if (!isRegionBlocked) {
|
toast.error("Chat Error", {
|
||||||
toast.error("Chat Error", {
|
description: chunk.message || chunk.content || "An error occurred",
|
||||||
description: chunk.message || chunk.content || "An error occurred",
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "usage":
|
case "usage":
|
||||||
@@ -1,33 +1,6 @@
|
|||||||
import { SessionKey, sessionStorage } from "@/services/storage/session-storage";
|
|
||||||
import type { ToolResult } from "@/types/chat";
|
import type { ToolResult } from "@/types/chat";
|
||||||
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
||||||
|
|
||||||
export function hasSentInitialPrompt(sessionId: string): boolean {
|
|
||||||
try {
|
|
||||||
const sent = JSON.parse(
|
|
||||||
sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
|
|
||||||
);
|
|
||||||
return sent[sessionId] === true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function markInitialPromptSent(sessionId: string): void {
|
|
||||||
try {
|
|
||||||
const sent = JSON.parse(
|
|
||||||
sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
|
|
||||||
);
|
|
||||||
sent[sessionId] = true;
|
|
||||||
sessionStorage.set(
|
|
||||||
SessionKey.CHAT_SENT_INITIAL_PROMPTS,
|
|
||||||
JSON.stringify(sent),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// Ignore storage errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removePageContext(content: string): string {
|
export function removePageContext(content: string): string {
|
||||||
// Remove "Page URL: ..." pattern at start of line (case insensitive, handles various formats)
|
// Remove "Page URL: ..." pattern at start of line (case insensitive, handles various formats)
|
||||||
let cleaned = content.replace(/^\s*Page URL:\s*[^\n\r]*/gim, "");
|
let cleaned = content.replace(/^\s*Page URL:\s*[^\n\r]*/gim, "");
|
||||||
@@ -234,22 +207,12 @@ export function parseToolResponse(
|
|||||||
if (responseType === "setup_requirements") {
|
if (responseType === "setup_requirements") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (responseType === "understanding_updated") {
|
|
||||||
return {
|
|
||||||
type: "tool_response",
|
|
||||||
toolId,
|
|
||||||
toolName,
|
|
||||||
result: (parsedResult || result) as ToolResult,
|
|
||||||
success: true,
|
|
||||||
timestamp: timestamp || new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: "tool_response",
|
type: "tool_response",
|
||||||
toolId,
|
toolId,
|
||||||
toolName,
|
toolName,
|
||||||
result: parsedResult ? (parsedResult as ToolResult) : result,
|
result,
|
||||||
success: true,
|
success: true,
|
||||||
timestamp: timestamp || new Date(),
|
timestamp: timestamp || new Date(),
|
||||||
};
|
};
|
||||||
@@ -304,34 +267,23 @@ export function extractCredentialsNeeded(
|
|||||||
| undefined;
|
| undefined;
|
||||||
if (missingCreds && Object.keys(missingCreds).length > 0) {
|
if (missingCreds && Object.keys(missingCreds).length > 0) {
|
||||||
const agentName = (setupInfo?.agent_name as string) || "this block";
|
const agentName = (setupInfo?.agent_name as string) || "this block";
|
||||||
const credentials = Object.values(missingCreds).map((credInfo) => {
|
const credentials = Object.values(missingCreds).map((credInfo) => ({
|
||||||
// Normalize to array at boundary - prefer 'types' array, fall back to single 'type'
|
provider: (credInfo.provider as string) || "unknown",
|
||||||
const typesArray = credInfo.types as
|
providerName:
|
||||||
| Array<"api_key" | "oauth2" | "user_password" | "host_scoped">
|
(credInfo.provider_name as string) ||
|
||||||
| undefined;
|
(credInfo.provider as string) ||
|
||||||
const singleType =
|
"Unknown Provider",
|
||||||
|
credentialType:
|
||||||
(credInfo.type as
|
(credInfo.type as
|
||||||
| "api_key"
|
| "api_key"
|
||||||
| "oauth2"
|
| "oauth2"
|
||||||
| "user_password"
|
| "user_password"
|
||||||
| "host_scoped"
|
| "host_scoped") || "api_key",
|
||||||
| undefined) || "api_key";
|
title:
|
||||||
const credentialTypes =
|
(credInfo.title as string) ||
|
||||||
typesArray && typesArray.length > 0 ? typesArray : [singleType];
|
`${(credInfo.provider_name as string) || (credInfo.provider as string)} credentials`,
|
||||||
|
scopes: credInfo.scopes as string[] | undefined,
|
||||||
return {
|
}));
|
||||||
provider: (credInfo.provider as string) || "unknown",
|
|
||||||
providerName:
|
|
||||||
(credInfo.provider_name as string) ||
|
|
||||||
(credInfo.provider as string) ||
|
|
||||||
"Unknown Provider",
|
|
||||||
credentialTypes,
|
|
||||||
title:
|
|
||||||
(credInfo.title as string) ||
|
|
||||||
`${(credInfo.provider_name as string) || (credInfo.provider as string)} credentials`,
|
|
||||||
scopes: credInfo.scopes as string[] | undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
type: "credentials_needed",
|
type: "credentials_needed",
|
||||||
toolName,
|
toolName,
|
||||||
@@ -406,14 +358,11 @@ export function extractInputsNeeded(
|
|||||||
credentials.forEach((cred) => {
|
credentials.forEach((cred) => {
|
||||||
const id = cred.id as string;
|
const id = cred.id as string;
|
||||||
if (id) {
|
if (id) {
|
||||||
const credentialTypes = Array.isArray(cred.types)
|
|
||||||
? cred.types
|
|
||||||
: [(cred.type as string) || "api_key"];
|
|
||||||
credentialsSchema[id] = {
|
credentialsSchema[id] = {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {},
|
properties: {},
|
||||||
credentials_provider: [cred.provider as string],
|
credentials_provider: [cred.provider as string],
|
||||||
credentials_types: credentialTypes,
|
credentials_types: [(cred.type as string) || "api_key"],
|
||||||
credentials_scopes: cred.scopes as string[] | undefined,
|
credentials_scopes: cred.scopes as string[] | undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -7,30 +7,15 @@ import {
|
|||||||
parseToolResponse,
|
parseToolResponse,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
function isToolCallMessage(
|
|
||||||
message: ChatMessageData,
|
|
||||||
): message is Extract<ChatMessageData, { type: "tool_call" }> {
|
|
||||||
return message.type === "tool_call";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HandlerDependencies {
|
export interface HandlerDependencies {
|
||||||
setHasTextChunks: Dispatch<SetStateAction<boolean>>;
|
setHasTextChunks: Dispatch<SetStateAction<boolean>>;
|
||||||
setStreamingChunks: Dispatch<SetStateAction<string[]>>;
|
setStreamingChunks: Dispatch<SetStateAction<string[]>>;
|
||||||
streamingChunksRef: MutableRefObject<string[]>;
|
streamingChunksRef: MutableRefObject<string[]>;
|
||||||
hasResponseRef: MutableRefObject<boolean>;
|
|
||||||
setMessages: Dispatch<SetStateAction<ChatMessageData[]>>;
|
setMessages: Dispatch<SetStateAction<ChatMessageData[]>>;
|
||||||
setIsStreamingInitiated: Dispatch<SetStateAction<boolean>>;
|
setIsStreamingInitiated: Dispatch<SetStateAction<boolean>>;
|
||||||
setIsRegionBlockedModalOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRegionBlockedError(chunk: StreamChunk): boolean {
|
|
||||||
if (chunk.code === "MODEL_NOT_AVAILABLE_REGION") return true;
|
|
||||||
const message = chunk.message || chunk.content;
|
|
||||||
if (typeof message !== "string") return false;
|
|
||||||
return message.toLowerCase().includes("not available in your region");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleTextChunk(chunk: StreamChunk, deps: HandlerDependencies) {
|
export function handleTextChunk(chunk: StreamChunk, deps: HandlerDependencies) {
|
||||||
if (!chunk.content) return;
|
if (!chunk.content) return;
|
||||||
deps.setHasTextChunks(true);
|
deps.setHasTextChunks(true);
|
||||||
@@ -45,17 +30,16 @@ export function handleTextEnded(
|
|||||||
_chunk: StreamChunk,
|
_chunk: StreamChunk,
|
||||||
deps: HandlerDependencies,
|
deps: HandlerDependencies,
|
||||||
) {
|
) {
|
||||||
|
console.log("[Text Ended] Saving streamed text as assistant message");
|
||||||
const completedText = deps.streamingChunksRef.current.join("");
|
const completedText = deps.streamingChunksRef.current.join("");
|
||||||
if (completedText.trim()) {
|
if (completedText.trim()) {
|
||||||
deps.setMessages((prev) => {
|
const assistantMessage: ChatMessageData = {
|
||||||
const assistantMessage: ChatMessageData = {
|
type: "message",
|
||||||
type: "message",
|
role: "assistant",
|
||||||
role: "assistant",
|
content: completedText,
|
||||||
content: completedText,
|
timestamp: new Date(),
|
||||||
timestamp: new Date(),
|
};
|
||||||
};
|
deps.setMessages((prev) => [...prev, assistantMessage]);
|
||||||
return [...prev, assistantMessage];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
deps.setStreamingChunks([]);
|
deps.setStreamingChunks([]);
|
||||||
deps.streamingChunksRef.current = [];
|
deps.streamingChunksRef.current = [];
|
||||||
@@ -66,45 +50,30 @@ export function handleToolCallStart(
|
|||||||
chunk: StreamChunk,
|
chunk: StreamChunk,
|
||||||
deps: HandlerDependencies,
|
deps: HandlerDependencies,
|
||||||
) {
|
) {
|
||||||
const toolCallMessage: Extract<ChatMessageData, { type: "tool_call" }> = {
|
const toolCallMessage: ChatMessageData = {
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
toolId: chunk.tool_id || `tool-${Date.now()}-${chunk.idx || 0}`,
|
toolId: chunk.tool_id || `tool-${Date.now()}-${chunk.idx || 0}`,
|
||||||
toolName: chunk.tool_name || "Executing",
|
toolName: chunk.tool_name || "Executing...",
|
||||||
arguments: chunk.arguments || {},
|
arguments: chunk.arguments || {},
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
|
deps.setMessages((prev) => [...prev, toolCallMessage]);
|
||||||
function updateToolCallMessages(prev: ChatMessageData[]) {
|
console.log("[Tool Call Start]", {
|
||||||
const existingIndex = prev.findIndex(function findToolCallIndex(msg) {
|
toolId: toolCallMessage.toolId,
|
||||||
return isToolCallMessage(msg) && msg.toolId === toolCallMessage.toolId;
|
toolName: toolCallMessage.toolName,
|
||||||
});
|
timestamp: new Date().toISOString(),
|
||||||
if (existingIndex === -1) {
|
});
|
||||||
return [...prev, toolCallMessage];
|
|
||||||
}
|
|
||||||
const nextMessages = [...prev];
|
|
||||||
const existing = nextMessages[existingIndex];
|
|
||||||
if (!isToolCallMessage(existing)) return prev;
|
|
||||||
const nextArguments =
|
|
||||||
toolCallMessage.arguments &&
|
|
||||||
Object.keys(toolCallMessage.arguments).length > 0
|
|
||||||
? toolCallMessage.arguments
|
|
||||||
: existing.arguments;
|
|
||||||
nextMessages[existingIndex] = {
|
|
||||||
...existing,
|
|
||||||
toolName: toolCallMessage.toolName || existing.toolName,
|
|
||||||
arguments: nextArguments,
|
|
||||||
timestamp: toolCallMessage.timestamp,
|
|
||||||
};
|
|
||||||
return nextMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
deps.setMessages(updateToolCallMessages);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleToolResponse(
|
export function handleToolResponse(
|
||||||
chunk: StreamChunk,
|
chunk: StreamChunk,
|
||||||
deps: HandlerDependencies,
|
deps: HandlerDependencies,
|
||||||
) {
|
) {
|
||||||
|
console.log("[Tool Response] Received:", {
|
||||||
|
toolId: chunk.tool_id,
|
||||||
|
toolName: chunk.tool_name,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
let toolName = chunk.tool_name || "unknown";
|
let toolName = chunk.tool_name || "unknown";
|
||||||
if (!chunk.tool_name || chunk.tool_name === "unknown") {
|
if (!chunk.tool_name || chunk.tool_name === "unknown") {
|
||||||
deps.setMessages((prev) => {
|
deps.setMessages((prev) => {
|
||||||
@@ -158,15 +127,22 @@ export function handleToolResponse(
|
|||||||
const toolCallIndex = prev.findIndex(
|
const toolCallIndex = prev.findIndex(
|
||||||
(msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
|
(msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
|
||||||
);
|
);
|
||||||
const hasResponse = prev.some(
|
|
||||||
(msg) => msg.type === "tool_response" && msg.toolId === chunk.tool_id,
|
|
||||||
);
|
|
||||||
if (hasResponse) return prev;
|
|
||||||
if (toolCallIndex !== -1) {
|
if (toolCallIndex !== -1) {
|
||||||
const newMessages = [...prev];
|
const newMessages = [...prev];
|
||||||
newMessages.splice(toolCallIndex + 1, 0, responseMessage);
|
newMessages[toolCallIndex] = responseMessage;
|
||||||
|
console.log(
|
||||||
|
"[Tool Response] Replaced tool_call with matching tool_id:",
|
||||||
|
chunk.tool_id,
|
||||||
|
"at index:",
|
||||||
|
toolCallIndex,
|
||||||
|
);
|
||||||
return newMessages;
|
return newMessages;
|
||||||
}
|
}
|
||||||
|
console.warn(
|
||||||
|
"[Tool Response] No tool_call found with tool_id:",
|
||||||
|
chunk.tool_id,
|
||||||
|
"appending instead",
|
||||||
|
);
|
||||||
return [...prev, responseMessage];
|
return [...prev, responseMessage];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -191,38 +167,55 @@ export function handleStreamEnd(
|
|||||||
deps: HandlerDependencies,
|
deps: HandlerDependencies,
|
||||||
) {
|
) {
|
||||||
const completedContent = deps.streamingChunksRef.current.join("");
|
const completedContent = deps.streamingChunksRef.current.join("");
|
||||||
if (!completedContent.trim() && !deps.hasResponseRef.current) {
|
// Only save message if there are uncommitted chunks
|
||||||
deps.setMessages((prev) => [
|
// (text_ended already saved if there were tool calls)
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "message",
|
|
||||||
role: "assistant",
|
|
||||||
content: "No response received. Please try again.",
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
if (completedContent.trim()) {
|
if (completedContent.trim()) {
|
||||||
|
console.log(
|
||||||
|
"[Stream End] Saving remaining streamed text as assistant message",
|
||||||
|
);
|
||||||
const assistantMessage: ChatMessageData = {
|
const assistantMessage: ChatMessageData = {
|
||||||
type: "message",
|
type: "message",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: completedContent,
|
content: completedContent,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
deps.setMessages((prev) => [...prev, assistantMessage]);
|
deps.setMessages((prev) => {
|
||||||
|
const updated = [...prev, assistantMessage];
|
||||||
|
console.log("[Stream End] Final state:", {
|
||||||
|
localMessages: updated.map((m) => ({
|
||||||
|
type: m.type,
|
||||||
|
...(m.type === "message" && {
|
||||||
|
role: m.role,
|
||||||
|
contentLength: m.content.length,
|
||||||
|
}),
|
||||||
|
...(m.type === "tool_call" && {
|
||||||
|
toolId: m.toolId,
|
||||||
|
toolName: m.toolName,
|
||||||
|
}),
|
||||||
|
...(m.type === "tool_response" && {
|
||||||
|
toolId: m.toolId,
|
||||||
|
toolName: m.toolName,
|
||||||
|
success: m.success,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
streamingChunks: deps.streamingChunksRef.current,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("[Stream End] No uncommitted chunks, message already saved");
|
||||||
}
|
}
|
||||||
deps.setStreamingChunks([]);
|
deps.setStreamingChunks([]);
|
||||||
deps.streamingChunksRef.current = [];
|
deps.streamingChunksRef.current = [];
|
||||||
deps.setHasTextChunks(false);
|
deps.setHasTextChunks(false);
|
||||||
deps.setIsStreamingInitiated(false);
|
deps.setIsStreamingInitiated(false);
|
||||||
|
console.log("[Stream End] Stream complete, messages in local state");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleError(chunk: StreamChunk, deps: HandlerDependencies) {
|
export function handleError(chunk: StreamChunk, deps: HandlerDependencies) {
|
||||||
const errorMessage = chunk.message || chunk.content || "An error occurred";
|
const errorMessage = chunk.message || chunk.content || "An error occurred";
|
||||||
console.error("Stream error:", errorMessage);
|
console.error("Stream error:", errorMessage);
|
||||||
if (isRegionBlockedError(chunk)) {
|
|
||||||
deps.setIsRegionBlockedModalOpen(true);
|
|
||||||
}
|
|
||||||
deps.setIsStreamingInitiated(false);
|
deps.setIsStreamingInitiated(false);
|
||||||
deps.setHasTextChunks(false);
|
deps.setHasTextChunks(false);
|
||||||
deps.setStreamingChunks([]);
|
deps.setStreamingChunks([]);
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
|
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useChatStream } from "../../useChatStream";
|
import { useChatStream } from "../../useChatStream";
|
||||||
import { usePageContext } from "../../usePageContext";
|
|
||||||
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
||||||
import { createStreamEventDispatcher } from "./createStreamEventDispatcher";
|
import { createStreamEventDispatcher } from "./createStreamEventDispatcher";
|
||||||
import {
|
import {
|
||||||
createUserMessage,
|
createUserMessage,
|
||||||
filterAuthMessages,
|
filterAuthMessages,
|
||||||
hasSentInitialPrompt,
|
|
||||||
isToolCallArray,
|
isToolCallArray,
|
||||||
isValidMessage,
|
isValidMessage,
|
||||||
markInitialPromptSent,
|
|
||||||
parseToolResponse,
|
parseToolResponse,
|
||||||
removePageContext,
|
removePageContext,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
@@ -19,45 +16,20 @@ import {
|
|||||||
interface Args {
|
interface Args {
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
initialMessages: SessionDetailResponse["messages"];
|
initialMessages: SessionDetailResponse["messages"];
|
||||||
initialPrompt?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChatContainer({
|
export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||||
sessionId,
|
|
||||||
initialMessages,
|
|
||||||
initialPrompt,
|
|
||||||
}: Args) {
|
|
||||||
const [messages, setMessages] = useState<ChatMessageData[]>([]);
|
const [messages, setMessages] = useState<ChatMessageData[]>([]);
|
||||||
const [streamingChunks, setStreamingChunks] = useState<string[]>([]);
|
const [streamingChunks, setStreamingChunks] = useState<string[]>([]);
|
||||||
const [hasTextChunks, setHasTextChunks] = useState(false);
|
const [hasTextChunks, setHasTextChunks] = useState(false);
|
||||||
const [isStreamingInitiated, setIsStreamingInitiated] = useState(false);
|
const [isStreamingInitiated, setIsStreamingInitiated] = useState(false);
|
||||||
const [isRegionBlockedModalOpen, setIsRegionBlockedModalOpen] =
|
|
||||||
useState(false);
|
|
||||||
const hasResponseRef = useRef(false);
|
|
||||||
const streamingChunksRef = useRef<string[]>([]);
|
const streamingChunksRef = useRef<string[]>([]);
|
||||||
const previousSessionIdRef = useRef<string | null>(null);
|
const { error, sendMessage: sendStreamMessage } = useChatStream();
|
||||||
const {
|
|
||||||
error,
|
|
||||||
sendMessage: sendStreamMessage,
|
|
||||||
stopStreaming,
|
|
||||||
} = useChatStream();
|
|
||||||
const isStreaming = isStreamingInitiated || hasTextChunks;
|
const isStreaming = isStreamingInitiated || hasTextChunks;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (sessionId !== previousSessionIdRef.current) {
|
|
||||||
stopStreaming(previousSessionIdRef.current ?? undefined, true);
|
|
||||||
previousSessionIdRef.current = sessionId;
|
|
||||||
setMessages([]);
|
|
||||||
setStreamingChunks([]);
|
|
||||||
streamingChunksRef.current = [];
|
|
||||||
setHasTextChunks(false);
|
|
||||||
setIsStreamingInitiated(false);
|
|
||||||
hasResponseRef.current = false;
|
|
||||||
}
|
|
||||||
}, [sessionId, stopStreaming]);
|
|
||||||
|
|
||||||
const allMessages = useMemo(() => {
|
const allMessages = useMemo(() => {
|
||||||
const processedInitialMessages: ChatMessageData[] = [];
|
const processedInitialMessages: ChatMessageData[] = [];
|
||||||
|
// Map to track tool calls by their ID so we can look up tool names for tool responses
|
||||||
const toolCallMap = new Map<string, string>();
|
const toolCallMap = new Map<string, string>();
|
||||||
|
|
||||||
for (const msg of initialMessages) {
|
for (const msg of initialMessages) {
|
||||||
@@ -73,9 +45,13 @@ export function useChatContainer({
|
|||||||
? new Date(msg.timestamp as string)
|
? new Date(msg.timestamp as string)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
// Remove page context from user messages when loading existing sessions
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
content = removePageContext(content);
|
content = removePageContext(content);
|
||||||
if (!content.trim()) continue;
|
// Skip user messages that become empty after removing page context
|
||||||
|
if (!content.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
processedInitialMessages.push({
|
processedInitialMessages.push({
|
||||||
type: "message",
|
type: "message",
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -85,15 +61,19 @@ export function useChatContainer({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle assistant messages first (before tool messages) to build tool call map
|
||||||
if (role === "assistant") {
|
if (role === "assistant") {
|
||||||
|
// Strip <thinking> tags from content
|
||||||
content = content
|
content = content
|
||||||
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "")
|
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
// If assistant has tool calls, create tool_call messages for each
|
||||||
if (toolCalls && isToolCallArray(toolCalls) && toolCalls.length > 0) {
|
if (toolCalls && isToolCallArray(toolCalls) && toolCalls.length > 0) {
|
||||||
for (const toolCall of toolCalls) {
|
for (const toolCall of toolCalls) {
|
||||||
const toolName = toolCall.function.name;
|
const toolName = toolCall.function.name;
|
||||||
const toolId = toolCall.id;
|
const toolId = toolCall.id;
|
||||||
|
// Store tool name for later lookup
|
||||||
toolCallMap.set(toolId, toolName);
|
toolCallMap.set(toolId, toolName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -116,6 +96,7 @@ export function useChatContainer({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Only add assistant message if there's content after stripping thinking tags
|
||||||
if (content.trim()) {
|
if (content.trim()) {
|
||||||
processedInitialMessages.push({
|
processedInitialMessages.push({
|
||||||
type: "message",
|
type: "message",
|
||||||
@@ -125,6 +106,7 @@ export function useChatContainer({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (content.trim()) {
|
} else if (content.trim()) {
|
||||||
|
// Assistant message without tool calls, but with content
|
||||||
processedInitialMessages.push({
|
processedInitialMessages.push({
|
||||||
type: "message",
|
type: "message",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -135,6 +117,7 @@ export function useChatContainer({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle tool messages - look up tool name from tool call map
|
||||||
if (role === "tool") {
|
if (role === "tool") {
|
||||||
const toolCallId = (msg.tool_call_id as string) || "";
|
const toolCallId = (msg.tool_call_id as string) || "";
|
||||||
const toolName = toolCallMap.get(toolCallId) || "unknown";
|
const toolName = toolCallMap.get(toolCallId) || "unknown";
|
||||||
@@ -150,6 +133,7 @@ export function useChatContainer({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle other message types (system, etc.)
|
||||||
if (content.trim()) {
|
if (content.trim()) {
|
||||||
processedInitialMessages.push({
|
processedInitialMessages.push({
|
||||||
type: "message",
|
type: "message",
|
||||||
@@ -170,10 +154,9 @@ export function useChatContainer({
|
|||||||
context?: { url: string; content: string },
|
context?: { url: string; content: string },
|
||||||
) {
|
) {
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
console.error("[useChatContainer] Cannot send message: no session ID");
|
console.error("Cannot send message: no session ID");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsRegionBlockedModalOpen(false);
|
|
||||||
if (isUserMessage) {
|
if (isUserMessage) {
|
||||||
const userMessage = createUserMessage(content);
|
const userMessage = createUserMessage(content);
|
||||||
setMessages((prev) => [...filterAuthMessages(prev), userMessage]);
|
setMessages((prev) => [...filterAuthMessages(prev), userMessage]);
|
||||||
@@ -184,19 +167,14 @@ export function useChatContainer({
|
|||||||
streamingChunksRef.current = [];
|
streamingChunksRef.current = [];
|
||||||
setHasTextChunks(false);
|
setHasTextChunks(false);
|
||||||
setIsStreamingInitiated(true);
|
setIsStreamingInitiated(true);
|
||||||
hasResponseRef.current = false;
|
|
||||||
|
|
||||||
const dispatcher = createStreamEventDispatcher({
|
const dispatcher = createStreamEventDispatcher({
|
||||||
setHasTextChunks,
|
setHasTextChunks,
|
||||||
setStreamingChunks,
|
setStreamingChunks,
|
||||||
streamingChunksRef,
|
streamingChunksRef,
|
||||||
hasResponseRef,
|
|
||||||
setMessages,
|
setMessages,
|
||||||
setIsRegionBlockedModalOpen,
|
|
||||||
sessionId,
|
sessionId,
|
||||||
setIsStreamingInitiated,
|
setIsStreamingInitiated,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendStreamMessage(
|
await sendStreamMessage(
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -206,12 +184,8 @@ export function useChatContainer({
|
|||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[useChatContainer] Failed to send message:", err);
|
console.error("Failed to send message:", err);
|
||||||
setIsStreamingInitiated(false);
|
setIsStreamingInitiated(false);
|
||||||
|
|
||||||
// Don't show error toast for AbortError (expected during cleanup)
|
|
||||||
if (err instanceof Error && err.name === "AbortError") return;
|
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
err instanceof Error ? err.message : "Failed to send message";
|
err instanceof Error ? err.message : "Failed to send message";
|
||||||
toast.error("Failed to send message", {
|
toast.error("Failed to send message", {
|
||||||
@@ -222,63 +196,11 @@ export function useChatContainer({
|
|||||||
[sessionId, sendStreamMessage],
|
[sessionId, sendStreamMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleStopStreaming = useCallback(() => {
|
|
||||||
stopStreaming();
|
|
||||||
setStreamingChunks([]);
|
|
||||||
streamingChunksRef.current = [];
|
|
||||||
setHasTextChunks(false);
|
|
||||||
setIsStreamingInitiated(false);
|
|
||||||
}, [stopStreaming]);
|
|
||||||
|
|
||||||
const { capturePageContext } = usePageContext();
|
|
||||||
|
|
||||||
// Send initial prompt if provided (for new sessions from homepage)
|
|
||||||
useEffect(
|
|
||||||
function handleInitialPrompt() {
|
|
||||||
if (!initialPrompt || !sessionId) return;
|
|
||||||
if (initialMessages.length > 0) return;
|
|
||||||
if (hasSentInitialPrompt(sessionId)) return;
|
|
||||||
|
|
||||||
markInitialPromptSent(sessionId);
|
|
||||||
const context = capturePageContext();
|
|
||||||
sendMessage(initialPrompt, true, context);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
initialPrompt,
|
|
||||||
sessionId,
|
|
||||||
initialMessages.length,
|
|
||||||
sendMessage,
|
|
||||||
capturePageContext,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
async function sendMessageWithContext(
|
|
||||||
content: string,
|
|
||||||
isUserMessage: boolean = true,
|
|
||||||
) {
|
|
||||||
const context = capturePageContext();
|
|
||||||
await sendMessage(content, isUserMessage, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRegionModalOpenChange(open: boolean) {
|
|
||||||
setIsRegionBlockedModalOpen(open);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRegionModalClose() {
|
|
||||||
setIsRegionBlockedModalOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages: allMessages,
|
messages: allMessages,
|
||||||
streamingChunks,
|
streamingChunks,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
error,
|
error,
|
||||||
isRegionBlockedModalOpen,
|
|
||||||
setIsRegionBlockedModalOpen,
|
|
||||||
sendMessageWithContext,
|
|
||||||
handleRegionModalOpenChange,
|
|
||||||
handleRegionModalClose,
|
|
||||||
sendMessage,
|
sendMessage,
|
||||||
stopStreaming: handleStopStreaming,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -9,9 +9,7 @@ import { useChatCredentialsSetup } from "./useChatCredentialsSetup";
|
|||||||
export interface CredentialInfo {
|
export interface CredentialInfo {
|
||||||
provider: string;
|
provider: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
credentialTypes: Array<
|
credentialType: "api_key" | "oauth2" | "user_password" | "host_scoped";
|
||||||
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
|
||||||
>;
|
|
||||||
title: string;
|
title: string;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
}
|
}
|
||||||
@@ -32,7 +30,7 @@ function createSchemaFromCredentialInfo(
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {},
|
properties: {},
|
||||||
credentials_provider: [credential.provider],
|
credentials_provider: [credential.provider],
|
||||||
credentials_types: credential.credentialTypes,
|
credentials_types: [credential.credentialType],
|
||||||
credentials_scopes: credential.scopes,
|
credentials_scopes: credential.scopes,
|
||||||
discriminator: undefined,
|
discriminator: undefined,
|
||||||
discriminator_mapping: undefined,
|
discriminator_mapping: undefined,
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Input } from "@/components/atoms/Input/Input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ArrowUpIcon } from "@phosphor-icons/react";
|
||||||
|
import { useChatInput } from "./useChatInput";
|
||||||
|
|
||||||
|
export interface ChatInputProps {
|
||||||
|
onSend: (message: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatInput({
|
||||||
|
onSend,
|
||||||
|
disabled = false,
|
||||||
|
placeholder = "Type your message...",
|
||||||
|
className,
|
||||||
|
}: ChatInputProps) {
|
||||||
|
const inputId = "chat-input";
|
||||||
|
const { value, setValue, handleKeyDown, handleSend } = useChatInput({
|
||||||
|
onSend,
|
||||||
|
disabled,
|
||||||
|
maxRows: 5,
|
||||||
|
inputId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative flex-1", className)}>
|
||||||
|
<Input
|
||||||
|
id={inputId}
|
||||||
|
label="Chat message input"
|
||||||
|
hideLabel
|
||||||
|
type="textarea"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
rows={1}
|
||||||
|
wrapperClassName="mb-0 relative"
|
||||||
|
className="pr-12"
|
||||||
|
/>
|
||||||
|
<span id="chat-input-hint" className="sr-only">
|
||||||
|
Press Enter to send, Shift+Enter for new line
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={disabled || !value.trim()}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-3 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full",
|
||||||
|
"border border-zinc-800 bg-zinc-800 text-white",
|
||||||
|
"hover:border-zinc-900 hover:bg-zinc-900",
|
||||||
|
"disabled:border-zinc-200 disabled:bg-zinc-200 disabled:text-white disabled:opacity-50",
|
||||||
|
"transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950",
|
||||||
|
"disabled:pointer-events-none",
|
||||||
|
)}
|
||||||
|
aria-label="Send message"
|
||||||
|
>
|
||||||
|
<ArrowUpIcon className="h-3 w-3" weight="bold" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { KeyboardEvent, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface UseChatInputArgs {
|
||||||
|
onSend: (message: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
maxRows?: number;
|
||||||
|
inputId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatInput({
|
||||||
|
onSend,
|
||||||
|
disabled = false,
|
||||||
|
maxRows = 5,
|
||||||
|
inputId = "chat-input",
|
||||||
|
}: UseChatInputArgs) {
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const textarea = document.getElementById(inputId) as HTMLTextAreaElement;
|
||||||
|
if (!textarea) return;
|
||||||
|
textarea.style.height = "auto";
|
||||||
|
const lineHeight = parseInt(
|
||||||
|
window.getComputedStyle(textarea).lineHeight,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const maxHeight = lineHeight * maxRows;
|
||||||
|
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||||
|
textarea.style.height = `${newHeight}px`;
|
||||||
|
textarea.style.overflowY =
|
||||||
|
textarea.scrollHeight > maxHeight ? "auto" : "hidden";
|
||||||
|
}, [value, maxRows, inputId]);
|
||||||
|
|
||||||
|
const handleSend = useCallback(() => {
|
||||||
|
if (disabled || !value.trim()) return;
|
||||||
|
onSend(value.trim());
|
||||||
|
setValue("");
|
||||||
|
const textarea = document.getElementById(inputId) as HTMLTextAreaElement;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = "auto";
|
||||||
|
}
|
||||||
|
}, [value, onSend, disabled, inputId]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
// Shift+Enter allows default behavior (new line) - no need to handle explicitly
|
||||||
|
},
|
||||||
|
[handleSend],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
handleKeyDown,
|
||||||
|
handleSend,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,65 +1,48 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
|
||||||
|
import Avatar, {
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
} from "@/components/atoms/Avatar/Avatar";
|
||||||
import { Button } from "@/components/atoms/Button/Button";
|
import { Button } from "@/components/atoms/Button/Button";
|
||||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
ArrowsClockwiseIcon,
|
ArrowClockwise,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
|
RobotIcon,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
import { getToolActionPhrase } from "../../helpers";
|
||||||
import { AgentCarouselMessage } from "../AgentCarouselMessage/AgentCarouselMessage";
|
import { AgentCarouselMessage } from "../AgentCarouselMessage/AgentCarouselMessage";
|
||||||
import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
|
|
||||||
import { AuthPromptWidget } from "../AuthPromptWidget/AuthPromptWidget";
|
import { AuthPromptWidget } from "../AuthPromptWidget/AuthPromptWidget";
|
||||||
import { ChatCredentialsSetup } from "../ChatCredentialsSetup/ChatCredentialsSetup";
|
import { ChatCredentialsSetup } from "../ChatCredentialsSetup/ChatCredentialsSetup";
|
||||||
import { ExecutionStartedMessage } from "../ExecutionStartedMessage/ExecutionStartedMessage";
|
import { ExecutionStartedMessage } from "../ExecutionStartedMessage/ExecutionStartedMessage";
|
||||||
import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
|
import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
|
||||||
|
import { MessageBubble } from "../MessageBubble/MessageBubble";
|
||||||
import { NoResultsMessage } from "../NoResultsMessage/NoResultsMessage";
|
import { NoResultsMessage } from "../NoResultsMessage/NoResultsMessage";
|
||||||
import { ToolCallMessage } from "../ToolCallMessage/ToolCallMessage";
|
import { ToolCallMessage } from "../ToolCallMessage/ToolCallMessage";
|
||||||
import { ToolResponseMessage } from "../ToolResponseMessage/ToolResponseMessage";
|
import { ToolResponseMessage } from "../ToolResponseMessage/ToolResponseMessage";
|
||||||
import { UserChatBubble } from "../UserChatBubble/UserChatBubble";
|
|
||||||
import { useChatMessage, type ChatMessageData } from "./useChatMessage";
|
import { useChatMessage, type ChatMessageData } from "./useChatMessage";
|
||||||
|
|
||||||
function stripInternalReasoning(content: string): string {
|
|
||||||
const cleaned = content.replace(
|
|
||||||
/<internal_reasoning>[\s\S]*?<\/internal_reasoning>/gi,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
return cleaned.replace(/\n{3,}/g, "\n\n").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayContent(message: ChatMessageData, isUser: boolean): string {
|
|
||||||
if (message.type !== "message") return "";
|
|
||||||
if (isUser) return message.content;
|
|
||||||
return stripInternalReasoning(message.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatMessageProps {
|
export interface ChatMessageProps {
|
||||||
message: ChatMessageData;
|
message: ChatMessageData;
|
||||||
messages?: ChatMessageData[];
|
|
||||||
index?: number;
|
|
||||||
isStreaming?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
onDismissLogin?: () => void;
|
onDismissLogin?: () => void;
|
||||||
onDismissCredentials?: () => void;
|
onDismissCredentials?: () => void;
|
||||||
onSendMessage?: (content: string, isUserMessage?: boolean) => void;
|
onSendMessage?: (content: string, isUserMessage?: boolean) => void;
|
||||||
agentOutput?: ChatMessageData;
|
agentOutput?: ChatMessageData;
|
||||||
isFinalMessage?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessage({
|
export function ChatMessage({
|
||||||
message,
|
message,
|
||||||
messages = [],
|
|
||||||
index = -1,
|
|
||||||
isStreaming = false,
|
|
||||||
className,
|
className,
|
||||||
onDismissCredentials,
|
onDismissCredentials,
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
agentOutput,
|
agentOutput,
|
||||||
isFinalMessage = true,
|
|
||||||
}: ChatMessageProps) {
|
}: ChatMessageProps) {
|
||||||
const { user } = useSupabase();
|
const { user } = useSupabase();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -71,7 +54,14 @@ export function ChatMessage({
|
|||||||
isLoginNeeded,
|
isLoginNeeded,
|
||||||
isCredentialsNeeded,
|
isCredentialsNeeded,
|
||||||
} = useChatMessage(message);
|
} = useChatMessage(message);
|
||||||
const displayContent = getDisplayContent(message, isUser);
|
|
||||||
|
const { data: profile } = useGetV2GetUserProfile({
|
||||||
|
query: {
|
||||||
|
select: (res) => (res.status === 200 ? res.data : null),
|
||||||
|
enabled: isUser && !!user,
|
||||||
|
queryKey: ["/api/store/profile", user?.id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleAllCredentialsComplete = useCallback(
|
const handleAllCredentialsComplete = useCallback(
|
||||||
function handleAllCredentialsComplete() {
|
function handleAllCredentialsComplete() {
|
||||||
@@ -97,25 +87,17 @@ export function ChatMessage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopy = useCallback(
|
const handleCopy = useCallback(async () => {
|
||||||
async function handleCopy() {
|
if (message.type !== "message") return;
|
||||||
if (message.type !== "message") return;
|
|
||||||
if (!displayContent) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(displayContent);
|
await navigator.clipboard.writeText(message.content);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to copy:", error);
|
console.error("Failed to copy:", error);
|
||||||
}
|
}
|
||||||
},
|
}, [message]);
|
||||||
[displayContent, message],
|
|
||||||
);
|
|
||||||
|
|
||||||
function isLongResponse(content: string): boolean {
|
|
||||||
return content.split("\n").length > 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTryAgain = useCallback(() => {
|
const handleTryAgain = useCallback(() => {
|
||||||
if (message.type !== "message" || !onSendMessage) return;
|
if (message.type !== "message" || !onSendMessage) return;
|
||||||
@@ -187,45 +169,9 @@ export function ChatMessage({
|
|||||||
|
|
||||||
// Render tool call messages
|
// Render tool call messages
|
||||||
if (isToolCall && message.type === "tool_call") {
|
if (isToolCall && message.type === "tool_call") {
|
||||||
// Check if this tool call is currently streaming
|
|
||||||
// A tool call is streaming if:
|
|
||||||
// 1. isStreaming is true
|
|
||||||
// 2. This is the last tool_call message
|
|
||||||
// 3. There's no tool_response for this tool call yet
|
|
||||||
const isToolCallStreaming =
|
|
||||||
isStreaming &&
|
|
||||||
index >= 0 &&
|
|
||||||
(() => {
|
|
||||||
// Find the last tool_call index
|
|
||||||
let lastToolCallIndex = -1;
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
|
||||||
if (messages[i].type === "tool_call") {
|
|
||||||
lastToolCallIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check if this is the last tool_call and there's no response yet
|
|
||||||
if (index === lastToolCallIndex) {
|
|
||||||
// Check if there's a tool_response for this tool call
|
|
||||||
const hasResponse = messages
|
|
||||||
.slice(index + 1)
|
|
||||||
.some(
|
|
||||||
(msg) =>
|
|
||||||
msg.type === "tool_response" && msg.toolId === message.toolId,
|
|
||||||
);
|
|
||||||
return !hasResponse;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("px-4 py-2", className)}>
|
<div className={cn("px-4 py-2", className)}>
|
||||||
<ToolCallMessage
|
<ToolCallMessage toolName={message.toolName} />
|
||||||
toolId={message.toolId}
|
|
||||||
toolName={message.toolName}
|
|
||||||
arguments={message.arguments}
|
|
||||||
isStreaming={isToolCallStreaming}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -272,11 +218,27 @@ export function ChatMessage({
|
|||||||
|
|
||||||
// Render tool response messages (but skip agent_output if it's being rendered inside assistant message)
|
// Render tool response messages (but skip agent_output if it's being rendered inside assistant message)
|
||||||
if (isToolResponse && message.type === "tool_response") {
|
if (isToolResponse && message.type === "tool_response") {
|
||||||
|
// Check if this is an agent_output that should be rendered inside assistant message
|
||||||
|
if (message.result) {
|
||||||
|
let parsedResult: Record<string, unknown> | null = null;
|
||||||
|
try {
|
||||||
|
parsedResult =
|
||||||
|
typeof message.result === "string"
|
||||||
|
? JSON.parse(message.result)
|
||||||
|
: (message.result as Record<string, unknown>);
|
||||||
|
} catch {
|
||||||
|
parsedResult = null;
|
||||||
|
}
|
||||||
|
if (parsedResult?.type === "agent_output") {
|
||||||
|
// Skip rendering - this will be rendered inside the assistant message
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("px-4 py-2", className)}>
|
<div className={cn("px-4 py-2", className)}>
|
||||||
<ToolResponseMessage
|
<ToolResponseMessage
|
||||||
toolId={message.toolId}
|
toolName={getToolActionPhrase(message.toolName)}
|
||||||
toolName={message.toolName}
|
|
||||||
result={message.result}
|
result={message.result}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -294,33 +256,40 @@ export function ChatMessage({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full max-w-3xl gap-3">
|
<div className="flex w-full max-w-3xl gap-3">
|
||||||
|
{!isUser && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-indigo-500">
|
||||||
|
<RobotIcon className="h-4 w-4 text-indigo-50" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-w-0 flex-1 flex-col",
|
"flex min-w-0 flex-1 flex-col",
|
||||||
isUser && "items-end",
|
isUser && "items-end",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isUser ? (
|
<MessageBubble variant={isUser ? "user" : "assistant"}>
|
||||||
<UserChatBubble>
|
<MarkdownContent content={message.content} />
|
||||||
<MarkdownContent content={displayContent} />
|
{agentOutput &&
|
||||||
</UserChatBubble>
|
agentOutput.type === "tool_response" &&
|
||||||
) : (
|
!isUser && (
|
||||||
<AIChatBubble>
|
|
||||||
<MarkdownContent content={displayContent} />
|
|
||||||
{agentOutput && agentOutput.type === "tool_response" && (
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<ToolResponseMessage
|
<ToolResponseMessage
|
||||||
toolId={agentOutput.toolId}
|
toolName={
|
||||||
toolName={agentOutput.toolName || "Agent Output"}
|
agentOutput.toolName
|
||||||
|
? getToolActionPhrase(agentOutput.toolName)
|
||||||
|
: "Agent Output"
|
||||||
|
}
|
||||||
result={agentOutput.result}
|
result={agentOutput.result}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AIChatBubble>
|
</MessageBubble>
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-0",
|
"mt-1 flex gap-1",
|
||||||
isUser ? "justify-end" : "justify-start",
|
isUser ? "justify-end" : "justify-start",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -331,25 +300,37 @@ export function ChatMessage({
|
|||||||
onClick={handleTryAgain}
|
onClick={handleTryAgain}
|
||||||
aria-label="Try again"
|
aria-label="Try again"
|
||||||
>
|
>
|
||||||
<ArrowsClockwiseIcon className="size-4 text-zinc-600" />
|
<ArrowClockwise className="size-3 text-neutral-500" />
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!isUser && isFinalMessage && isLongResponse(displayContent) && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={handleCopy}
|
|
||||||
aria-label="Copy message"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<CheckIcon className="size-4 text-green-600" />
|
|
||||||
) : (
|
|
||||||
<CopyIcon className="size-4 text-zinc-600" />
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleCopy}
|
||||||
|
aria-label="Copy message"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<CheckIcon className="size-3 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<CopyIcon className="size-3 text-neutral-500" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isUser && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Avatar className="h-7 w-7">
|
||||||
|
<AvatarImage
|
||||||
|
src={profile?.avatar_url ?? ""}
|
||||||
|
alt={profile?.username ?? "User"}
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="rounded-lg bg-neutral-200 text-neutral-600">
|
||||||
|
{profile?.username?.charAt(0)?.toUpperCase() || "U"}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -41,9 +41,7 @@ export type ChatMessageData =
|
|||||||
credentials: Array<{
|
credentials: Array<{
|
||||||
provider: string;
|
provider: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
credentialTypes: Array<
|
credentialType: "api_key" | "oauth2" | "user_password" | "host_scoped";
|
||||||
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
|
||||||
>;
|
|
||||||
title: string;
|
title: string;
|
||||||
scopes?: string[];
|
scopes?: string[];
|
||||||
}>;
|
}>;
|
||||||