mirror of
https://github.com/slhaf/Partner.git
synced 2026-10-25 02:43:49 +00:00
Compare commits
148
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bb266a1c3 | ||
|
|
9054a9b4ad | ||
|
|
c8d5f577a1 | ||
|
|
7c82c4aea5 | ||
|
|
5491ad1747 | ||
|
|
1be6ed0198 | ||
|
|
01bfc3ee18 | ||
|
|
2d45adf8c3 | ||
|
|
707fddda79 | ||
|
|
68589a21fc | ||
|
|
e8228d0cc1 | ||
|
|
cc99e565f0 | ||
|
|
718271417d | ||
|
|
04ee38726b | ||
|
|
4a00e65868 | ||
|
|
6c3c08b5d5 | ||
|
|
ef5a332f17 | ||
|
|
9acabca40e | ||
|
|
b654090a6e | ||
|
|
c9a7343b30 | ||
|
|
6250b480e0 | ||
|
|
63e8cc314a | ||
|
|
451f83e14d | ||
|
|
2b7e6718d9 | ||
|
|
ed806c668a | ||
|
|
da381c3adf | ||
|
|
1cbff98b36 | ||
|
|
95a30f147c | ||
|
|
0ea8dfc7d9 | ||
|
|
8cb876b532 | ||
|
|
d72b34acfd | ||
|
|
79dcda082f | ||
|
|
fbd1d17fc4 | ||
|
|
a8e3a84db8 | ||
|
|
e6e0eef161 | ||
|
|
4c042769f2 | ||
|
|
7bee14452e | ||
|
|
442397c1f5 | ||
|
|
0b329aa370 | ||
|
|
ffc96bbb64 | ||
|
|
8398c14794 | ||
|
|
bd4d5164d3 | ||
|
|
9073f88117 | ||
|
|
0710f60ed6 | ||
|
|
1bf02f543e | ||
|
|
19f87c93e3 | ||
|
|
828ddff2c2 | ||
|
|
57b23e0dcb | ||
|
|
11f7200e23 | ||
|
|
ee8f568006 | ||
|
|
393dcff6df | ||
|
|
0fdc0038a5 | ||
|
|
97bf0618f4 | ||
|
|
4200df6525 | ||
|
|
686dc76b55 | ||
|
|
adab925e7d | ||
|
|
6c74de2126 | ||
|
|
7fcdb1c671 | ||
|
|
78877f14d5 | ||
|
|
729700ceb7 | ||
|
|
6f48c36f67 | ||
|
|
551a2a16d9 | ||
|
|
40af7a4de6 | ||
|
|
cbdb33fefe | ||
|
|
d2c3416f48 | ||
|
|
c3dc8180f1 | ||
|
|
1564c7d5e1 | ||
|
|
9e33f16aae | ||
|
|
c84d88eab7 | ||
|
|
692c601f17 | ||
|
|
2a8b2aff7b | ||
|
|
afb896e6db | ||
|
|
2b575df3f9 | ||
|
|
db18c818ac | ||
|
|
973875b2e7 | ||
|
|
018b524be3 | ||
|
|
843549dadf | ||
|
|
32f0c5c6a8 | ||
|
|
dd31f6a20d | ||
|
|
178c5138f7 | ||
|
|
f9cdf164f3 | ||
|
|
0c2843a889 | ||
|
|
572bedcd18 | ||
|
|
32ee6b5ed6 | ||
|
|
428f133ac3 | ||
|
|
7aab236221 | ||
|
|
0d456596a8 | ||
|
|
cbd926bd41 | ||
|
|
4a00e5f320 | ||
|
|
690f258dc9 | ||
|
|
d7b271bf71 | ||
|
|
ff80d6bbc4 | ||
|
|
3e73d574df | ||
|
|
9ea475432f | ||
|
|
e66ed9e9c0 | ||
|
|
73f6ff2745 | ||
|
|
3eac52f4e2 | ||
|
|
cf61c171a5 | ||
|
|
822ea82593 | ||
|
|
f957d7caa4 | ||
|
|
9be499eb64 | ||
|
|
184c396752 | ||
|
|
04ceccc46f | ||
|
|
b0185c65c8 | ||
|
|
d43476864c | ||
|
|
e022f134ac | ||
|
|
9144cd90ce | ||
|
|
c23bda430f | ||
|
|
aa98c29744 | ||
|
|
eade39328a | ||
|
|
94adf9a368 | ||
|
|
dd5ab3aaf3 | ||
|
|
075a8ece3e | ||
|
|
4bc2e4adbf | ||
|
|
9240ed44c4 | ||
|
|
878fe4dc10 | ||
|
|
03bae6424e | ||
|
|
a14e123ff6 | ||
|
|
d4277aed33 | ||
|
|
e37dc7bc9b | ||
|
|
51706eb55e | ||
|
|
eb3e5f3f34 | ||
|
|
164078bf42 | ||
|
|
13d04f85e2 | ||
|
|
3a5d0f1cb4 | ||
|
|
aa05d3817b | ||
|
|
45f7d1c51e | ||
|
|
3d56966381 | ||
|
|
de340029c6 | ||
|
|
96101de227 | ||
|
|
2927cb2b6f | ||
|
|
4af8feb178 | ||
|
|
43c2c648ac | ||
|
|
a64c8f6c97 | ||
|
|
406b7ba831 | ||
|
|
9c902e68d0 | ||
|
|
4e894384bf | ||
|
|
a5cee79a8f | ||
|
|
d1da99c44d | ||
|
|
d90c514159 | ||
|
|
ac715602a6 | ||
|
|
e1a5a07e2a | ||
|
|
9108d1db46 | ||
|
|
95a3f782e6 | ||
|
|
6ec3bfcc9b | ||
|
|
b4d6be849b | ||
|
|
a9e41eb4c7 | ||
|
|
7ce7461f79 |
@@ -0,0 +1,137 @@
|
||||
name: Release Partner Core
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Core release tag, for example release-core/0.5.0"
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- "release-core/*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release-core:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Resolve release metadata
|
||||
id: release
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG="${{ inputs.tag }}"
|
||||
else
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
fi
|
||||
|
||||
VERSION="${TAG#release-core/}"
|
||||
VERSION="${VERSION#v}"
|
||||
ENCODED_TAG="${TAG//\//%2F}"
|
||||
ASSET_NAME="partner-core-${VERSION}.jar"
|
||||
ASSET_URL="https://github.com/slhaf/Partner/releases/download/${ENCODED_TAG}/${ASSET_NAME}"
|
||||
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "asset_name=${ASSET_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "asset_url=${ASSET_URL}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Core release tag: ${TAG}"
|
||||
echo "Core release version: ${VERSION}"
|
||||
echo "Core release asset: ${ASSET_NAME}"
|
||||
|
||||
- name: Checkout release source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.release.outputs.tag }}
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Build Partner Core
|
||||
run: |
|
||||
mvn -B -DskipTests=true -pl Partner-Core -am package
|
||||
|
||||
- name: Prepare release artifact
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p dist
|
||||
|
||||
SOURCE_JAR="Partner-Core/target/partner-core-${{ steps.release.outputs.version }}.jar"
|
||||
|
||||
if [ ! -f "$SOURCE_JAR" ]; then
|
||||
echo "Expected artifact not found: $SOURCE_JAR"
|
||||
echo "Available Partner-Core target files:"
|
||||
find Partner-Core/target -maxdepth 1 -type f -name "*.jar" -print
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$SOURCE_JAR" "dist/${{ steps.release.outputs.asset_name }}"
|
||||
ls -lh dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
shell: bash
|
||||
run: |
|
||||
if gh release view "${{ steps.release.outputs.tag }}" >/dev/null 2>&1; then
|
||||
echo "Release ${{ steps.release.outputs.tag }} already exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh release create "${{ steps.release.outputs.tag }}" \
|
||||
"dist/${{ steps.release.outputs.asset_name }}" \
|
||||
--title "Partner Core ${{ steps.release.outputs.version }}" \
|
||||
--notes "Partner Core ${{ steps.release.outputs.version }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Checkout master for registry update
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
path: registry-worktree
|
||||
|
||||
- name: Update latestRelease
|
||||
working-directory: registry-worktree
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
version = "${{ steps.release.outputs.version }}"
|
||||
url = "${{ steps.release.outputs.asset_url }}"
|
||||
|
||||
index_path = Path("registry/index.json")
|
||||
index = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
|
||||
index["partner"]["latestRelease"] = {
|
||||
"url": url,
|
||||
"version": version
|
||||
}
|
||||
|
||||
index_path.write_text(
|
||||
json.dumps(index, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8"
|
||||
)
|
||||
PY
|
||||
|
||||
- name: Commit registry update
|
||||
working-directory: registry-worktree
|
||||
run: |
|
||||
if git diff --quiet registry/index.json; then
|
||||
echo "No latestRelease changes."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add registry/index.json
|
||||
git commit -m "chore(registry): update latest core release to ${{ steps.release.outputs.tag }}"
|
||||
git push origin HEAD:master
|
||||
@@ -0,0 +1,161 @@
|
||||
name: Release PartnerCtl
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Ctl release tag, for example release-ctl/0.5.0"
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- "release-ctl/*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.platform }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux-x64
|
||||
runner: ubuntu-latest
|
||||
binaryPath: PartnerCtl/target/partnerctl
|
||||
assetSuffix: linux-x64
|
||||
- platform: linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
binaryPath: PartnerCtl/target/partnerctl
|
||||
assetSuffix: linux-arm64
|
||||
- platform: windows-x64
|
||||
runner: windows-latest
|
||||
binaryPath: PartnerCtl/target/partnerctl.exe
|
||||
assetSuffix: windows-x64.exe
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
steps:
|
||||
- name: Resolve release metadata
|
||||
id: release
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG="${{ inputs.tag }}"
|
||||
else
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
fi
|
||||
|
||||
VERSION="${TAG#release-ctl/}"
|
||||
VERSION="${VERSION#v}"
|
||||
ASSET_NAME="partnerctl-${VERSION}-${{ matrix.assetSuffix }}"
|
||||
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "asset_name=${ASSET_NAME}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Ctl release tag: ${TAG}"
|
||||
echo "Ctl release version: ${VERSION}"
|
||||
echo "Ctl release asset: ${ASSET_NAME}"
|
||||
|
||||
- name: Checkout release source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.release.outputs.tag }}
|
||||
|
||||
- name: Set up GraalVM 21
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
distribution: graalvm-community
|
||||
java-version: "21"
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Build partnerctl native image
|
||||
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
|
||||
|
||||
- name: Prepare release artifact
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p dist
|
||||
|
||||
BINARY_PATH="${{ matrix.binaryPath }}"
|
||||
ASSET_PATH="dist/${{ steps.release.outputs.asset_name }}"
|
||||
|
||||
if [ ! -f "$BINARY_PATH" ]; then
|
||||
echo "Expected native binary not found: $BINARY_PATH"
|
||||
echo "Available PartnerCtl target files:"
|
||||
find PartnerCtl/target -maxdepth 2 -type f -print
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$BINARY_PATH" "$ASSET_PATH"
|
||||
|
||||
if [[ "${{ matrix.platform }}" == linux-* ]]; then
|
||||
chmod +x "$ASSET_PATH"
|
||||
fi
|
||||
|
||||
ls -lh dist
|
||||
|
||||
- name: Upload release artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.release.outputs.asset_name }}
|
||||
path: dist/${{ steps.release.outputs.asset_name }}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Resolve release metadata
|
||||
id: release
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
TAG="${{ inputs.tag }}"
|
||||
else
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
fi
|
||||
|
||||
VERSION="${TAG#release-ctl/}"
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Ctl release tag: ${TAG}"
|
||||
echo "Ctl release version: ${VERSION}"
|
||||
|
||||
- name: Checkout release source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.release.outputs.tag }}
|
||||
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Show release artifacts
|
||||
run: |
|
||||
ls -lh dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
shell: bash
|
||||
run: |
|
||||
if gh release view "${{ steps.release.outputs.tag }}" >/dev/null 2>&1; then
|
||||
echo "Release ${{ steps.release.outputs.tag }} already exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh release create "${{ steps.release.outputs.tag }}" \
|
||||
dist/* \
|
||||
--title "PartnerCtl ${{ steps.release.outputs.version }}" \
|
||||
--notes "PartnerCtl ${{ steps.release.outputs.version }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Sync from Gitea
|
||||
|
||||
# 1. 给 GITHUB_TOKEN 开写权限
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/30 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 配置 Git 用户
|
||||
run: |
|
||||
git config --global user.name "Gitea Sync Bot"
|
||||
git config --global user.email "slhafzjw@slhaf.work"
|
||||
|
||||
- name: 关闭全局 SSL 校验
|
||||
run: git config --global http.sslVerify false
|
||||
|
||||
- name: Clone from Gitea (mirror)
|
||||
run: |
|
||||
git clone --mirror \
|
||||
https://${{ secrets.GITEA_USER }}:${{ secrets.GITEA_TOKEN }}@${{ secrets.GITEA_URL }} \
|
||||
gitea-mirror
|
||||
|
||||
- name: Push to GitHub
|
||||
run: |
|
||||
cd gitea-mirror
|
||||
# 明确推到名为 "github" 的 remote
|
||||
git remote add github \
|
||||
https://${{ github.repository_owner }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
git push --mirror github
|
||||
@@ -0,0 +1,137 @@
|
||||
name: Test PartnerCtl Native Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Target platform to test"
|
||||
required: true
|
||||
default: "windows-x64"
|
||||
type: choice
|
||||
options:
|
||||
- windows-x64
|
||||
- linux-x64
|
||||
- linux-arm64
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-linux-x64:
|
||||
name: Test linux-x64
|
||||
if: ${{ inputs.platform == 'linux-x64' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up GraalVM 21
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
distribution: graalvm-community
|
||||
java-version: "21"
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Build partnerctl native image
|
||||
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
|
||||
|
||||
- name: Check Linux output
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -f "PartnerCtl/target/partnerctl" ]; then
|
||||
echo "Expected native binary not found: PartnerCtl/target/partnerctl"
|
||||
echo "Available PartnerCtl target files:"
|
||||
find PartnerCtl/target -maxdepth 2 -type f -print
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x PartnerCtl/target/partnerctl
|
||||
./PartnerCtl/target/partnerctl --help
|
||||
|
||||
- name: Upload test artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: partnerctl-linux-x64-test
|
||||
path: PartnerCtl/target/partnerctl
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
test-linux-arm64:
|
||||
name: Test linux-arm64
|
||||
if: ${{ inputs.platform == 'linux-arm64' }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up GraalVM 21
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
distribution: graalvm-community
|
||||
java-version: "21"
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Build partnerctl native image
|
||||
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
|
||||
|
||||
- name: Check Linux output
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -f "PartnerCtl/target/partnerctl" ]; then
|
||||
echo "Expected native binary not found: PartnerCtl/target/partnerctl"
|
||||
echo "Available PartnerCtl target files:"
|
||||
find PartnerCtl/target -maxdepth 2 -type f -print
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x PartnerCtl/target/partnerctl
|
||||
./PartnerCtl/target/partnerctl --help
|
||||
|
||||
- name: Upload test artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: partnerctl-linux-arm64-test
|
||||
path: PartnerCtl/target/partnerctl
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
test-windows-x64:
|
||||
name: Test windows-x64
|
||||
if: ${{ inputs.platform == 'windows-x64' }}
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up GraalVM 21
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
distribution: graalvm-community
|
||||
java-version: "21"
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Build partnerctl native image
|
||||
run: mvn -B -DskipTests=true -pl PartnerCtl -am package native:compile
|
||||
|
||||
- name: Check Windows output
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (!(Test-Path "PartnerCtl/target/partnerctl.exe")) {
|
||||
Write-Host "Expected native binary not found: PartnerCtl/target/partnerctl.exe"
|
||||
Write-Host "Available PartnerCtl target files:"
|
||||
Get-ChildItem -Recurse PartnerCtl/target | Select-Object FullName
|
||||
throw "partnerctl.exe not found"
|
||||
}
|
||||
|
||||
.\PartnerCtl\target\partnerctl.exe --help
|
||||
|
||||
- name: Upload test artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: partnerctl-windows-x64-test
|
||||
path: PartnerCtl/target/partnerctl.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Update Latest Buildable
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Buildable ref, for example buildable/v0.5.0"
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- "buildable/*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-latest-buildable:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Resolve buildable ref
|
||||
id: buildable
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "ref=${{ inputs.ref }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Update latestBuildable
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ref = "${{ steps.buildable.outputs.ref }}"
|
||||
index_path = Path("registry/index.json")
|
||||
|
||||
index = json.loads(index_path.read_text(encoding="utf-8"))
|
||||
|
||||
index["partner"]["latestBuildable"] = {
|
||||
"url": "https://github.com/slhaf/Partner.git",
|
||||
"ref": ref
|
||||
}
|
||||
|
||||
index_path.write_text(
|
||||
json.dumps(index, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8"
|
||||
)
|
||||
PY
|
||||
|
||||
- name: Commit registry update
|
||||
run: |
|
||||
if git diff --quiet registry/index.json; then
|
||||
echo "No latestBuildable changes."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add registry/index.json
|
||||
git commit -m "chore(registry): update latest buildable to ${{ steps.buildable.outputs.ref }}"
|
||||
git push
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Update Module Index
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "registry/modules/*.json"
|
||||
- "registry/index.json"
|
||||
- "scripts/update_module_index.py"
|
||||
- "update-module-index.yml"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-registry-index:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update module index
|
||||
run: python3 scripts/update_module_index.py
|
||||
|
||||
- name: Commit updated index
|
||||
run: |
|
||||
if git diff --quiet registry/index.json; then
|
||||
echo "No module index changes"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
git add registry/index.json
|
||||
git commit -m "chore: update registry index"
|
||||
git push
|
||||
+4
-6
@@ -4,12 +4,7 @@ target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
.idea/db-forest-config.xml
|
||||
.idea/markdown.xml
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
@@ -63,3 +58,6 @@ build/
|
||||
/Partner-Core/data/
|
||||
/.ai/mcp/mcp.json
|
||||
/.codex
|
||||
|
||||
# Maven / build outputs
|
||||
dependency-reduced-pom.xml
|
||||
Generated
+3
@@ -1,7 +1,10 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>Onebot</w>
|
||||
<w>onebot</w>
|
||||
<w>openai</w>
|
||||
<w>partnerctl</w>
|
||||
<w>zuper</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
||||
Generated
+8
-5
@@ -1,17 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/Partner-Common/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Common/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/java/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Core/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-External-Modules/Partner-Onebot-Adapter/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-External-Modules/Partner-Onebot-Adapter/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-External-Modules/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-External-Modules/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Framework/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Framework/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-SandboxRunner/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Test-Demo/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Test-Demo/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Interaction-Api/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/Partner-Interaction-Api/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/PartnerCtl/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/PartnerCtl/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
|
||||
Generated
+23
-18
@@ -1,26 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<list size="19">
|
||||
<list size="22">
|
||||
<item index="0" class="java.lang.String" itemvalue="lombok.Data" />
|
||||
<item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" />
|
||||
<item index="2" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
|
||||
<item index="3" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
|
||||
<item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
|
||||
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
|
||||
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
|
||||
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
|
||||
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
|
||||
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
|
||||
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
|
||||
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
|
||||
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
|
||||
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
|
||||
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
|
||||
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
|
||||
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
|
||||
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
|
||||
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" />
|
||||
<item index="2" class="java.lang.String" itemvalue="picocli.CommandLine.Command" />
|
||||
<item index="3" class="java.lang.String" itemvalue="picocli.CommandLine.Mixin" />
|
||||
<item index="4" class="java.lang.String" itemvalue="picocli.CommandLine.Option" />
|
||||
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
|
||||
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
|
||||
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
|
||||
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
|
||||
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
|
||||
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
|
||||
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
|
||||
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
|
||||
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
|
||||
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
|
||||
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
|
||||
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
|
||||
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
|
||||
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
|
||||
<item index="19" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
|
||||
<item index="20" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
|
||||
<item index="21" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" />
|
||||
</list>
|
||||
<writeAnnotations>
|
||||
<writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" />
|
||||
@@ -40,6 +43,8 @@
|
||||
</option>
|
||||
<option name="ignoredFiles">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/Partner-Common/pom.xml" />
|
||||
<option value="$PROJECT_DIR$/Partner-SandboxRunner/pom.xml" />
|
||||
<option value="$PROJECT_DIR$/Partner-Test-Demo/pom.xml" />
|
||||
</set>
|
||||
</option>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>Partner-Common</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/io.modelcontextprotocol.sdk/mcp -->
|
||||
<dependency>
|
||||
<groupId>io.modelcontextprotocol.sdk</groupId>
|
||||
<artifactId>mcp</artifactId>
|
||||
<version>0.17.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
@@ -1,44 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>Partner</artifactId>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<version>0.5.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>Partner-Main</artifactId>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>work.slhaf.partner.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<properties>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>partner</artifactId>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<version>0.5.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>partner-core</artifactId>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>work.slhaf.partner.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>process-sources</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>${project.basedir}/src/main/java</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>${project.basedir}/src/test/java</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<jvmTarget>${maven.compiler.target}</jvmTarget>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test</artifactId>
|
||||
<version>2.2.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
</project>
|
||||
|
||||
+8
-13
@@ -4,12 +4,13 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>Partner-Core</artifactId>
|
||||
<artifactId>partner-core</artifactId>
|
||||
<version>0.9.0-preview</version>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -18,9 +19,9 @@
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner-Framework</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner-framework</artifactId>
|
||||
<version>${partner.runtime.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.nd4j/nd4j-api -->
|
||||
<dependency>
|
||||
@@ -44,12 +45,6 @@
|
||||
<artifactId>mcp</artifactId>
|
||||
<version>0.17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner-Common</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package work.slhaf.partner;
|
||||
|
||||
import work.slhaf.partner.common.vector.VectorClientRegistry;
|
||||
import work.slhaf.partner.framework.agent.Agent;
|
||||
import work.slhaf.partner.runtime.gateway.WebSocketGatewayRegistration;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
Agent.newAgent(Main.class)
|
||||
.addGatewayRegistration(WebSocketGatewayRegistration.INSTANCE)
|
||||
.addConfigurable(new VectorClientRegistry())
|
||||
.launch();
|
||||
boolean launched = Agent.newAgent(Main.class).launch();
|
||||
if (!launched) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,13 +229,20 @@ public class ActionCore implements StateSerializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在未进入执行阶段的行动单元组新增新的行动
|
||||
* 在指定阶段之后追加新的行动单元组
|
||||
*/
|
||||
private void handleAppend(ExecutableAction executableAction, int order, List<MetaAction> actions) {
|
||||
if (order <= executableAction.getExecutingStage())
|
||||
return;
|
||||
Map<Integer, List<MetaAction>> actionChain = executableAction.getActionChain();
|
||||
int targetOrder = nextAvailableOrder(actionChain, order + 1);
|
||||
actionChain.put(targetOrder, actions);
|
||||
}
|
||||
|
||||
executableAction.getActionChain().put(order, actions);
|
||||
private int nextAvailableOrder(Map<Integer, List<MetaAction>> actionChain, int startOrder) {
|
||||
int order = startOrder;
|
||||
while (actionChain.containsKey(order)) {
|
||||
order++;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+18
-2
@@ -76,6 +76,10 @@ final class ActionPoolStateCodec {
|
||||
.<StateValue>map(entry -> {
|
||||
Map<String, StateValue> stageMap = new LinkedHashMap<>();
|
||||
stageMap.put("stage", StateValue.num(entry.getKey()));
|
||||
String stageDescription = action.getStageDescriptions().get(entry.getKey());
|
||||
if (stageDescription != null && !stageDescription.isBlank()) {
|
||||
stageMap.put("description", StateValue.str(stageDescription));
|
||||
}
|
||||
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
|
||||
.map(metaAction -> (StateValue) encodeMetaAction(metaAction))
|
||||
.toList()));
|
||||
@@ -126,7 +130,11 @@ final class ActionPoolStateCodec {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<Integer, List<MetaAction>> restoredChain = decodeActionChain(actionObject.getJSONArray("action_chain"));
|
||||
Map<Integer, String> restoredStageDescriptions = new LinkedHashMap<>();
|
||||
Map<Integer, List<MetaAction>> restoredChain = decodeActionChain(
|
||||
actionObject.getJSONArray("action_chain"),
|
||||
restoredStageDescriptions
|
||||
);
|
||||
ExecutableAction executableAction;
|
||||
if ("schedulable".equals(kind)) {
|
||||
String scheduleType = actionObject.getString("schedule_type");
|
||||
@@ -173,11 +181,15 @@ final class ActionPoolStateCodec {
|
||||
if (result != null) {
|
||||
executableAction.setResult(result);
|
||||
}
|
||||
executableAction.getStageDescriptions().putAll(restoredStageDescriptions);
|
||||
executableAction.getHistory().putAll(decodeHistory(actionObject.getJSONArray("history")));
|
||||
return executableAction;
|
||||
}
|
||||
|
||||
private static Map<Integer, List<MetaAction>> decodeActionChain(@Nullable JSONArray actionChainArray) {
|
||||
private static Map<Integer, List<MetaAction>> decodeActionChain(
|
||||
@Nullable JSONArray actionChainArray,
|
||||
Map<Integer, String> stageDescriptions
|
||||
) {
|
||||
Map<Integer, List<MetaAction>> restored = new LinkedHashMap<>();
|
||||
if (actionChainArray == null) {
|
||||
return toMutableActionChain(restored);
|
||||
@@ -188,10 +200,14 @@ final class ActionPoolStateCodec {
|
||||
continue;
|
||||
}
|
||||
Integer stage = stageObject.getInteger("stage");
|
||||
String description = stageObject.getString("description");
|
||||
JSONArray actions = stageObject.getJSONArray("actions");
|
||||
if (stage == null || actions == null) {
|
||||
continue;
|
||||
}
|
||||
if (description != null && !description.isBlank()) {
|
||||
stageDescriptions.put(stage, description);
|
||||
}
|
||||
List<MetaAction> metaActions = new ArrayList<>();
|
||||
for (int j = 0; j < actions.size(); j++) {
|
||||
JSONObject actionObject = actions.getJSONObject(j);
|
||||
|
||||
@@ -96,6 +96,11 @@ sealed class ExecutableAction(
|
||||
*/
|
||||
abstract val actionChain: MutableMap<Int, MutableList<MetaAction>>
|
||||
|
||||
/**
|
||||
* 行动阶段描述,用于为每个 stage 提供可落地的执行目标说明
|
||||
*/
|
||||
val stageDescriptions: MutableMap<Int, String> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* 行动阶段(当前阶段)
|
||||
*/
|
||||
@@ -143,6 +148,7 @@ sealed class ExecutableAction(
|
||||
status = status,
|
||||
tendency = tendency,
|
||||
actionChainSize = actionChain.size,
|
||||
stageDescriptions = stageDescriptions.toMap(),
|
||||
executingStage = executingStage,
|
||||
result = result,
|
||||
history = history.mapValues { (_, value) -> value.toList() },
|
||||
@@ -268,6 +274,7 @@ data class ExecutableActionSnapshot(
|
||||
|
||||
val tendency: String,
|
||||
val actionChainSize: Int,
|
||||
val stageDescriptions: Map<Int, String>,
|
||||
val executingStage: Int,
|
||||
val result: String?,
|
||||
val history: Map<Int, List<HistoryAction>>,
|
||||
|
||||
-1
@@ -11,7 +11,6 @@ import io.modelcontextprotocol.spec.McpSchema;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
|
||||
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package work.slhaf.partner.common.mcp;
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import io.modelcontextprotocol.common.McpTransportContext;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
-1
@@ -12,7 +12,6 @@ import javassist.NotFoundException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
-2
@@ -1,7 +1,5 @@
|
||||
package work.slhaf.partner.core.action.runner.mcp;
|
||||
|
||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ public class CognitionCore implements StateSerializable {
|
||||
new BlockContent("recent_chat_messages", "communication_producer") {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
document.appendChild(document.importNode(messageNotesElement(), true));
|
||||
root.appendChild(document.importNode(messageNotesElement(), true));
|
||||
Element chatMessagesElement = document.createElement("chat_messages");
|
||||
root.appendChild(chatMessagesElement);
|
||||
appendRepeatedElements(document, chatMessagesElement, "chat_message", resolveRecentChatMessages(), (messageElement, message) -> {
|
||||
|
||||
@@ -4,10 +4,8 @@ import com.alibaba.fastjson2.JSONObject
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import work.slhaf.partner.common.base.Block
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter
|
||||
import work.slhaf.partner.framework.agent.log.TraceEvent
|
||||
import work.slhaf.partner.framework.agent.log.TraceRecorder
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@@ -18,11 +16,6 @@ import kotlin.math.min
|
||||
|
||||
class ContextWorkspace {
|
||||
|
||||
private val tracePath: Path = ConfigCenter.paths.stateDir
|
||||
.resolve("trace")
|
||||
.resolve("context-workspace")
|
||||
.normalize()
|
||||
.toAbsolutePath()
|
||||
private val stateSet = mutableSetOf<ContextBlock>()
|
||||
private val lock = ReentrantReadWriteLock()
|
||||
|
||||
@@ -103,7 +96,7 @@ class ContextWorkspace {
|
||||
* @param contextBlock 注册的新上下文块
|
||||
*/
|
||||
fun register(contextBlock: ContextBlock) = lock.write {
|
||||
val removedBlocks = mutableListOf<ContextBlock>()
|
||||
val changedSourceKeys = linkedSetOf<ContextBlock.SourceKey>()
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val currentBlock = iterator.next()
|
||||
@@ -113,44 +106,51 @@ class ContextWorkspace {
|
||||
|
||||
if (currentBlock.applyReplaceFade() <= 0.0) {
|
||||
iterator.remove()
|
||||
removedBlocks.add(currentBlock)
|
||||
changedSourceKeys.add(currentBlock.sourceKey)
|
||||
}
|
||||
}
|
||||
stateSet += contextBlock
|
||||
recordRegister(contextBlock, removedBlocks)
|
||||
changedSourceKeys.add(contextBlock.sourceKey)
|
||||
record("register", changedSourceKeys)
|
||||
}
|
||||
|
||||
fun expire(blockName: String, source: String) = lock.write {
|
||||
val sourceKey = ContextBlock.SourceKey(blockName, source)
|
||||
val removedBlocks = mutableListOf<ContextBlock>()
|
||||
val changedSourceKeys = linkedSetOf<ContextBlock.SourceKey>()
|
||||
val iterator = stateSet.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val block = iterator.next()
|
||||
if (block.sourceKey == sourceKey) {
|
||||
iterator.remove()
|
||||
removedBlocks.add(block)
|
||||
changedSourceKeys.add(block.sourceKey)
|
||||
}
|
||||
}
|
||||
if (removedBlocks.isNotEmpty()) {
|
||||
recordExpire(sourceKey, removedBlocks)
|
||||
if (changedSourceKeys.isNotEmpty()) {
|
||||
record("expire", changedSourceKeys)
|
||||
}
|
||||
}
|
||||
|
||||
private fun recordRegister(addedBlock: ContextBlock, removedBlocks: List<ContextBlock>) {
|
||||
private fun record(action: String, changedSourceKeys: Set<ContextBlock.SourceKey>) {
|
||||
val payload = JSONObject()
|
||||
payload["action"] = "register"
|
||||
payload["added"] = blockSnapshot(addedBlock)
|
||||
payload["removed"] = removedBlocks.map(::blockSnapshot)
|
||||
TraceRecorder.record(TraceEvent(tracePath, payload))
|
||||
}
|
||||
|
||||
private fun recordExpire(sourceKey: ContextBlock.SourceKey, removedBlocks: List<ContextBlock>) {
|
||||
val payload = JSONObject()
|
||||
payload["action"] = "expire"
|
||||
payload["blockName"] = sourceKey.blockName
|
||||
payload["source"] = sourceKey.source
|
||||
payload["removed"] = removedBlocks.map(::blockSnapshot)
|
||||
TraceRecorder.record(TraceEvent(tracePath, payload))
|
||||
payload["action"] = action
|
||||
payload["changedSourceKeys"] = changedSourceKeys
|
||||
.sortedWith(
|
||||
compareBy<ContextBlock.SourceKey> { it.blockName }
|
||||
.thenBy { it.source }
|
||||
)
|
||||
.map { sourceKey ->
|
||||
JSONObject.of(
|
||||
"blockName", sourceKey.blockName,
|
||||
"source", sourceKey.source
|
||||
)
|
||||
}
|
||||
payload["blocks"] = stateSet
|
||||
.sortedWith(
|
||||
compareBy<ContextBlock> { it.sourceKey.blockName }
|
||||
.thenBy { it.sourceKey.source }
|
||||
)
|
||||
.map(::blockSnapshot)
|
||||
TraceRecorder.record(TraceEvent("context-workspace", payload))
|
||||
}
|
||||
|
||||
private fun blockSnapshot(block: ContextBlock): JSONObject {
|
||||
@@ -176,7 +176,7 @@ private data class ResolvedContextBlock(
|
||||
data class ContextBlock @JvmOverloads constructor(
|
||||
val blockContent: BlockContent,
|
||||
val compactBlock: BlockContent = blockContent,
|
||||
val abstractBlock: BlockContent = blockContent,
|
||||
val abstractBlock: BlockContent = compactBlock,
|
||||
/**
|
||||
* 该 block 集中在哪些域
|
||||
*/
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package work.slhaf.partner.module
|
||||
|
||||
import work.slhaf.partner.common.base.Block
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message
|
||||
|
||||
abstract class TaskBlock @JvmOverloads constructor(
|
||||
blockName: String = "task_input"
|
||||
) : Block(blockName) {
|
||||
|
||||
fun encodeToMessage(): Message {
|
||||
return Message(Message.Character.USER, encodeToXmlString())
|
||||
}
|
||||
|
||||
}
|
||||
+6
-1
@@ -2,8 +2,13 @@ package work.slhaf.partner.module.action.builtin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
interface BuiltinActionProvider {
|
||||
public interface BuiltinActionProvider {
|
||||
List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions();
|
||||
|
||||
String createActionKey(String actionName);
|
||||
|
||||
default String param(String requirement, String type, String detail) {
|
||||
return requirement + "|" + type + "|" + detail;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-24
@@ -9,7 +9,6 @@ import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCa
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -26,32 +25,26 @@ public class BuiltinActionRegistry extends AbstractAgentModule.Standalone {
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
definitions.clear();
|
||||
for (BuiltinActionDefinition definition : buildDefaultActionDefinitions()) {
|
||||
definitions.put(definition.actionKey(), definition);
|
||||
}
|
||||
actionCapability.registerMetaActions(exportMetaActionInfos());
|
||||
actionCapability.runnerClient().setBuiltinActionRegistry(this);
|
||||
}
|
||||
|
||||
protected List<BuiltinActionDefinition> buildDefaultActionDefinitions() {
|
||||
List<BuiltinActionDefinition> builtinActionDefinitions = new ArrayList<>();
|
||||
BuiltinActionProvider commandActionProvider = new BuiltinCommandActionProvider();
|
||||
BuiltinActionProvider capabilityActionProvider = new BuiltinCapabilityActionProvider();
|
||||
BuiltinActionProvider interventionActionProvider = new BuiltinInterventionActionProvider();
|
||||
BuiltinActionProvider dynamicActionProvider = new BuiltinDynamicActionProvider();
|
||||
|
||||
builtinActionDefinitions.addAll(commandActionProvider.provideBuiltinActions());
|
||||
builtinActionDefinitions.addAll(capabilityActionProvider.provideBuiltinActions());
|
||||
builtinActionDefinitions.addAll(interventionActionProvider.provideBuiltinActions());
|
||||
builtinActionDefinitions.addAll(dynamicActionProvider.provideBuiltinActions());
|
||||
|
||||
return builtinActionDefinitions;
|
||||
public void register(BuiltinActionProvider provider) {
|
||||
List<BuiltinActionDefinition> builtinActionDefinitions = provider.provideBuiltinActions();
|
||||
if (builtinActionDefinitions == null || builtinActionDefinitions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, MetaActionInfo> metaActionInfos = new LinkedHashMap<>();
|
||||
for (BuiltinActionDefinition definition : builtinActionDefinitions) {
|
||||
definitions.put(definition.actionKey(), definition);
|
||||
metaActionInfos.put(definition.actionKey(), definition.metaActionInfo());
|
||||
}
|
||||
actionCapability.registerMetaActions(metaActionInfos);
|
||||
}
|
||||
|
||||
public void defineBuiltinAction(String name, MetaActionInfo metaActionInfo, Function<Map<String, Object>, String> invoker) {
|
||||
BuiltinActionDefinition definition = new BuiltinActionDefinition(BUILTIN_LOCATION + "::" + name, metaActionInfo, invoker);
|
||||
definitions.put(definition.actionKey(), definition);
|
||||
actionCapability.registerMetaActions(Map.of(definition.actionKey(), definition.metaActionInfo()));
|
||||
}
|
||||
|
||||
public String call(@NonNull String actionKey, @NonNull Map<String, Object> params) {
|
||||
@@ -70,11 +63,6 @@ public class BuiltinActionRegistry extends AbstractAgentModule.Standalone {
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, MetaActionInfo> exportMetaActionInfos() {
|
||||
Map<String, MetaActionInfo> metaActions = new LinkedHashMap<>();
|
||||
definitions.forEach((key, value) -> metaActions.put(key, value.metaActionInfo()));
|
||||
return metaActions;
|
||||
}
|
||||
|
||||
public record BuiltinActionDefinition(
|
||||
String actionKey,
|
||||
|
||||
+15
-7
@@ -14,6 +14,8 @@ import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.util.*;
|
||||
@@ -32,6 +34,13 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
|
||||
private CognitionCapability cognitionCapability;
|
||||
@InjectCapability
|
||||
private MemoryCapability memoryCapability;
|
||||
@InjectModule
|
||||
private BuiltinActionRegistry builtinActionRegistry;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
builtinActionRegistry.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
@@ -49,11 +58,10 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"unit_id", "The id of the memory unit that contains the target memory slice.",
|
||||
"slice_id", "The id of the memory slice to recall into context."
|
||||
"unit_id", param("required", "string", "Memory unit id that contains the target memory slice."),
|
||||
"slice_id", param("required", "string", "Memory slice id to load into short-lived context.")
|
||||
),
|
||||
"Recall the target memory slice into context using its unit_id and slice_id. " +
|
||||
"This action loads the slice's original conversation messages as a short-lived recalled memory context block.",
|
||||
"Purpose: bring a known memory slice back into the agent's working context so later reasoning can use the original conversation messages. Use when: the exact memory unit and slice have already been identified. Input: memory slice id and memory unit id. Does not: search, rank, summarize, or infer memories. Returns: whether the slice was recalled and a short result message.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
@@ -125,10 +133,10 @@ class BuiltinCapabilityActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"input", "Input required to initiate an internal Agent Turn.",
|
||||
"target", "The people expected to reply to this internal Agent Turn."
|
||||
"input", param("required", "string", "Prompt content for the internal Agent Turn."),
|
||||
"target", param("required", "string", "Target user expected to receive/respond to the turn.")
|
||||
),
|
||||
"Create an internal Agent Turn to resolve a task.",
|
||||
"Purpose: initiate a self-dialogue or proactive dialogue by sending input to a target participant. Use when: the agent needs to start a new turn for reflection, clarification, coordination, or active communication. Input: input content and the user expected to respond. Returns: confirmation that the turn was initiated. Does not: wait for or include the target's answer.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
|
||||
+68
-37
@@ -6,21 +6,25 @@ import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.Duration;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION;
|
||||
|
||||
@AgentComponent
|
||||
class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
|
||||
private static final String COMMAND_LOCATION = BUILTIN_LOCATION + "::" + "command";
|
||||
@@ -38,6 +42,30 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
private final ConcurrentHashMap<String, CommandHandle> commandHandles = new ConcurrentHashMap<>();
|
||||
private final CommandExecutionService commandExecutionService = CommandExecutionService.INSTANCE;
|
||||
|
||||
@InjectModule
|
||||
private BuiltinActionRegistry builtinActionRegistry;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
builtinActionRegistry.register(this);
|
||||
}
|
||||
|
||||
private Map<String, String> commandParams() {
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
params.put("arg", param("required", "string", "Command executable name or path."));
|
||||
params.put("arg1", param("optional", "string", "First command argument after the executable."));
|
||||
params.put("arg2", param("optional", "string", "Second command argument after the executable."));
|
||||
params.put("argN", param("optional", "string", "Additional command arguments after arg2. Use consecutive numeric keys such as arg3, arg4, arg5, ... when more arguments are needed."));
|
||||
return params;
|
||||
}
|
||||
|
||||
private Map<String, String> commandSessionParams() {
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
params.put("desc", param("required", "string", "Short human-readable description of why this background command session exists."));
|
||||
params.putAll(commandParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
return List.of(
|
||||
@@ -61,8 +89,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("arg / argN", "Command arguments. Use arg for first argument, arg1/arg2... for remaining arguments."),
|
||||
"Execute any allowed system commands and get result instantly, the number of arguments is not limited.",
|
||||
commandParams(),
|
||||
"Purpose: execute a short, non-interactive system command synchronously and return its completed output. Inputs: argv entries through arg/argN. Returns: JSON with result containing stdout/stderr/exit summary text. Use when: the task can finish quickly in one process call. Notes: do not use for long-running services, interactive programs, streaming output, or processes that need later inspection.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("inspect")),
|
||||
@@ -92,11 +120,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"desc", "Command session description.",
|
||||
"arg / argN", "Command arguments. Use arg for first argument, arg1/arg2... for remaining arguments."
|
||||
),
|
||||
"Start a background command session and return execution id.",
|
||||
commandSessionParams(),
|
||||
"Purpose: start a long-running or streaming command as a background session. Inputs: session description and argument entries. Returns: JSON with executionId; pass it as id to inspect, read, or cancel. Use when: the command may run for a while, produce incremental output, or need later control. Notes: usually follow with builtin::command::inspect or builtin::command::read.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("inspect")),
|
||||
@@ -145,8 +170,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("id", "Command session id."),
|
||||
"Inspect a background command session.",
|
||||
Map.of("id", param("required", "string", "Command executionId returned by builtin::command::start.")),
|
||||
"Purpose: inspect status and short output summaries for an existing background command session. Inputs: id from command::start. Returns: JSON with executionId, desc, exitCode, stdoutSize, stderrSize, stdoutSummary, stderrSummary, startAt, and endAt. Use when: deciding whether a background command is still running, failed, or needs a full output read. Notes: use builtin::command::read for complete or paginated output.",
|
||||
tags,
|
||||
Set.of(createActionKey("overview")),
|
||||
Set.of(),
|
||||
@@ -195,12 +220,12 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"id", "Command execution session id.",
|
||||
"stream", "Target stream, stdout or stderr. Default stdout.",
|
||||
"offset", "Read start offset. Default 0.",
|
||||
"limit", "Read max length. Default 4096."
|
||||
"id", param("required", "string", "Command executionId returned by builtin::command::start."),
|
||||
"stream", param("optional", "string", "Target stream to read. Allowed values: stdout, stderr. Default: stdout."),
|
||||
"offset", param("optional", "int", "Start byte offset in the persisted stream log. Default: 0. Use nextOffset from a previous read to continue."),
|
||||
"limit", param("optional", "int", "Maximum bytes to read from the persisted stream log. Default: 4096. Must be greater than 0.")
|
||||
),
|
||||
"Read output from a background command session.",
|
||||
"Purpose: read stdout or stderr content from a background command session with offset pagination. Returns: JSON with content, contentTruncated, nextOffset, and eof. Use when: inspect summaries are insufficient or a command failed and stderr/stdout details are needed. Notes: repeat with nextOffset until eof or enough evidence is collected.",
|
||||
tags,
|
||||
Set.of(createActionKey("overview")),
|
||||
Set.of(),
|
||||
@@ -263,8 +288,8 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("id", "Command session id."),
|
||||
"Cancel a background command session.",
|
||||
Map.of("id", param("required", "string", "Command executionId returned by builtin::command::start.")),
|
||||
"Purpose: terminate an existing background command session. Inputs: id from command::start. Returns: JSON with ok and executionId. Use when: the user explicitly asks to stop a command, or correction determines the running command is wrong, stuck, unsafe, or no longer needed. Notes: this has process side effects and should not be used as a status check.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("overview")),
|
||||
@@ -316,16 +341,16 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(),
|
||||
"List all background command sessions.",
|
||||
"Purpose: list known background command sessions. Inputs: none. Returns: JSON with result array of session overviews containing executionId, desc, and exitCode. Use when: an id is missing or correction needs to discover active command sessions before inspect/read/cancel. Notes: this does not start, stop, or read a command.",
|
||||
tags,
|
||||
Set.of(createActionKey("start")),
|
||||
Set.of(createActionKey("inspect"), createActionKey("read"), createActionKey("cancel")),
|
||||
false,
|
||||
JSONObject.of(
|
||||
"commands", "Array of command session overview items.",
|
||||
"command.executionId", "Command execution session id for each overview item.",
|
||||
"command.desc", "Command session description for each overview item.",
|
||||
"command.exitCode", "Process exit code for each overview item. Null when still running."
|
||||
"result", "Array of command session overview items.",
|
||||
"result[].executionId", "Command execution session id for each overview item.",
|
||||
"result[].desc", "Command session description for each overview item.",
|
||||
"result[].exitCode", "Process exit code for each overview item. Null when still running."
|
||||
)
|
||||
);
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
@@ -492,30 +517,36 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
||||
}
|
||||
|
||||
private List<String> requireCommandArguments(Map<String, Object> params) {
|
||||
List<Map.Entry<String, Object>> entries = params.entrySet().stream()
|
||||
List<Map.Entry<String, Object>> unexpectedArgs = params.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().startsWith(COMMAND_ARG_PREFIX))
|
||||
.sorted(Comparator.comparingInt(entry -> commandArgIndex(entry.getKey())))
|
||||
.filter(entry -> !COMMAND_ARG_PREFIX.equals(entry.getKey()) && !isNumberedCommandArg(entry.getKey()))
|
||||
.toList();
|
||||
if (entries.isEmpty()) {
|
||||
throw new IllegalArgumentException("缺少命令参数");
|
||||
if (!unexpectedArgs.isEmpty()) {
|
||||
throw new IllegalArgumentException("非法命令参数: " + unexpectedArgs.getFirst().getKey());
|
||||
}
|
||||
|
||||
List<String> commands = new ArrayList<>();
|
||||
for (Map.Entry<String, Object> entry : entries) {
|
||||
commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, entry.getKey()));
|
||||
}
|
||||
commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, COMMAND_ARG_PREFIX));
|
||||
params.entrySet().stream()
|
||||
.filter(entry -> isNumberedCommandArg(entry.getKey()))
|
||||
.sorted(Comparator.comparingInt(entry -> commandArgIndex(entry.getKey())))
|
||||
.forEach(entry -> commands.add(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, entry.getKey())));
|
||||
return commands;
|
||||
}
|
||||
|
||||
private int commandArgIndex(String key) {
|
||||
private boolean isNumberedCommandArg(String key) {
|
||||
if (!key.startsWith(COMMAND_ARG_PREFIX) || COMMAND_ARG_PREFIX.equals(key)) {
|
||||
return false;
|
||||
}
|
||||
String suffix = key.substring(COMMAND_ARG_PREFIX.length()).trim();
|
||||
if (suffix.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(suffix);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return Integer.MAX_VALUE;
|
||||
return false;
|
||||
}
|
||||
return suffix.chars().allMatch(Character::isDigit);
|
||||
}
|
||||
|
||||
private int commandArgIndex(String key) {
|
||||
return Integer.parseInt(key.substring(COMMAND_ARG_PREFIX.length()).trim());
|
||||
}
|
||||
|
||||
private String bufferSnapshot(StringBuilder buffer) {
|
||||
|
||||
+16
-8
@@ -7,6 +7,7 @@ import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.module.action.scheduler.ActionScheduler;
|
||||
|
||||
@@ -29,6 +30,13 @@ class BuiltinDynamicActionProvider implements BuiltinActionProvider {
|
||||
private ActionCapability actionCapability;
|
||||
@InjectModule
|
||||
private ActionScheduler actionScheduler;
|
||||
@InjectModule
|
||||
private BuiltinActionRegistry builtinActionRegistry;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
builtinActionRegistry.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
@@ -43,13 +51,13 @@ class BuiltinDynamicActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"desc", "Human-readable description for the temporary action. Required because the generated action name is only a short id.",
|
||||
"code", "Dynamic action source code content.",
|
||||
"codeType", "Code extension, for example py/sh/js.",
|
||||
"launcher", "Interpreter or launcher used for ORIGIN execution.",
|
||||
"meta", "MetaActionInfo extra fields as JSON string. Available fields example: {\"io\":true,\"params\":{\"input\":\"user input\"},\"tags\":[\"dynamic\"],\"preActions\":[\"builtin::command::execute\"],\"postActions\":[\"builtin::dynamic::persist\"],\"strictDependencies\":false,\"responseSchema\":{\"result\":\"dynamic result\"}}"
|
||||
"desc", param("required", "string", "Human-readable description for the generated temporary action. This becomes the new action's planning description."),
|
||||
"code", param("required", "string", "Dynamic action source code content."),
|
||||
"codeType", param("required", "string", "Source file extension without or with leading dot, for example py, sh, js, or .py."),
|
||||
"launcher", param("required", "string", "Interpreter or launcher command used to run the generated code, for example python3, bash, or node."),
|
||||
"meta", param("required", "json-string", "MetaActionInfo fields for the generated action. Supported fields: io, params, tags, preActions, postActions, strictDependencies, responseSchema.")
|
||||
),
|
||||
"Generate a temporary ORIGIN action from source code and return a temporary actionKey.",
|
||||
"Purpose: generate and register a temporary script as an ORIGIN MetaAction from supplied source code. Inputs: desc, code, codeType, launcher, and meta info. Returns: JSON with ok and temporary actionKey. Use when: existing MetaActions cannot perform the task and a temporary code-backed action is needed. Notes: this registers executable code with cleanup TTL; the generated action is temporary until builtin::dynamic::persist is called.",
|
||||
basicTags,
|
||||
Set.of(),
|
||||
Set.of(createActionKey("persist")),
|
||||
@@ -110,8 +118,8 @@ class BuiltinDynamicActionProvider implements BuiltinActionProvider {
|
||||
MetaActionInfo info = new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of("actionKey", "Temporary ORIGIN actionKey returned by generate."),
|
||||
"Persist a temporary ORIGIN action and cancel its cleanup task.",
|
||||
Map.of("actionKey", param("required", "string", "Temporary ORIGIN actionKey returned by builtin::dynamic::generate.")),
|
||||
"Purpose: persist a previously generated temporary ORIGIN MetaAction and cancel its cleanup task. Inputs: actionKey from dynamic::generate. Returns: JSON with ok and actionKey. Use when: a generated temporary action should become reusable beyond its cleanup TTL. Notes: this has persistence side effects and should only be used when the generated action is validated or explicitly requested.",
|
||||
basicTags,
|
||||
Set.of(createActionKey("generate")),
|
||||
Set.of(),
|
||||
|
||||
+31
-23
@@ -17,6 +17,8 @@ import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.util.*;
|
||||
@@ -35,6 +37,13 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
private ActionCapability actionCapability;
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
@InjectModule
|
||||
private BuiltinActionRegistry builtinActionRegistry;
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
builtinActionRegistry.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
@@ -56,9 +65,9 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"actionId", "Uuid of the interrupted executable action to resume."
|
||||
"actionId", param("required", "string", "Uuid of an executable action currently in INTERRUPTED status.")
|
||||
),
|
||||
"Resume action that is interrupted.",
|
||||
"Purpose: resume an executable action that is currently interrupted. Inputs: actionId. Returns: plain text success or failure reason. Use when: an intervention or user decision is complete and the original action should continue. Notes: this does not modify the action chain; use create_intervention before resuming when chain changes are required.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
@@ -71,7 +80,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
Function<Map<String, Object>, String> invoker = params -> {
|
||||
String actionId = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "actionId");
|
||||
try {
|
||||
ExecutableAction executableAction = getExecutableAction(actionId);
|
||||
ExecutableAction executableAction = getExecutableAction(actionId, Action.Status.INTERRUPTED);
|
||||
executableAction.resume();
|
||||
return "Resume succeed";
|
||||
} catch (Exception e) {
|
||||
@@ -101,14 +110,14 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
true,
|
||||
null,
|
||||
Map.of(
|
||||
"actionId", "Uuid of the executing action that should enter intervention flow.",
|
||||
"actionInfo", "Readable summary of the current action, used to explain the intervention context to the target.",
|
||||
"demand", "What feedback, decision or operation is required from the target user.",
|
||||
"target", "Target user or channel identifier that should receive the intervention request.",
|
||||
"input", "Prompt content used to initiate the intervention turn toward the target.",
|
||||
"timeout", "Maximum wait time for the interruption result, in the unit expected by ExecutableAction.interrupt(timeout)."
|
||||
"actionId", param("required", "string", "Uuid of the executing action that should be interrupted for external intervention."),
|
||||
"actionInfo", param("required", "string", "Readable summary of the current action and why intervention is needed."),
|
||||
"demand", param("required", "string", "Specific feedback, decision, permission, or operation required from the target."),
|
||||
"target", param("required", "string", "Target user or channel identifier that should receive the intervention request."),
|
||||
"input", param("required", "string", "Prompt content used to initiate the intervention turn toward the target."),
|
||||
"timeout", param("required", "int", "Maximum wait time passed to ExecutableAction.interrupt(timeout).")
|
||||
),
|
||||
"Try to acquire the target user to intervene this Action.",
|
||||
"Purpose: interrupt an executing action and ask a target participant/channel for intervention. Inputs: actionId, actionInfo, demand, target, input, timeout. Returns: plain text intervention status. Use when: execution cannot safely continue without user decision, missing information, permission, or manual operation. Notes: this creates communication side effects and temporarily interrupts the action.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
@@ -147,7 +156,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
|
||||
ExecutableAction executableAction = null;
|
||||
try {
|
||||
executableAction = getExecutableAction(actionId);
|
||||
executableAction = getExecutableAction(actionId, Action.Status.EXECUTING);
|
||||
cognitionCapability.initiateTurn(input, target);
|
||||
boolean normal = executableAction.interrupt(timeout);
|
||||
return normal ? target + "not resumed execution in time" : target + "answered, looking for related answer in recent-chat-messages";
|
||||
@@ -168,12 +177,12 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
);
|
||||
}
|
||||
|
||||
private ExecutableAction getExecutableAction(String actionId) {
|
||||
return actionCapability.listActions(Action.Status.EXECUTING, null)
|
||||
private ExecutableAction getExecutableAction(String actionId, Action.Status status) {
|
||||
return actionCapability.listActions(status, null)
|
||||
.stream()
|
||||
.filter(action -> action.getUuid().equals(actionId))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
.orElseThrow(() -> new IllegalArgumentException("未找到对应状态的 Action: " + actionId + ", status=" + status));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +199,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(),
|
||||
"List existing actions that can be intervened",
|
||||
"Purpose: list executable actions that can be inspected for possible intervention. Inputs: none. Returns: JSON array with tendency, description, status, and uuid for each action. Use when: an action id is needed before acquire_intervention, create_intervention, or resume_interrupted_action. Notes: this only discovers actions; it does not modify them.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
@@ -237,7 +246,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(),
|
||||
"List available MetaActions.",
|
||||
"Purpose: list currently available MetaActions that can be used in action chains or interventions. Inputs: none. Returns: JSON array with action key/location, description, tags, and params. Use when: correction needs to discover valid action_key values for APPEND, INSERT, DELETE, or REBUILD. Notes: this is a discovery action and does not execute those MetaActions.",
|
||||
tags,
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
@@ -282,12 +291,12 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
false,
|
||||
null,
|
||||
Map.of(
|
||||
"id", "The uuid of the Action to be intervened on.",
|
||||
"type", "Intervention type. Allowed values: APPEND, INSERT, DELETE, CANCEL.",
|
||||
"order", "Action chain order/stage to apply the intervention on.",
|
||||
"actions", "Comma-separated actionKey list to be inserted, appended, rebuilt or deleted. Example: \"builtin::command::execute, builtin::capability::show_memory_slices\""
|
||||
"id", param("required", "string", "Uuid of the executable action to modify."),
|
||||
"type", param("required", "string", "Intervention type. Allowed values: APPEND, INSERT, DELETE, CANCEL, REBUILD."),
|
||||
"order", param("required", "int", "Action chain order/stage where the intervention applies."),
|
||||
"actions", param("conditional", "string", "Comma-separated actionKey list to insert, append, rebuild, or delete. Required except for CANCEL. Example: builtin::command::execute,builtin::command::read.")
|
||||
),
|
||||
"Used to create an Action Intervention and act on the specified Executable Action.",
|
||||
"Purpose: apply a structural intervention to an executable action chain. Inputs: id, type, order, and conditional actions. Returns: JSON with ok and optional result. Use when: correction must append, insert, delete, cancel, or rebuild future action-chain steps. Notes: actions must be valid action_key values; CANCEL may omit actions; this has direct action-chain side effects and resumes the target action after applying changes.",
|
||||
basicTags,
|
||||
Set.of(
|
||||
createActionKey("list_available_meta_actions"),
|
||||
@@ -324,8 +333,7 @@ class BuiltinInterventionActionProvider implements BuiltinActionProvider {
|
||||
|
||||
actionCapability.handleInterventions(List.of(intervention), target);
|
||||
|
||||
ExecutableAction executableAction = getExecutableAction(targetId);
|
||||
executableAction.resume();
|
||||
target.resume();
|
||||
|
||||
return JSONObject.of("ok", true).toJSONString();
|
||||
};
|
||||
|
||||
+18
-9
@@ -28,21 +28,25 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
|
||||
- 一条任务消息,其中包含:
|
||||
- check_mode:当前识别模式,PROCESS_CHECK 表示过程纠偏识别,FINAL_CHECK 表示链路末尾完成度验收;
|
||||
- executable_action_info:当前正在执行的行动信息,包括 executing_action_id、original_tendency、evaluation_passed_reason、description 与 from_who;
|
||||
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description 与 status。
|
||||
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description、status 与 result。
|
||||
|
||||
你的任务:
|
||||
- 基于当前上下文、当前行动信息与当前行动链概览,判断这条行动链是否仍在合理推进;
|
||||
- 判断当前是否已经出现明显偏航、停滞、重复打转、条件失配、目标漂移,或与最新语境不再一致的情况;
|
||||
- 当 check_mode=PROCESS_CHECK 时,基于当前上下文、当前行动信息与当前行动链概览,判断这条行动链是否仍在合理推进;
|
||||
- 当 check_mode=FINAL_CHECK 时,判断当前行动链即将结束时,original_tendency 是否已经被执行结果满足;
|
||||
- 判断当前是否已经出现明显偏航、停滞、重复打转、条件失配、目标漂移、目标未完成,或与最新语境不再一致的情况;
|
||||
- 若需要引入 corrector,则返回 needCorrection=true,并给出简洁明确的 reason;
|
||||
- 若当前仍可继续推进,则返回 needCorrection=false。
|
||||
- 若当前仍可继续推进或已经满足目标,则返回 needCorrection=false。
|
||||
|
||||
识别原则:
|
||||
- executable_action_info 用于说明当前链路最初为何成立、要解决什么问题、当前由谁发起;你的判断必须围绕这条行动本身,而不是被无关历史带偏。
|
||||
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构与已知阶段状态。
|
||||
- executing_action_id 用于标识当前正在执行的行动;current_action_chain_overview 用于说明当前链路整体结构、已知阶段状态与执行结果。
|
||||
- original_tendency 表示这条行动链最初要解决的问题;若当前链路明显偏离这一倾向,应优先视为异常信号。
|
||||
- evaluation_passed_reason 与 description 表示该行动最初为何被判定为可推进;若当前状态已经与这些前提不再一致,应考虑触发纠正。
|
||||
- current_action_chain_overview 是判断当前链路是否停滞、重复、缺步骤、顺序异常或整体失衡的主要依据。
|
||||
- current_action_chain_overview 是判断当前链路是否停滞、重复、缺步骤、顺序异常、整体失衡或最终目标未满足的主要依据。
|
||||
- PROCESS_CHECK 关注执行过程是否仍然合理;FINAL_CHECK 关注链路结果是否满足 original_tendency。
|
||||
- FINAL_CHECK 时应重点查看各 meta_action 的 result;如果 result 显示对象不存在、权限不足、参数错误、输出为空、命令失败、目标未产生或结果明显不符合 original_tendency,应触发纠正。
|
||||
- communication 域用于判断最新交流语境是否已发生明显变化,导致当前行动继续推进不再合适。
|
||||
- memory 域只在与当前行动明显相关时作为辅助参考使用。
|
||||
|
||||
@@ -51,12 +55,14 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
|
||||
- 当前执行显著偏离 original_tendency,开始围绕无关目标展开;
|
||||
- 当前行动所依赖的前提已被新的交流内容或上下文推翻;
|
||||
- 当前行动链反复遇到同类失败、阻塞或空转迹象,继续按原链推进意义不大;
|
||||
- 当前行动链已经明显需要改写策略、调整阶段顺序、补入新步骤或删除无效步骤,但现有链路无法自行收敛。
|
||||
- 当前行动链已经明显需要改写策略、调整阶段顺序、补入新步骤或删除无效步骤,但现有链路无法自行收敛;
|
||||
- FINAL_CHECK 时,执行结果明确显示 original_tendency 尚未满足,且可能通过补救步骤继续推进。
|
||||
|
||||
不应轻易触发纠正的情形:
|
||||
- 只是正常的多步推进;
|
||||
- 存在短暂等待、一次性失败或合理重试,但整体方向仍正确;
|
||||
- PROCESS_CHECK 时,只是正常的多步推进;
|
||||
- PROCESS_CHECK 时,存在短暂等待、一次性失败或合理重试,但整体方向仍正确;
|
||||
- 行动链整体仍与 original_tendency、evaluation_passed_reason 和当前语境保持一致;
|
||||
- FINAL_CHECK 时,执行结果已经足以满足 original_tendency,只需要向用户报告结果;
|
||||
- 仅凭旧对话、低相关记忆或轻微波动,不足以判定当前行动异常。
|
||||
|
||||
关于输出:
|
||||
@@ -85,6 +91,7 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "check_mode", input.getCheckMode());
|
||||
appendChildElement(document, root, "executable_action_info", block -> {
|
||||
appendTextElement(document, block, "executing_action_id", input.getActionId());
|
||||
appendTextElement(document, block, "original_tendency", input.getTendency());
|
||||
@@ -100,6 +107,7 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
|
||||
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
|
||||
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
|
||||
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
|
||||
appendTextElement(document, metaActionElement, "result", metaActionData.getResult());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
@@ -111,6 +119,7 @@ public class ActionCorrectionRecognizer extends AbstractAgentModule.Sub<Correcto
|
||||
private Message resolveContextMessage() {
|
||||
return cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.PERCEIVE,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
)).encodeToMessage();
|
||||
}
|
||||
|
||||
+28
-3
@@ -4,6 +4,7 @@ import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
@@ -28,13 +29,17 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中包含近期交流轨迹与当前活跃记忆切片;
|
||||
- 一条任务消息,其中包含:
|
||||
- check_mode:当前纠偏模式,PROCESS_CHECK 表示过程纠偏,FINAL_CHECK 表示链路末尾目标未满足后的补救;
|
||||
- executable_action_info:当前正在执行的行动信息,包括 executing_action_id、original_tendency、evaluation_passed_reason、description 与 from_who;
|
||||
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description 与 status。
|
||||
- current_action_chain_overview:当前行动链概览,按 stage_count 分组,包含各阶段已有 meta_action 的 action_key、description、stage_description、status 与 result;stage_description 是该阶段的具体执行目标,description 是 MetaAction 的通用能力说明。
|
||||
- available_meta_action:当前系统真实可用的 MetaAction 候选,包含 meta_action_key 与 meta_action_description。
|
||||
|
||||
你的任务:
|
||||
- 基于当前上下文、原始行动意图与当前行动链进展,判断后续行动是否仍然符合目的;
|
||||
- 若当前链路仍可继续推进,则不要随意干预;
|
||||
- 当 check_mode=PROCESS_CHECK 时,基于当前上下文、原始行动意图与当前行动链进展,判断后续行动是否仍然符合目的;
|
||||
- 当 check_mode=FINAL_CHECK 时,基于当前行动链的最终结果判断 original_tendency 尚未满足时应如何补救;
|
||||
- 若当前链路仍可继续推进或最终结果已足够满足目标,则不要随意干预;
|
||||
- 若当前链路已明显跑偏、缺少必要步骤、顺序不合理、存在冗余、已经不再适合继续,或需要引入新的动作单元,则输出干预方案;
|
||||
- FINAL_CHECK 下若结果显示目标未满足,应优先补入可继续执行的后续动作;如果无法补救,可以输出空 intervention 并在 correctionReason 中说明原因。
|
||||
- correctionReason 用于简洁说明为何需要这些纠偏。
|
||||
|
||||
纠偏原则:
|
||||
@@ -43,6 +48,7 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
|
||||
- original_tendency 是这条行动链最初要解决的问题;后续行动的调整必须仍然围绕它。
|
||||
- evaluation_passed_reason 与 description 表示该行动最初为何能够成立;若当前链路已经与这些前提不一致,可据此进行纠偏。
|
||||
- current_action_chain_overview 是判断后续链路是否缺步骤、顺序失衡、重复冗余或整体方向错误的主要依据。
|
||||
- available_meta_action 是你进行 APPEND、INSERT 或 REBUILD 时可选择的动作全集;不要使用其中不存在的 action_key。
|
||||
- communication 域用于判断最新交流语境是否已经变化,导致当前链路需要调整。
|
||||
- memory 域只在与当前行动明显相关时作为辅助参考使用。
|
||||
|
||||
@@ -88,6 +94,8 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
@Override
|
||||
protected @NotNull Result<CorrectorResult> doExecute(CorrectorInput input) {
|
||||
@@ -102,6 +110,7 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "check_mode", input.getCheckMode());
|
||||
appendChildElement(document, root, "executable_action_info", block -> {
|
||||
appendTextElement(document, block, "executing_action_id", input.getActionId());
|
||||
appendTextElement(document, block, "original_tendency", input.getTendency());
|
||||
@@ -116,11 +125,26 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
|
||||
appendRepeatedElements(document, stageElement, "meta_action", stageData.getValue(), (metaActionElement, metaActionData) -> {
|
||||
appendTextElement(document, metaActionElement, "action_key", metaActionData.getActionKey());
|
||||
appendTextElement(document, metaActionElement, "description", metaActionData.getDescription());
|
||||
if (metaActionData.getStageDescription() != null && !metaActionData.getStageDescription().isBlank()) {
|
||||
appendTextElement(document, metaActionElement, "stage_description", metaActionData.getStageDescription());
|
||||
}
|
||||
appendTextElement(document, metaActionElement, "status", metaActionData.getStatus());
|
||||
appendTextElement(document, metaActionElement, "result", metaActionData.getResult());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
appendRepeatedElements(
|
||||
document,
|
||||
root,
|
||||
"available_meta_action",
|
||||
actionCapability.listAvailableMetaActions().entrySet(),
|
||||
(block, value) -> {
|
||||
appendTextElement(document, block, "meta_action_key", value.getKey());
|
||||
appendTextElement(document, block, "meta_action_description", value.getValue().getDescription());
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
);
|
||||
}
|
||||
}.encodeToMessage();
|
||||
}
|
||||
@@ -128,6 +152,7 @@ public class ActionCorrector extends AbstractAgentModule.Sub<CorrectorInput, Res
|
||||
private Message resolveContextMessage() {
|
||||
return cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.PERCEIVE,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
)).encodeToMessage();
|
||||
}
|
||||
|
||||
+103
-13
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
|
||||
public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
|
||||
private static final int MAX_EXTRACTOR_ATTEMPTS = 3;
|
||||
private static final int MAX_FINAL_CORRECTION_ATTEMPTS = 1;
|
||||
|
||||
private final AssemblyHelper assemblyHelper = new AssemblyHelper();
|
||||
|
||||
@@ -105,12 +106,12 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
if (action instanceof ExecutableAction executableAction) {
|
||||
if (action.getStatus() == Action.Status.FAILED) {
|
||||
ensureExecutableResult(executableAction, true, null);
|
||||
blockManager.emitActionFinishedBlock(executableAction);
|
||||
emitExecutableActionFinished(executableAction);
|
||||
return;
|
||||
}
|
||||
action.setStatus(Action.Status.SUCCESS);
|
||||
ensureExecutableResult(executableAction, false, null);
|
||||
blockManager.emitActionFinishedBlock(executableAction);
|
||||
emitExecutableActionFinished(executableAction);
|
||||
return;
|
||||
}
|
||||
if (action.getStatus() == Action.Status.FAILED) {
|
||||
@@ -122,7 +123,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
action.setStatus(Action.Status.FAILED);
|
||||
if (action instanceof ExecutableAction executableAction) {
|
||||
ensureExecutableResult(executableAction, true, e.getLocalizedMessage());
|
||||
blockManager.emitActionFinishedBlock(executableAction);
|
||||
emitExecutableActionFinished(executableAction);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -133,6 +134,15 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
action.setStatus(Action.Status.FAILED);
|
||||
}
|
||||
|
||||
private void emitExecutableActionFinished(ExecutableAction executableAction) {
|
||||
blockManager.emitActionFinishedBlock(executableAction);
|
||||
cognitionCapability.initiateTurn(
|
||||
"An executable action has finished. Check the latest action-finished state and inform the user of the outcome if it is relevant to the current conversation.",
|
||||
executableAction.getSource(),
|
||||
"action_planner"
|
||||
);
|
||||
}
|
||||
|
||||
private void handleStateAction(StateAction stateAction) {
|
||||
if (closed.get()) {
|
||||
return;
|
||||
@@ -145,7 +155,6 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
actionCapability.putAction(executableAction);
|
||||
|
||||
val actionChain = executableAction.getActionChain();
|
||||
val phaser = new Phaser();
|
||||
if (!prepareExecutableAction(executableAction, actionChain)) {
|
||||
return;
|
||||
}
|
||||
@@ -153,6 +162,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
blockManager.emitActionLaunchedBlock(executableAction);
|
||||
|
||||
val stageCursor = initStageCursor(executableAction, actionChain);
|
||||
int finalCorrectionAttempts = 0;
|
||||
while (true) {
|
||||
val stageSelection = selectCurrentStage(executableAction, actionChain);
|
||||
if (stageSelection.shouldReturn()) {
|
||||
@@ -161,12 +171,15 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
if (stageSelection.shouldStop()) {
|
||||
break;
|
||||
}
|
||||
val stageExecution = runCurrentStage(executableAction, phaser, stageCursor, stageSelection.metaActions());
|
||||
val stageExecution = runCurrentStage(executableAction, stageCursor, stageSelection.metaActions());
|
||||
if (stageExecution.closed()) {
|
||||
return;
|
||||
}
|
||||
if (!applyStageCorrectionAndAdvance(executableAction, stageCursor, stageExecution)) {
|
||||
break;
|
||||
if (finalCorrectionAttempts >= MAX_FINAL_CORRECTION_ATTEMPTS || !applyFinalCorrection(executableAction, stageCursor)) {
|
||||
break;
|
||||
}
|
||||
finalCorrectionAttempts++;
|
||||
}
|
||||
}
|
||||
finishExecutableAction(executableAction);
|
||||
@@ -211,10 +224,10 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
|
||||
private StageExecution runCurrentStage(
|
||||
ExecutableAction executableAction,
|
||||
Phaser phaser,
|
||||
StageCursor stageCursor,
|
||||
List<MetaAction> metaActions
|
||||
) {
|
||||
Phaser phaser = new Phaser();
|
||||
val recognizerRecord = startRecognizerIfNeeded(executableAction, phaser);
|
||||
val listeningRecord = executeAndListening(metaActions, phaser, executableAction);
|
||||
phaser.awaitAdvance(listeningRecord.phase());
|
||||
@@ -248,7 +261,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
shouldRunCorrector = recognizerResult != null && recognizerResult.isNeedCorrection();
|
||||
}
|
||||
if (shouldRunCorrector) {
|
||||
val correctorInput = assemblyHelper.buildCorrectorInput(executableAction);
|
||||
val correctorInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.PROCESS_CHECK);
|
||||
actionCorrector.execute(correctorInput)
|
||||
.onSuccess(correctorResult -> {
|
||||
actionCapability.handleInterventions(correctorResult.getMetaInterventionList(), executableAction);
|
||||
@@ -267,6 +280,59 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean applyFinalCorrection(ExecutableAction executableAction, StageCursor stageCursor) {
|
||||
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.FINAL_CHECK);
|
||||
val recognizerResult = actionCorrectionRecognizer.execute(recognizerInput)
|
||||
.getOrDefault(new RecognizerResult());
|
||||
if (!recognizerResult.isNeedCorrection()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
val previousStage = executableAction.getExecutingStage();
|
||||
val correctorResult = actionCorrector.execute(recognizerInput).getOrDefault(new CorrectorResult());
|
||||
val interventions = correctorResult.getMetaInterventionList() == null
|
||||
? new ArrayList<work.slhaf.partner.core.action.entity.intervention.MetaIntervention>()
|
||||
: correctorResult.getMetaInterventionList();
|
||||
if (!interventions.isEmpty()) {
|
||||
actionCapability.handleInterventions(interventions, executableAction);
|
||||
}
|
||||
blockManager.emitActionCorrectionBlock(
|
||||
executableAction,
|
||||
recognizerResult.getReason() == null || recognizerResult.getReason().isBlank()
|
||||
? correctorResult.getCorrectionReason()
|
||||
: recognizerResult.getReason(),
|
||||
interventions
|
||||
);
|
||||
|
||||
synchronized (executableAction.getExecutionLock()) {
|
||||
if (executableAction.getStatus() == Action.Status.FAILED) {
|
||||
return false;
|
||||
}
|
||||
if (interventions.isEmpty() || executableAction.getActionChain().isEmpty()) {
|
||||
executableAction.setStatus(Action.Status.FAILED);
|
||||
ensureExecutableResult(executableAction, true, "行动未达到目标,且未能生成可继续执行的纠偏行动");
|
||||
return false;
|
||||
}
|
||||
if (executableAction.getStatus() == Action.Status.PREPARE) {
|
||||
normalizeExecutingStage(executableAction, executableAction.getActionChain());
|
||||
executableAction.setStatus(Action.Status.EXECUTING);
|
||||
} else {
|
||||
Integer nextStage = executableAction.getActionChain().keySet().stream()
|
||||
.filter(stage -> stage > previousStage)
|
||||
.min(Integer::compareTo)
|
||||
.orElse(null);
|
||||
if (nextStage == null) {
|
||||
executableAction.setStatus(Action.Status.FAILED);
|
||||
ensureExecutableResult(executableAction, true, "行动未达到目标,纠偏未生成后续执行阶段");
|
||||
return false;
|
||||
}
|
||||
executableAction.setExecutingStage(nextStage);
|
||||
}
|
||||
stageCursor.refresh();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishExecutableAction(ExecutableAction executableAction) {
|
||||
// 如果是 ScheduledActionData, 则重置 ActionData 内容,记录执行历史与最终结果
|
||||
if (executableAction instanceof SchedulableExecutableAction scheduledActionData) {
|
||||
@@ -339,7 +405,12 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
result.reset();
|
||||
metaAction.getParams().clear();
|
||||
|
||||
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(metaAction.getKey(), actionData.getUuid(), actionData.getDescription());
|
||||
Result<ExtractorInput> extractorInputResult = assemblyHelper.buildExtractorInput(
|
||||
metaAction.getKey(),
|
||||
actionData.getUuid(),
|
||||
actionData.getDescription(),
|
||||
resolveCurrentStageDescription(actionData, executingStage)
|
||||
);
|
||||
AgentRuntimeException exception = extractorInputResult.exceptionOrNull();
|
||||
if (exception != null) {
|
||||
failureReason.set(exception.getMessage());
|
||||
@@ -413,7 +484,7 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
if (!shouldRunCorrectionRecognizer(executableAction)) {
|
||||
return RecognizerTaskRecord.disabled();
|
||||
}
|
||||
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction);
|
||||
val recognizerInput = assemblyHelper.buildCorrectorInput(executableAction, CorrectorInput.CheckMode.PROCESS_CHECK);
|
||||
val task = buildRecognizerTask(recognizerInput, phaser);
|
||||
Future<RecognizerResult> future = virtualExecutor.submit(task);
|
||||
return new RecognizerTaskRecord(true, future);
|
||||
@@ -445,6 +516,10 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveCurrentStageDescription(ExecutableAction executableAction, int executingStage) {
|
||||
return executableAction.getStageDescriptions().get(executingStage);
|
||||
}
|
||||
|
||||
private String buildAttemptFailureReason(String prefix, String detail) {
|
||||
if (detail == null || detail.isBlank()) {
|
||||
return prefix;
|
||||
@@ -655,6 +730,17 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
return stageCount < actionChain.size();
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
val orderList = new ArrayList<>(actionChain.keySet());
|
||||
orderList.sort(Integer::compareTo);
|
||||
stageCount = orderList.indexOf(executableAction.getExecutingStage());
|
||||
if (stageCount < 0) {
|
||||
stageCount = 0;
|
||||
}
|
||||
executingStageUpdated = false;
|
||||
stageCountUpdated = false;
|
||||
}
|
||||
|
||||
private void update() {
|
||||
val orderList = new ArrayList<>(actionChain.keySet());
|
||||
orderList.sort(Integer::compareTo);
|
||||
@@ -667,32 +753,36 @@ public class ActionExecutor extends AbstractAgentModule.Standalone {
|
||||
private AssemblyHelper() {
|
||||
}
|
||||
|
||||
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description) {
|
||||
private Result<ExtractorInput> buildExtractorInput(String actionKey, @NotNull String uuid, @NotNull String description, String currentStageDescription) {
|
||||
return actionCapability.loadMetaActionInfo(actionKey).fold(
|
||||
metaActionInfo -> {
|
||||
ExtractorInput input = new ExtractorInput();
|
||||
input.setMetaActionInfo(metaActionInfo);
|
||||
input.setTargetActionId(uuid);
|
||||
input.setTargetActionDesc(description);
|
||||
input.setCurrentStageDesc(currentStageDescription);
|
||||
return Result.success(input);
|
||||
},
|
||||
Result::failure
|
||||
);
|
||||
}
|
||||
|
||||
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction) {
|
||||
private CorrectorInput buildCorrectorInput(ExecutableAction executableAction, CorrectorInput.CheckMode checkMode) {
|
||||
Map<Integer, List<CorrectorInput.ActionChainItem>> overview = new LinkedHashMap<>();
|
||||
executableAction.getActionChain().forEach((stage, list) -> {
|
||||
List<CorrectorInput.ActionChainItem> overviewItems = list.stream()
|
||||
.map(metaAction -> new CorrectorInput.ActionChainItem(
|
||||
metaAction.getKey(),
|
||||
resolveHistoryDescription(metaAction.getKey()),
|
||||
metaAction.getResult().getStatus().name().toLowerCase(Locale.ROOT)
|
||||
executableAction.getStageDescriptions().get(stage),
|
||||
metaAction.getResult().getStatus().name().toLowerCase(Locale.ROOT),
|
||||
metaAction.getResult().getData()
|
||||
))
|
||||
.toList();
|
||||
overview.put(stage, overviewItems);
|
||||
});
|
||||
return CorrectorInput.builder()
|
||||
.checkMode(checkMode)
|
||||
.tendency(executableAction.getTendency())
|
||||
.source(executableAction.getSource())
|
||||
.reason(executableAction.getReason())
|
||||
|
||||
+22
-2
@@ -9,6 +9,7 @@ import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.ContextWorkspace;
|
||||
import work.slhaf.partner.module.StateHintContent;
|
||||
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
@@ -98,8 +99,8 @@ class ExecutingActionBlockManager {
|
||||
buildStateActionCompactBlock(snapshot, blockName, emittedAt, event),
|
||||
buildStateActionAbstractBlock(snapshot, blockName, event),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
70,
|
||||
18,
|
||||
90,
|
||||
30,
|
||||
10
|
||||
));
|
||||
}
|
||||
@@ -382,6 +383,7 @@ class ExecutingActionBlockManager {
|
||||
14,
|
||||
24
|
||||
));
|
||||
contextWorkspace.register(buildActionFinishedStateHintBlock(snapshot, emittedAt, event));
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildActionFinishedAbstractBlock(ExecutableActionSnapshot snapshot, String blockName, String event) {
|
||||
@@ -406,6 +408,24 @@ class ExecutingActionBlockManager {
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull ContextBlock buildActionFinishedStateHintBlock(ExecutableActionSnapshot snapshot, String emittedAt, String event) {
|
||||
return StateHintContent.createBlock(new StateHintContent(
|
||||
SOURCE,
|
||||
"An executable action has finished."
|
||||
) {
|
||||
@Override
|
||||
public void fillStateContent(@NotNull Document document, @NotNull Element stateElement) {
|
||||
appendTextElement(document, stateElement, "event", event);
|
||||
appendTextElement(document, stateElement, "emitted_at", emittedAt);
|
||||
appendTextElement(document, stateElement, "action_id", snapshot.getUuid());
|
||||
appendTextElement(document, stateElement, "final_status", snapshot.getStatus().name().toLowerCase(Locale.ROOT));
|
||||
appendTextElement(document, stateElement, "description", snapshot.getDescription());
|
||||
appendTextElement(document, stateElement, "result", snapshot.getResult());
|
||||
appendTextElement(document, stateElement, "source_user", snapshot.getSource());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String emittedAt() {
|
||||
return ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
+5
-1
@@ -29,7 +29,7 @@ public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Res
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中可能包含当前行动相关状态、近期交流轨迹、以及活跃记忆切片;
|
||||
- 一条任务消息,其中包含:
|
||||
- target_action:本次参数提取所面向的目标行动,用于帮助你判断上下文中哪些内容与当前提取直接相关;
|
||||
- target_action:本次参数提取所面向的目标行动,用于帮助你判断上下文中哪些内容与当前提取直接相关;其中 current_stage_description 表示本次 MetaAction 所属阶段的具体执行目标,应优先用于约束参数提取;
|
||||
- meta_action_info:该行动对应的说明,以及允许提取的参数列表。每个 <param name="..."> 节点的文本内容表示该参数的含义或期望内容。
|
||||
|
||||
你的任务:
|
||||
@@ -78,6 +78,9 @@ public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Res
|
||||
appendChildElement(document, root, "target_action", block -> {
|
||||
appendTextElement(document, block, "uuid", input.getTargetActionId());
|
||||
appendTextElement(document, block, "description", input.getTargetActionDesc());
|
||||
if (input.getCurrentStageDesc() != null && !input.getCurrentStageDesc().isBlank()) {
|
||||
appendTextElement(document, block, "current_stage_description", input.getCurrentStageDesc());
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
appendChildElement(document, root, "meta_action_info", element -> {
|
||||
@@ -99,6 +102,7 @@ public class ParamsExtractor extends AbstractAgentModule.Sub<ExtractorInput, Res
|
||||
return cognitionCapability.contextWorkspace()
|
||||
.resolve(List.of(
|
||||
ContextBlock.FocusedDomain.ACTION,
|
||||
ContextBlock.FocusedDomain.PERCEIVE,
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
))
|
||||
|
||||
+8
@@ -10,6 +10,7 @@ import java.util.Map;
|
||||
@Data
|
||||
@Builder
|
||||
public class CorrectorInput {
|
||||
private CheckMode checkMode;
|
||||
private String tendency;
|
||||
private String source;
|
||||
private String reason;
|
||||
@@ -18,11 +19,18 @@ public class CorrectorInput {
|
||||
|
||||
private Map<Integer, List<ActionChainItem>> actionChainOverview;
|
||||
|
||||
public enum CheckMode {
|
||||
PROCESS_CHECK,
|
||||
FINAL_CHECK
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class ActionChainItem {
|
||||
private String actionKey;
|
||||
private String description;
|
||||
private String stageDescription;
|
||||
private String status;
|
||||
private String result;
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -16,6 +16,11 @@ public class ExtractorInput {
|
||||
*/
|
||||
private String targetActionDesc;
|
||||
|
||||
/**
|
||||
* 当前执行阶段的 description
|
||||
*/
|
||||
private String currentStageDesc;
|
||||
|
||||
/**
|
||||
* 目标 MetaActionInfo
|
||||
*/
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ package work.slhaf.partner.module.action.executor.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -12,6 +13,7 @@ public class ExtractorResult {
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class ParamEntry {
|
||||
private String name;
|
||||
private String value;
|
||||
|
||||
+81
-129
@@ -10,7 +10,6 @@ import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.CommunicationBlockContent;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
@@ -19,6 +18,7 @@ import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAg
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.StateHintContent;
|
||||
import work.slhaf.partner.module.action.executor.ActionExecutor;
|
||||
import work.slhaf.partner.module.action.planner.evaluator.ActionEvaluator;
|
||||
import work.slhaf.partner.module.action.planner.evaluator.entity.EvaluatorInput;
|
||||
@@ -42,8 +42,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
|
||||
|
||||
private static final String IMMEDIATE_WATCHER_CRON = "0/5 * * * * ?";
|
||||
private static final String BLOCK_SOURCE = "action_planner_pending";
|
||||
private static final String TENDENCIES_EVALUATING_BLOCK_NAME = "tendencies_in_evaluating";
|
||||
private static final String TENDENCIES_EVALUATING_BLOCK_NAME = "pending_action_intentions";
|
||||
|
||||
private final ActionAssemblyHelper assemblyHelper = new ActionAssemblyHelper();
|
||||
|
||||
@@ -83,99 +82,89 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
return;
|
||||
}
|
||||
appendTendencyBlock(tendencies);
|
||||
evaluateTendency(context.getSource(), input, extractorResult);
|
||||
evaluateTendency(context.getSource(), extractorResult);
|
||||
}
|
||||
|
||||
private void appendTendencyBlock(List<String> tendencies) {
|
||||
String datetime = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||
buildTendenciesEvaluatingFullBlock(tendencies),
|
||||
buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
|
||||
buildTendenciesEvaluatingCompactBlock(tendencies, datetime),
|
||||
buildTendenciesEvaluatingAbstractBlock(tendencies, datetime),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
60,
|
||||
18,
|
||||
4
|
||||
60, 18, 4
|
||||
));
|
||||
|
||||
cognitionCapability.contextWorkspace().register(StateHintContent.createBlock(new StateHintContent(
|
||||
TENDENCIES_EVALUATING_BLOCK_NAME,
|
||||
"Partner intends to handle the latest input through actions. When replying to the user, present this as Partner preparing to take action, not as an internal evaluation process."
|
||||
) {
|
||||
@Override
|
||||
public void fillStateContent(@NotNull Document document, @NotNull Element stateElement) {
|
||||
appendListElement(document, stateElement, "action_intentions", "action_intention", tendencies);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildTendenciesEvaluatingAbstractBlock(List<String> tendencies, String datetime) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName()) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "datetime", datetime);
|
||||
appendTextElement(document, root, "abstract", "There are " + tendencies.size() + " related tendencies in evaluating.");
|
||||
appendTextElement(document, root, "abstract", "There are " + tendencies.size() + " action intentions that Partner is preparing to handle.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildTendenciesEvaluatingCompactBlock(List<String> tendencies, String datetime) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName()) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
int size = tendencies.size();
|
||||
boolean num = size > 3;
|
||||
appendTextElement(document, root, "datetime", datetime);
|
||||
appendTextElement(document, root, "state", "Partner is considering whether to do these");
|
||||
appendTextElement(document, root, "tendencies_count", size);
|
||||
appendListElement(document, root, num ? "tendencies_truncated" : "tendencies", "tendency", num ? tendencies.subList(0, 3) : tendencies);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull BlockContent buildTendenciesEvaluatingFullBlock(List<String> tendencies) {
|
||||
return new BlockContent(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName(), BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "Partner is considering whether to do these");
|
||||
appendListElement(document, root, "tendencies", "tendency", tendencies);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void evaluateTendency(String source, String input, ExtractorResult extractorResult) {
|
||||
private void evaluateTendency(String source, ExtractorResult extractorResult) {
|
||||
executor.execute(() -> {
|
||||
EvaluatorInput evaluatorInput = assemblyHelper.buildEvaluatorInput(extractorResult);
|
||||
List<EvaluatorResult> evaluatorResults = actionEvaluator.execute(evaluatorInput); // 并发操作均为访问
|
||||
handleEvaluatorResults(evaluatorResults, source, input);
|
||||
handleEvaluatorResults(evaluatorResults, source);
|
||||
|
||||
cognitionCapability.contextWorkspace().expire(TENDENCIES_EVALUATING_BLOCK_NAME, getModuleName());
|
||||
});
|
||||
}
|
||||
|
||||
private void handleEvaluatorResults(List<EvaluatorResult> evaluatorResults, String source, String input) {
|
||||
List<ExecutableAction> passedActions = new ArrayList<>();
|
||||
int approvedExecutableCount = 0;
|
||||
int pendingConfirmCount = 0;
|
||||
private void handleEvaluatorResults(List<EvaluatorResult> evaluatorResults, String source) {
|
||||
boolean hasPendingConfirmation = false;
|
||||
List<String> refusedTendencies = new ArrayList<>();
|
||||
for (EvaluatorResult evaluatorResult : evaluatorResults) {
|
||||
expireResolvedPending(evaluatorResult);
|
||||
if (!evaluatorResult.isOk()) {
|
||||
refusedTendencies.add(evaluatorResult.getTendency());
|
||||
continue;
|
||||
}
|
||||
ExecutableAction executableAction = assemblyHelper.buildActionData(evaluatorResult, source);
|
||||
if (executableAction == null) {
|
||||
continue;
|
||||
}
|
||||
passedActions.add(executableAction);
|
||||
if (evaluatorResult.isNeedConfirm()) {
|
||||
registerPendingContextBlock(executableAction, evaluatorResult, input);
|
||||
pendingConfirmCount++;
|
||||
hasPendingConfirmation = true;
|
||||
registerPendingContextBlock(executableAction, evaluatorResult);
|
||||
continue;
|
||||
}
|
||||
executeOrSchedule(executableAction);
|
||||
approvedExecutableCount++;
|
||||
}
|
||||
if (passedActions.isEmpty()) {
|
||||
return;
|
||||
if (!refusedTendencies.isEmpty()) {
|
||||
cognitionCapability.initiateTurn("Some candidate action tendencies were refused during evaluation. Only reply if the user has not already received a suitable response and one of the refused tendencies was explicitly promised or implied as executable. If a response has already been given, or no promise was made, respond with NO_REPLY.", source, getModuleName());
|
||||
}
|
||||
if (hasPendingConfirmation) {
|
||||
cognitionCapability.initiateTurn("Some actions are pending user confirmation. Ask the user to confirm before executing them.", source, getModuleName());
|
||||
}
|
||||
createTurn(approvedExecutableCount, pendingConfirmCount, source, input);
|
||||
}
|
||||
|
||||
private void createTurn(int approvedExecutableCount, int pendingConfirmCount, String source, String input) {
|
||||
String turnInput = approvedExecutableCount + " actions are approved for execution, " +
|
||||
pendingConfirmCount + " actions are waiting for confirmation, " +
|
||||
"according to input: " + trimInput(input) + ". For more information, please refer to the context content or other tags in this input block.";
|
||||
cognitionCapability.initiateTurn(turnInput, source, getModuleName());
|
||||
}
|
||||
|
||||
private void expireResolvedPending(EvaluatorResult evaluatorResult) {
|
||||
@@ -192,47 +181,30 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
);
|
||||
}
|
||||
|
||||
private void registerPendingContextBlock(ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
|
||||
String blockName = buildPendingBlockName(executableAction);
|
||||
input = trimInput(input);
|
||||
ContextBlock block = new ContextBlock(
|
||||
buildPendingBlock(blockName, executableAction, evaluatorResult),
|
||||
buildPendingCompactBlock(blockName, executableAction, evaluatorResult, input),
|
||||
buildPendingAbstractBlock(blockName, executableAction, evaluatorResult, input),
|
||||
Set.of(ContextBlock.FocusedDomain.ACTION),
|
||||
30,
|
||||
10,
|
||||
5
|
||||
);
|
||||
cognitionCapability.contextWorkspace().register(block);
|
||||
}
|
||||
|
||||
private String buildPendingBlockName(ExecutableAction executableAction) {
|
||||
return "pending_action-" + executableAction.getUuid();
|
||||
}
|
||||
|
||||
private BlockContent buildPendingBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult) {
|
||||
return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
|
||||
private void registerPendingContextBlock(ExecutableAction executableAction, EvaluatorResult evaluatorResult) {
|
||||
cognitionCapability.contextWorkspace().register(StateHintContent.createBlock(new StateHintContent(
|
||||
"actions_need_confirmation",
|
||||
"Partner is waiting for user confirmation before executing a pending action."
|
||||
) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "need_user_confirm");
|
||||
appendTextElement(document, root, "action_uuid", executableAction.getUuid());
|
||||
appendTextElement(document, root, "action_type", evaluatorResult.getType());
|
||||
appendTextElement(document, root, "tendency", executableAction.getTendency());
|
||||
appendTextElement(document, root, "reason", executableAction.getReason());
|
||||
appendTextElement(document, root, "description", executableAction.getDescription());
|
||||
appendTextElement(document, root, "source_user", executableAction.getSource());
|
||||
public void fillStateContent(@NotNull Document document, @NotNull Element stateElement) {
|
||||
appendTextElement(document, stateElement, "state", "need_user_confirm");
|
||||
appendTextElement(document, stateElement, "action_type", evaluatorResult.getType());
|
||||
appendTextElement(document, stateElement, "tendency", executableAction.getTendency());
|
||||
appendTextElement(document, stateElement, "reason", executableAction.getReason());
|
||||
appendTextElement(document, stateElement, "description", executableAction.getDescription());
|
||||
appendTextElement(document, stateElement, "source_user", executableAction.getSource());
|
||||
EvaluatorResult.ScheduleData scheduleData = evaluatorResult.getScheduleData();
|
||||
if (scheduleData != null) {
|
||||
appendTextElement(document, root, "schedule_type", scheduleData.getType());
|
||||
appendTextElement(document, root, "schedule_content", scheduleData.getContent());
|
||||
appendTextElement(document, stateElement, "schedule_type", scheduleData.getType());
|
||||
appendTextElement(document, stateElement, "schedule_content", scheduleData.getContent());
|
||||
}
|
||||
Map<Integer, List<String>> primaryActionChain = evaluatorResult.getPrimaryActionChainAsMap();
|
||||
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Element chainRoot = document.createElement("primary_action_chain");
|
||||
root.appendChild(chainRoot);
|
||||
stateElement.appendChild(chainRoot);
|
||||
List<Integer> orders = new ArrayList<>(primaryActionChain.keySet());
|
||||
orders.sort(Integer::compareTo);
|
||||
for (Integer order : orders) {
|
||||
@@ -247,33 +219,7 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private BlockContent buildPendingCompactBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
|
||||
return new CommunicationBlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH, CommunicationBlockContent.Projection.SUPPLY) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "need_user_confirm");
|
||||
appendTextElement(document, root, "related_input", input);
|
||||
appendTextElement(document, root, "tendency", executableAction.getTendency());
|
||||
appendTextElement(document, root, "description", executableAction.getDescription());
|
||||
appendTextElement(document, root, "action_type", evaluatorResult.getType());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private BlockContent buildPendingAbstractBlock(String blockName, ExecutableAction executableAction, EvaluatorResult evaluatorResult, String input) {
|
||||
return new BlockContent(blockName, BLOCK_SOURCE, BlockContent.Urgency.HIGH) {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendTextElement(document, root, "state", "need_user_confirm");
|
||||
appendTextElement(document, root, "related_input", input);
|
||||
appendTextElement(document, root, "pending_tendency", executableAction.getTendency());
|
||||
appendTextElement(document, root, "summary", "exists pending action waiting for confirmation");
|
||||
appendTextElement(document, root, "action_type", evaluatorResult.getType());
|
||||
}
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
private void executeOrSchedule(ExecutableAction executableAction) {
|
||||
@@ -306,7 +252,6 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
if (!notified.compareAndSet(false, true)) {
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
watcherSelfTalk(action);
|
||||
return Unit.INSTANCE;
|
||||
})
|
||||
);
|
||||
@@ -314,34 +259,11 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
actionScheduler.schedule(watcher);
|
||||
}
|
||||
|
||||
private void watcherSelfTalk(ImmediateExecutableAction action) {
|
||||
String result = action.getResult();
|
||||
String structuredSignal = String.format(
|
||||
"{event=immediate_action_finished,actionUuid=%s,tendency=%s,status=%s,source=%s,result=%s}",
|
||||
action.getUuid(),
|
||||
action.getTendency(),
|
||||
action.getStatus(),
|
||||
action.getSource(),
|
||||
result == null ? "" : result //将会在 ActionExecutor
|
||||
);
|
||||
try {
|
||||
cognitionCapability.initiateTurn(structuredSignal, action.getSource());
|
||||
} catch (Exception e) {
|
||||
log.warn("触发 immediate 行动完成自对话失败, actionUuid: {}", action.getUuid(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
private String trimInput(@NotNull String input) {
|
||||
input = input.trim();
|
||||
input = input.length() <= 100 ? input : input.substring(0, 100);
|
||||
return input;
|
||||
}
|
||||
|
||||
private final class ActionAssemblyHelper {
|
||||
private ActionAssemblyHelper() {
|
||||
}
|
||||
@@ -357,7 +279,7 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
if (actionChain == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (evaluatorResult.getType()) {
|
||||
ExecutableAction executableAction = switch (evaluatorResult.getType()) {
|
||||
case PLANNING -> new SchedulableExecutableAction(
|
||||
evaluatorResult.getTendency(),
|
||||
actionChain,
|
||||
@@ -375,6 +297,27 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
userId
|
||||
);
|
||||
};
|
||||
executableAction.getStageDescriptions().putAll(getStageDescriptions(evaluatorResult));
|
||||
return executableAction;
|
||||
}
|
||||
|
||||
private Map<Integer, String> getStageDescriptions(EvaluatorResult evaluatorResult) {
|
||||
Map<Integer, String> descriptions = new HashMap<>();
|
||||
List<EvaluatorResult.ChainElement> primaryActionChain = evaluatorResult.getPrimaryActionChain();
|
||||
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
|
||||
return descriptions;
|
||||
}
|
||||
List<EvaluatorResult.ChainElement> orderedChain = new ArrayList<>(primaryActionChain);
|
||||
orderedChain.sort(Comparator.comparing(EvaluatorResult.ChainElement::getOrder, Comparator.nullsLast(Integer::compareTo)));
|
||||
int fixedOrder = 1;
|
||||
for (EvaluatorResult.ChainElement chainElement : orderedChain) {
|
||||
String description = chainElement.getDescription();
|
||||
if (description != null && !description.isBlank()) {
|
||||
descriptions.put(fixedOrder, description);
|
||||
}
|
||||
fixedOrder++;
|
||||
}
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
private Map<Integer, List<MetaAction>> getActionChain(EvaluatorResult evaluatorResult) {
|
||||
@@ -399,6 +342,9 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
}
|
||||
|
||||
private boolean fixDependencies(Map<Integer, List<String>> primaryActionChain) {
|
||||
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 先将 primaryActionChain 的节点序号修正为从1开始依次增大
|
||||
fixOrder(primaryActionChain);
|
||||
List<Integer> fixedOrders = new ArrayList<>(primaryActionChain.keySet().stream().toList());
|
||||
@@ -409,6 +355,9 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
for (Integer fixedOrder : fixedOrders) {
|
||||
int lastOrder = fixedOrder - 1;
|
||||
List<String> actionKeys = primaryActionChain.get(fixedOrder);
|
||||
if (actionKeys == null || actionKeys.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (String actionKey : actionKeys) {
|
||||
// 根据 actionKey 加载行动信息,并检查是否存在必需前置依赖
|
||||
|
||||
@@ -451,9 +400,12 @@ public class ActionPlanner extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
private void fixOrder(Map<Integer, List<String>> primaryActionChain) {
|
||||
Map<Integer, List<String>> tempChain = new HashMap<>(primaryActionChain);
|
||||
primaryActionChain.clear();
|
||||
int chainSize = tempChain.size();
|
||||
for (int i = 0; i < chainSize; i++) {
|
||||
primaryActionChain.put(i, tempChain.get(i));
|
||||
List<Integer> orders = new ArrayList<>(tempChain.keySet());
|
||||
orders.sort(Integer::compareTo);
|
||||
int fixedOrder = 1;
|
||||
for (Integer order : orders) {
|
||||
List<String> actionKeys = tempChain.get(order);
|
||||
primaryActionChain.put(fixedOrder++, actionKeys == null ? new ArrayList<>() : new ArrayList<>(actionKeys));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+164
-40
@@ -26,51 +26,174 @@ import java.util.concurrent.ExecutorService;
|
||||
|
||||
public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, List<EvaluatorResult>> implements ActivateModel {
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责评估单条行动倾向,并判断它是否值得进入后续行动链。
|
||||
你负责评估单条行动倾向 tendency,并判断它在当前系统状态下是否可以进入后续行动链。
|
||||
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中可能包含当前活跃的行动相关状态、近期交流轨迹、认知相关上下文、以及当前活跃记忆切片;
|
||||
- 一组 <available_meta_actions>,表示当前可用的 MetaAction 候选;
|
||||
- 一条最新的 user message,其内容就是当前需要评估的单条 tendency。
|
||||
- 一条结构化上下文消息,其中可能包含 action、communication、perceive、memory、cognition 等上下文;
|
||||
- 一组 <available_meta_actions>,表示当前系统真实可用的 MetaAction 候选;
|
||||
- 一条 user message,其内容是上游 ActionExtractor 提取出的单条 tendency。
|
||||
|
||||
重要前提:
|
||||
- 你的输入 tendency 已经由 ActionExtractor 根据当前输入和上下文提取完成。
|
||||
- 你不负责重新判断 tendency 是否来自最新用户输入。
|
||||
- 你不负责重新做意图抽取、指代解析或上下文对齐。
|
||||
- recent_chatmessage 中的最后一条不一定是最新输入,可能只是上一轮对话记录;不要依赖它来否定 tendency。
|
||||
- 除非上下文中存在明确证据表明 tendency 已过期、已被取消、已被覆盖或与当前行动状态冲突,否则应把 tendency 当作当前待评估的候选行动。
|
||||
|
||||
你的任务:
|
||||
- 判断该 tendency 是否成立、是否值得推进;
|
||||
- 判断它更适合立即执行,还是先进入规划;
|
||||
- 判断是否需要先向用户确认;
|
||||
- 若可推进,则基于 available_meta_actions 生成 primaryActionChain;
|
||||
- 若当前 tendency 与某个待处理 pending 明确对应,且本轮已完成承接或推进,应正确填写 resolvedPending;
|
||||
- 若当前 tendency 明确包含可调度语义,再填写 scheduleData。
|
||||
- 判断该 tendency 在当前系统状态下是否可推进;
|
||||
- 判断完成该 tendency 是否需要行动链;
|
||||
- 判断 available_meta_actions 是否能承接该 tendency;
|
||||
- 判断是否需要用户确认;
|
||||
- 判断该 tendency 更适合立即执行还是进入规划;
|
||||
- 输出严格符合 EvaluatorResult 的评估结果。
|
||||
|
||||
评估原则:
|
||||
- 结合上下文理解当前 tendency 与近期交流、当前行动状态、活跃记忆之间的关系。
|
||||
- 若 tendency 与已有正在执行、等待确认、或已明确覆盖的行动完全等价,通常不应重复建立新的行动链。
|
||||
- 若 tendency 是对已有待确认行动的确认、拒绝、补充条件、修改要求或继续推进,则应优先视为对原有行动状态的承接,而不是无关新任务。
|
||||
- 若 action 相关上下文中存在等待确认的 block,且当前 tendency 明显与其相关,则必须显式承接这一点,不要因为其已存在于上下文中而省略。
|
||||
- 只有在 tendency 明确具有可执行意义时,才返回 ok=true。
|
||||
- 若 tendency 更适合直接交流回应,或尚不足以形成行动推进,则返回 ok=false。
|
||||
- primaryActionChain 中只能使用 available_meta_actions 内出现的 action_key,不要编造不存在的动作。
|
||||
- 不要输出完整执行细节、自然语言计划正文或额外解释,只输出 EvaluatorResult 对应结构。
|
||||
核心原则:
|
||||
- 不要把“需要确认”误判成“不可执行”。
|
||||
- 不要把“有风险”误判成“用户没有要求”。
|
||||
- 不要把“能力不足”误判成“用户没有要求”。
|
||||
- 不要因为 recent_chatmessage 没有出现对应内容就否定 tendency。
|
||||
- 不要根据上一轮聊天内容推翻当前 tendency。
|
||||
- 只有在可行性、能力、风险、状态覆盖、信息完整性等评估层面拒绝 tendency。
|
||||
|
||||
关于字段:
|
||||
- ok 表示该 tendency 是否值得进入后续行动推进。
|
||||
- needConfirm 表示在真正推进前是否必须先得到用户确认。
|
||||
- type:
|
||||
- IMMEDIATE: 可直接进入即时执行链;
|
||||
- PLANNING: 需要先形成或进入规划链,再决定后续执行。
|
||||
- primaryActionChain 表示按 order 分组的候选动作链,每个元素包含:
|
||||
- order: 执行顺序
|
||||
- actionKeys: 该顺序下的 action_key 列表
|
||||
- reason 用于说明你为何做出该判断,应简洁明确。
|
||||
- description 用于概括本次行动评估结果,应能帮助后续模块快速理解该 tendency 的推进方向。
|
||||
- scheduleData 仅在该 tendency 明确包含可调度语义时填写;否则留空。
|
||||
- scheduleData.type: 一次性计划或周期性计划
|
||||
- scheduleData.content: 符合 Quartz 标准的 Cron 表达式
|
||||
- resolvedPending 仅在你能明确判断当前 tendency 已承接某个 pending block 时填写;否则留空。
|
||||
- 当 ok=false 时,type、primaryActionChain、scheduleData、resolvedPending 通常应留空或保持无效默认值,不要强行填充。
|
||||
决策流程:
|
||||
|
||||
0. 优先处理 pending action 的确认、拒绝、修改或取消
|
||||
|
||||
- 如果 action 上下文中存在等待用户确认的 pending action,且 tendency 表达的是确认、同意、继续、执行、可以、拒绝、取消、修改、补充条件等承接行为,应优先按 pending action 承接处理。
|
||||
- 这类 tendency 不是新的独立行动,不要为“确认、拒绝、继续”本身寻找 available_meta_actions。
|
||||
- 不要因为 pending action 有副作用就再次设置 needConfirm=true;用户确认 pending action 时,needConfirm 必须为 false,避免确认循环。
|
||||
- 若 tendency 表示确认、同意、继续或执行 pending action:
|
||||
- 应返回 ok=true;
|
||||
- needConfirm=false;
|
||||
- resolvedPending 应填写对应 pending block 的 blockName 与 source;
|
||||
- primaryActionChain 应从 pending action 的 primary_action_chain 承接;
|
||||
- type 应从 pending action 的 action_type 承接;
|
||||
- scheduleData 应从 pending action 的 schedule 信息承接;若 pending action 没有调度信息则留空;
|
||||
- reason 应说明用户已确认该 pending action,现在可以执行。
|
||||
- 若 tendency 表示拒绝、取消或不执行 pending action:
|
||||
- 应填写 resolvedPending;
|
||||
- ok=false;
|
||||
- reason 应说明用户取消了 pending action,因此不再推进;
|
||||
- 不要生成 primaryActionChain。
|
||||
- 若 tendency 表示修改 pending action、补充条件或改变目标:
|
||||
- 应填写 resolvedPending;
|
||||
- 不要把它当作全新无关任务;
|
||||
- 若修改后的目标足够明确且 available_meta_actions 可承接,则按修改后的行动返回 ok=true;
|
||||
- 若信息不足,则 ok=false,并说明需要澄清。
|
||||
- 只有当 action 上下文中不存在相关 pending action,或 tendency 明显不是对 pending action 的承接时,才继续后续可行性评估。
|
||||
|
||||
1. 判断是否已经被现有 action 状态覆盖
|
||||
|
||||
- 若 tendency 与已有正在执行、等待确认、已完成且仍有效的 action 完全等价,通常不应重复建立行动链。
|
||||
- 若 tendency 与现有 action 相关但不是重复,而是追加约束、改变目标或要求继续,应按新的行动需求评估。
|
||||
- 若拒绝是因为已有 action 覆盖,ok=false,并在 reason 中说明覆盖关系。
|
||||
- 不要仅因为 conversation 中存在相似历史内容就认为已覆盖;必须有 action 状态证据。
|
||||
|
||||
2. 判断是否需要行动链
|
||||
|
||||
按照完成条件判断,而不是按行为类别死板判断:
|
||||
|
||||
- 如果完成 tendency 需要系统获取当前状态、访问资源、调用能力、操作对象、改变状态、触发过程、安排未来动作或向外部系统发出请求,则通常需要行动链。
|
||||
- 如果完成 tendency 只需要基于已有上下文进行解释、分析、总结、评价、翻译、改写、闲聊或普通交流,且不需要任何系统能力介入,则通常不需要行动链。
|
||||
- 如果 tendency 的完成结果依赖“实际执行后发生什么”“当前对象真实状态是什么”“外部资源现在是什么结果”,则通常需要行动链。
|
||||
- 如果用户目标本身是让系统代为完成某件事,而不是只要建议、说明或解释,则通常需要行动链。
|
||||
|
||||
这里的“对象”可以是任何可被系统能力作用的东西,包括本地资源、远程资源、应用状态、文件系统、运行环境、记忆、计划、任务、接口、页面、数据、设备或会话状态。
|
||||
|
||||
这里的“产生影响”包括改变对象状态、创建结果、触发过程、安排未来动作或向外部系统发出请求。
|
||||
|
||||
不要试图枚举所有用户行为;依据“是否需要系统能力介入才能完成”来判断。
|
||||
|
||||
3. 判断 available_meta_actions 是否能承接
|
||||
|
||||
ok=true 的必要条件:
|
||||
- tendency 当前没有被已有 action 状态覆盖;
|
||||
- 完成 tendency 需要行动链;
|
||||
- available_meta_actions 中存在能承接该 tendency 的 action_key;
|
||||
- 可以生成非空 primaryActionChain。
|
||||
|
||||
规则:
|
||||
- ok=true 时 primaryActionChain 必须非空。
|
||||
- primaryActionChain 只能使用 available_meta_actions 中真实存在的 action_key。
|
||||
- 不要编造 action_key。
|
||||
- 对 pending action 的确认行为,不要为“确认”本身寻找 action_key;应从 pending action 中承接原 action chain。
|
||||
- 如果没有任何可用能力能承接,即使 tendency 本身合理,也必须 ok=false。
|
||||
- 如果无法承接,reason 应说明缺少能力、能力不匹配、信息不足或策略限制,不要说用户没有要求。
|
||||
|
||||
4. 判断信息是否足够
|
||||
|
||||
- 若 tendency 的目标、对象、范围、参数或约束足够明确,可以继续评估可执行性。
|
||||
- 若缺少必要信息,但可以通过向用户确认或澄清补足,应 ok=false,并说明需要澄清。
|
||||
- 若缺少的信息可以通过已有 action/context/pending 状态明确补足,则可以继续推进。
|
||||
- 不要因为 tendency 中的信息来自上游解析或上下文补全就认为信息不足;只判断当前 tendency 自身是否足以形成行动链。
|
||||
|
||||
5. 判断是否需要确认
|
||||
|
||||
needConfirm 判断的是“是否需要先获得用户确认”,不是“是否能执行”。
|
||||
|
||||
- needConfirm=true 仍然可以 ok=true。
|
||||
- 如果当前 tendency 是对已有 pending action 的确认、同意、继续或执行,needConfirm 必须为 false;确认行为不能再次触发确认。
|
||||
- 若行动可能产生副作用、不可逆影响、权限风险、隐私风险、安全风险、资源消耗、外部可见影响、长期运行影响,或用户可能希望先确认,则应 needConfirm=true。
|
||||
- 若行动是低风险、短时、可逆、范围明确、授权清晰,且系统策略允许直接推进,则可 needConfirm=false。
|
||||
- 如果风险或目标不确定到无法形成可确认行动,应 ok=false,并说明需要澄清或缺少必要信息。
|
||||
- 不要因为行动有副作用就直接拒绝;先判断是否可以通过 needConfirm 承接。
|
||||
- 不要把“需要确认”写成“无法执行”。
|
||||
|
||||
6. 判断 type
|
||||
|
||||
- IMMEDIATE:目标清楚,能力链清楚,可以直接由当前 action chain 推进。
|
||||
- PLANNING:目标需要拆解、多步协调、长期推进、条件判断或中间决策。
|
||||
- ok=false 时,type 不应表达可执行状态;若结构必须填默认值,不要在 reason/description 中暗示会执行。
|
||||
|
||||
7. 输出字段要求
|
||||
|
||||
primaryActionChain:
|
||||
- 只在 ok=true 时填写。
|
||||
- 每个元素包含 order、description 和 actionKeys。
|
||||
- description 是该 stage 的具体执行目标,用一句短句说明本阶段要获得、检查、修改或触发什么;它不是用户回复文案,也不是完整推理过程。
|
||||
- 不要写伪代码。
|
||||
- 若 tendency 包含多个依赖步骤、需要先获取 A 再根据 A 决定 B、需要多次调用同一能力处理不同对象,或需要为后续汇报获取多类事实,应输出多 order 的 primaryActionChain。
|
||||
- 不要为了减少步骤而把复杂诊断、查找、读取、分析全部压缩成单个 command::execute。
|
||||
- 同一个 available_meta_action 可以在不同 order 中重复使用,只要每次对应的阶段目标、参数或对象不同。
|
||||
- 对需要最终汇报的任务,行动链应负责获取支撑汇报所需的事实;最终自然语言汇报由 Communication 基于 action-finished state 完成,不需要把“汇报”本身作为一个 MetaAction。
|
||||
|
||||
scheduleData:
|
||||
- 仅当 tendency 明确要求未来、周期、延迟、提醒、定时或计划安排时填写。
|
||||
- 没有调度语义时留空。
|
||||
- 不要为了完整性强行填写。
|
||||
|
||||
resolvedPending:
|
||||
- 仅当当前 tendency 明确承接某个 pending block 时填写。
|
||||
- 无法明确对应时留空。
|
||||
- 不要猜测。
|
||||
|
||||
reason:
|
||||
- 说明可行性判断依据。
|
||||
- 若 ok=true,说明为什么当前可以推进、使用能力链是否充分、是否需要确认。
|
||||
- 若 ok=false,必须说明真正失败的层级:
|
||||
- 不需要行动链,直接交流即可;
|
||||
- 已有 action 覆盖;
|
||||
- 缺少可用 MetaAction;
|
||||
- action_key 无法承接;
|
||||
- 必要信息不足;
|
||||
- 指代或目标不明确;
|
||||
- 风险/权限/策略限制导致无法推进;
|
||||
- 需要澄清。
|
||||
- 不得把“能力不足”“需要确认”“有风险”“信息不足”“上下文缺失”写成“用户没有要求”。
|
||||
- 对确认类 tendency,不得输出“缺少确认动作能力”“确认本身不需要行动链”作为拒绝理由;应判断是否能承接 pending action。
|
||||
- 不得基于 recent_chatmessage 的上一轮内容否定 tendency。
|
||||
- 不得输出“用户当前并未提出该要求”这类意图抽取判断,除非上下文中存在明确取消、冲突或过期证据。
|
||||
|
||||
description:
|
||||
- 简短概括处理结论。
|
||||
- 若 ok=true,描述将要推进的行动。
|
||||
- 若 needConfirm=true,描述应体现该行动需要确认。
|
||||
- 若 ok=false,描述应体现不推进的真实原因。
|
||||
|
||||
输出要求:
|
||||
- 严格按照 EvaluatorResult 对应结构输出。
|
||||
- 不要输出结构之外的解释、注释或额外文本。
|
||||
- 不要输出结构之外的解释、注释、推理过程或额外文本。
|
||||
""";
|
||||
|
||||
@InjectCapability
|
||||
@@ -104,8 +227,9 @@ public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, Lis
|
||||
cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.ACTION,
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.COGNITION,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
ContextBlock.FocusedDomain.PERCEIVE,
|
||||
ContextBlock.FocusedDomain.MEMORY,
|
||||
ContextBlock.FocusedDomain.COGNITION
|
||||
)).encodeToMessage(),
|
||||
availableMetaActionContext(),
|
||||
new Message(Message.Character.USER, tendency)
|
||||
@@ -144,8 +268,8 @@ public class ActionEvaluator extends AbstractAgentModule.Sub<EvaluatorInput, Lis
|
||||
"available_meta_action",
|
||||
actionCapability.listAvailableMetaActions().entrySet(),
|
||||
(block, value) -> {
|
||||
appendTextElement(document, root, "action_key", value.getKey());
|
||||
appendTextElement(document, root, "action_value", value.getValue().getDescription());
|
||||
appendTextElement(document, block, "meta_action_key", value.getKey());
|
||||
appendTextElement(document, block, "meta_action_description", value.getValue().getDescription());
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
);
|
||||
|
||||
+9
-1
@@ -3,6 +3,7 @@ package work.slhaf.partner.module.action.planner.evaluator.entity;
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.core.action.entity.Schedulable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -31,9 +32,15 @@ public class EvaluatorResult {
|
||||
}
|
||||
|
||||
public Map<Integer, List<String>> getPrimaryActionChainAsMap() {
|
||||
if (primaryActionChain == null || primaryActionChain.isEmpty()) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
return primaryActionChain.stream().collect(Collectors.toMap(
|
||||
ChainElement::getOrder,
|
||||
ChainElement::getActionKeys,
|
||||
chainElement -> {
|
||||
List<String> actionKeys = chainElement.getActionKeys();
|
||||
return actionKeys == null ? new ArrayList<>() : new ArrayList<>(actionKeys);
|
||||
},
|
||||
(oldValue, newValue) -> newValue,
|
||||
LinkedHashMap::new
|
||||
));
|
||||
@@ -48,6 +55,7 @@ public class EvaluatorResult {
|
||||
@Data
|
||||
public static class ChainElement {
|
||||
private Integer order;
|
||||
private String description;
|
||||
private List<String> actionKeys;
|
||||
}
|
||||
}
|
||||
|
||||
+42
-8
@@ -17,19 +17,52 @@ import java.util.List;
|
||||
public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<ExtractorResult>> implements ActivateModel {
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你负责从当前输入中提取可能的行动倾向,供后续模块继续评估。
|
||||
你负责从当前输入中提取“可能值得进入后续行动评估”的行动倾向。
|
||||
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,主要包含 communication 域与 action 域内容;
|
||||
- 一条结构化上下文消息,主要包含 communication 域、perceive 域与 action 域内容;
|
||||
- 一条最新输入,作为本轮重点分析对象。
|
||||
|
||||
规则:
|
||||
- communication 域主要用于理解当前会话语境、主题延续与用户意图。
|
||||
- action 域主要用于判断相关行动是否已经在执行、等待确认,或已被覆盖。
|
||||
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤或工具细节。
|
||||
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息或条件变化,应避免重复提出。
|
||||
你的任务:
|
||||
- 基于当前输入与上下文,提取少量、明确、值得继续进入行动评估的行动倾向;
|
||||
- 这里只提取“可能需要通过行动推进”的倾向,而不是所有可回应的意图;
|
||||
- 若当前输入更适合直接交流回应,而不需要额外行动推进,则返回空结果。
|
||||
|
||||
基本原则:
|
||||
- communication 域主要用于理解当前会话语境、话题延续、指代对象与用户真实意图。
|
||||
- perceive 域可用于理解当前系统状态、环境信息、时间信息等;若当前输入仅是在询问这些已可直接回答的信息,不应因此提取行动倾向。
|
||||
- action 域主要用于判断相关行动是否已经在执行、等待确认、或已被覆盖。
|
||||
- 只提取“可能值得进入后续评估的行动倾向”,不要输出完整行动计划、执行步骤、工具细节或命令内容。
|
||||
- 若某个倾向已经明显处于执行中,且当前输入没有带来新的推进信息、修正信息、确认信息或条件变化,应避免重复提出。
|
||||
- 若 action 域中存在等待确认的 block,且当前输入与其相关,则必须提取出对应的行动倾向;不要因为其已存在于上下文中而省略。
|
||||
- 若当前输入没有明显行动倾向,可返回空结果。
|
||||
|
||||
复合任务合并规则:
|
||||
- 若当前输入包含“先...再...最后...”“并且/然后/同时”等多个动作描述,但这些动作共同服务于同一个最终目标,应提取为一个整体行动倾向,而不是拆成多个并列倾向。
|
||||
- 中间的信息获取、检查、读取、查询步骤如果只是为了支撑最终判断、汇报、生成结果,不应作为独立 tendency 输出。
|
||||
- 最终的“总结、判断、汇报、说明结果”如果依赖前置行动结果,应并入同一个整体 tendency,而不是作为单独的 communication 类 tendency 输出。
|
||||
- 只有当多个动作目标彼此独立、可以分别完成且不共享同一个最终目的时,才输出多个 tendency。
|
||||
|
||||
什么情况下应提取行动倾向:
|
||||
- 用户明确要求系统执行某个动作、调用某项能力、访问某类外部信息、操作某个对象或推进某个待办;
|
||||
- 用户要求修改、继续、取消、确认或补充某个已有待处理行动;
|
||||
- 用户明确要求系统代其完成某件事,而不是仅作解释、说明或回答;
|
||||
- 若不进入行动链,则该请求无法被真正推进或完成。
|
||||
|
||||
什么情况下不应提取行动倾向:
|
||||
- 当前输入本质上是一个可直接回答的提问;
|
||||
- 当前输入只是要求解释、分析、总结、说明、翻译、评价、闲聊或感叹;
|
||||
- 当前输入只是询问当前上下文、系统状态、已知信息、记忆内容或当前可见内容,而这些内容可直接通过交流给出;
|
||||
- 当前输入只是一般性的信息查询,但没有明确要求系统通过外部行动补充信息;
|
||||
- 当前输入只是对已有回复的轻量追问、澄清或自然延续,仍然更适合直接回应;
|
||||
- 当前输入语义模糊,仅凭“可能有东西可做”不足以进入行动评估;
|
||||
- 当前输入虽然提到某个对象或任务,但并未形成明确的行动推进意图。
|
||||
|
||||
重要限制:
|
||||
- 不要把“可以通过行动获得更多信息”当成提取理由。
|
||||
- 不要把“询问信息”自动等同于“要求去执行查询动作”。
|
||||
- 不要为了提高召回而泛化提取。
|
||||
- 如果更适合 communication 直接回答,就返回空结果。
|
||||
- 倾向应简洁明确,表达“要推进什么”,不要写成自然语言执行方案。
|
||||
|
||||
输出要求:
|
||||
- 严格按照 ExtractorResult 对应结构输出。
|
||||
@@ -44,6 +77,7 @@ public class ActionExtractor extends AbstractAgentModule.Sub<String, Result<Extr
|
||||
List<Message> messages = List.of(
|
||||
cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.COMMUNICATION,
|
||||
ContextBlock.FocusedDomain.PERCEIVE,
|
||||
ContextBlock.FocusedDomain.ACTION
|
||||
)).encodeToMessage(),
|
||||
new Message(Message.Character.USER, input)
|
||||
|
||||
+39
-9
@@ -15,12 +15,14 @@ import work.slhaf.partner.core.action.ActionCapability
|
||||
import work.slhaf.partner.core.action.entity.Action
|
||||
import work.slhaf.partner.core.action.entity.Schedulable
|
||||
import work.slhaf.partner.core.action.entity.SchedulableExecutableAction
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule
|
||||
import work.slhaf.partner.module.action.executor.ActionExecutor
|
||||
import java.io.Closeable
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
@@ -70,8 +72,9 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
val doneCondition: (Schedulable) -> Boolean = { schedulable ->
|
||||
if (schedulable is Action) {
|
||||
schedulable.status == Action.Status.FAILED || schedulable.status == Action.Status.SUCCESS
|
||||
} else {
|
||||
true
|
||||
}
|
||||
true
|
||||
}
|
||||
timeWheel = TimeWheel(listScheduledActions, onTrigger, doneCondition)
|
||||
}
|
||||
@@ -126,6 +129,12 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
val onTrigger: (toTrigger: Set<Schedulable>) -> Unit,
|
||||
val doneCondition: (schedulable: Schedulable) -> Boolean
|
||||
) : Closeable {
|
||||
private val tracePath: Path = ConfigCenter.paths.stateDir
|
||||
.resolve("trace")
|
||||
.resolve("time-wheel")
|
||||
.normalize()
|
||||
.toAbsolutePath()
|
||||
|
||||
private val schedulableGroupByHour = Array<MutableSet<Schedulable>>(24) { mutableSetOf() }
|
||||
private val wheel = Array<MutableSet<Schedulable>>(60 * 60) { mutableSetOf() }
|
||||
private var recordHour: Int = -1
|
||||
@@ -151,15 +160,14 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
logFailedStatus(schedulableData)
|
||||
return@checkThenExecute
|
||||
}
|
||||
log.debug("Action next execution time: {}", parseToZonedDateTime)
|
||||
log.debug("${schedulableData.uuid} next execution time: {}", parseToZonedDateTime)
|
||||
val hour = parseToZonedDateTime.hour
|
||||
schedulableGroupByHour[hour].add(schedulableData)
|
||||
log.debug("Action scheduled at {}", hour)
|
||||
if (it.hour == hour) {
|
||||
val wheelOffset = parseToZonedDateTime.minute * 60 + parseToZonedDateTime.second
|
||||
wheel[wheelOffset].add(schedulableData)
|
||||
state.value = WheelState.ACTIVE
|
||||
log.debug("Action scheduled at wheel offset {}", wheelOffset)
|
||||
log.debug("${schedulableData.uuid} scheduled at wheel offset {}", wheelOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,8 +209,13 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
val shouldBreak: Boolean
|
||||
)
|
||||
|
||||
fun collectToTrigger(tick: Int, previousTick: Int, triggerHour: Int): Set<Schedulable>? {
|
||||
if (tick > previousTick) {
|
||||
fun collectToTrigger(
|
||||
tick: Int,
|
||||
previousTick: Int,
|
||||
triggerHour: Int,
|
||||
includeRemainingHour: Boolean = false
|
||||
): Set<Schedulable>? {
|
||||
if (tick >= previousTick) {
|
||||
val toTrigger = mutableSetOf<Schedulable>()
|
||||
for (i in previousTick..tick) {
|
||||
val bucket = wheel[i]
|
||||
@@ -213,6 +226,13 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
bucket.clear() // 避免重复触发
|
||||
}
|
||||
}
|
||||
if (includeRemainingHour) {
|
||||
val remainingHourBucket = schedulableGroupByHour[triggerHour]
|
||||
if (remainingHourBucket.isNotEmpty()) {
|
||||
toTrigger.addAll(remainingHourBucket.filter { it.enabled })
|
||||
remainingHourBucket.clear()
|
||||
}
|
||||
}
|
||||
return toTrigger
|
||||
}
|
||||
return null
|
||||
@@ -261,7 +281,12 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
checkThenExecute(false) {
|
||||
if (it.hour != launchingHour) {
|
||||
shouldBreak = true
|
||||
toTrigger = collectToTrigger(wheel.lastIndex, previousTick, launchingHour)
|
||||
toTrigger = collectToTrigger(
|
||||
wheel.lastIndex,
|
||||
previousTick,
|
||||
launchingHour,
|
||||
includeRemainingHour = true
|
||||
)
|
||||
log.debug(
|
||||
"Hour changed, previousTick: {}, tick: {}, toTriggerSize: {}",
|
||||
previousTick,
|
||||
@@ -270,8 +295,13 @@ class ActionScheduler : AbstractAgentModule.Standalone() {
|
||||
)
|
||||
return@checkThenExecute
|
||||
}
|
||||
toTrigger = collectToTrigger(tick, previousTick, launchingHour)
|
||||
if (tick >= wheel.lastIndex || schedulableGroupByHour[launchingHour].isEmpty()) {
|
||||
toTrigger = collectToTrigger(
|
||||
tick,
|
||||
previousTick,
|
||||
launchingHour,
|
||||
includeRemainingHour = tick >= wheel.lastIndex
|
||||
)
|
||||
if (tick >= wheel.lastIndex) {
|
||||
state.value = WheelState.SLEEPING
|
||||
shouldBreak = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package work.slhaf.partner.module
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import work.slhaf.partner.common.base.Block
|
||||
import work.slhaf.partner.core.cognition.CommunicationBlockContent
|
||||
import work.slhaf.partner.core.cognition.ContextBlock
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message
|
||||
|
||||
abstract class TaskBlock @JvmOverloads constructor(
|
||||
blockName: String = "task_input"
|
||||
) : Block(blockName) {
|
||||
|
||||
fun encodeToMessage(): Message {
|
||||
return Message(Message.Character.USER, encodeToXmlString())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class StateHintContent protected constructor(
|
||||
source: String,
|
||||
val stateMsg: String
|
||||
) : CommunicationBlockContent("new_state", source, Urgency.NORMAL, Projection.SUPPLY) {
|
||||
|
||||
override fun fillXml(document: Document, root: Element) {
|
||||
appendTextElement(document, root, "state_msg", stateMsg)
|
||||
appendChildElement(document, root, "state_content") {
|
||||
fillStateContent(document, this@appendChildElement)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun fillStateContent(document: Document, stateElement: Element)
|
||||
|
||||
private fun toContextBlock(): ContextBlock {
|
||||
return ContextBlock(
|
||||
blockContent = this,
|
||||
focusedOn = setOf(ContextBlock.FocusedDomain.COGNITION),
|
||||
replaceFadeFactor = 60.0,
|
||||
timeFadeFactor = 100.0,
|
||||
activateFactor = 0.0
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createBlock(block: StateHintContent): ContextBlock {
|
||||
return block.toContextBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-6
@@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import work.slhaf.partner.core.cognition.*;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
@@ -29,7 +30,6 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRunningFlowContext> implements ActivateModel {
|
||||
|
||||
private static final String INTERRUPTED_MARKER = " [response interrupted due to internal exception]";
|
||||
private static final String NO_REPLY_MARKER = "NO_REPLY";
|
||||
private static final String AGENT_MARKER = "[[AGENT]: self]";
|
||||
private static final String NOT_REPLIED_PREFIX = "[NOT_REPLIED]";
|
||||
@@ -37,6 +37,10 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
|
||||
private static final String MODULE_PROMPT = """
|
||||
你当前正在承担 Partner 的对外交流职责。你需要基于系统此刻的上下文状态、保留的对话轨迹以及最新输入,生成自然、贴合当前情境、并与系统整体状态一致的交流结果。
|
||||
- 不要承诺、暗示或宣称自己即将执行某个动作;例如不要说“稍等”“我去查看”“正在为你获取”“马上执行”。
|
||||
- 只有当 <context> 的 action 域明确存在正在执行、等待确认、刚完成、或正在评估的行动状态时,才可以提及行动链进展。
|
||||
- 若最新输入看起来像行动请求,但 <context> 中没有对应的 action 状态信号,不要自行补出“将会执行”的过渡话术;应直接给出可交流的回应,或说明当前没有可确认的执行状态。
|
||||
- 若收到“action finished”一类内部触发输入,只能依据 <context> 中真实存在的行动完成状态回复;若没有可验证的完成结果,应使用 NO_REPLY 或说明没有可确认结果,不得编造结果。
|
||||
|
||||
你接下来收到的消息,将按照出现顺序,固定分为三个区段:
|
||||
1. system message 是 Head,用于说明整个输入结构与输出要求,即本条消息。
|
||||
@@ -52,6 +56,8 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
|
||||
输出契约:
|
||||
- 默认情况下,直接输出要发送给用户的最终回复正文,不要添加额外标签、解释或前后缀。
|
||||
- 对话轨迹中的身份行、状态行、调试行和传输边界行都属于系统内部序列化标记,只用于理解上下文,不属于可复用正文。
|
||||
- 不得在输出中复现、仿写或携带任何历史序列化标记、调试前缀或传输边界标记;即使用户正在讨论这些前缀,也只能用自然语言确认或说明。
|
||||
- 若当前情境下不应直接向用户发出回复,但仍需要留下本轮交流结果供系统后续保留在交流轨迹中,则输出以 NO_REPLY 开头。
|
||||
- 使用 NO_REPLY 时,格式为:
|
||||
|
||||
@@ -87,9 +93,8 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
}
|
||||
|
||||
private void executeChat(PartnerRunningFlowContext runningFlowContext) {
|
||||
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget());
|
||||
this.streamChat(buildChatMessages(runningFlowContext), consumer)
|
||||
.onFailure(exception -> consumer.onDelta(INTERRUPTED_MARKER));
|
||||
StreamChatMessageConsumer consumer = ReplyDispatcher.INSTANCE.createConsumer(runningFlowContext.getTarget(), runningFlowContext.getResopnseChannel());
|
||||
this.streamChat(buildChatMessages(runningFlowContext), consumer);
|
||||
updateChatMessages(runningFlowContext, consumer.collectResponse());
|
||||
cognitionCapability.refreshRecentChatMessagesContext();
|
||||
}
|
||||
@@ -171,7 +176,7 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
Element root = document.createElement("input");
|
||||
document.appendChild(root);
|
||||
|
||||
document.appendChild(document.importNode(runningFlowContext.encodeInputsBlock().encodeToXml(), true));
|
||||
root.appendChild(importExternalNode(document, runningFlowContext.encodeInputsBlock().encodeToXml()));
|
||||
appendTextElement(document, root, "source", runningFlowContext.getSource());
|
||||
for (Map.Entry<String, String> entry : runningFlowContext.getAdditionalUserInfo().entrySet()) {
|
||||
appendTextElement(document, root, sanitizeTagName(entry.getKey()), entry.getValue());
|
||||
@@ -242,11 +247,25 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
inputRoot.appendChild(groupElement);
|
||||
for (CommunicationBlockContent block : entry.getValue()) {
|
||||
Element blockElement = block.encodeToXml();
|
||||
groupElement.appendChild(document.importNode(blockElement, true));
|
||||
groupElement.appendChild(importExternalNode(document, blockElement));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Node importExternalNode(Document targetDocument, Node externalNode) {
|
||||
Node nodeToImport = externalNode;
|
||||
if (externalNode != null && externalNode.getNodeType() == Node.DOCUMENT_NODE) {
|
||||
nodeToImport = ((Document) externalNode).getDocumentElement();
|
||||
}
|
||||
if (nodeToImport == null) {
|
||||
throw new IllegalArgumentException("待导入节点不能为空");
|
||||
}
|
||||
if (nodeToImport.getOwnerDocument() == targetDocument) {
|
||||
return nodeToImport.cloneNode(true);
|
||||
}
|
||||
return targetDocument.importNode(nodeToImport, true);
|
||||
}
|
||||
|
||||
private String sanitizeTagName(String rawTagName) {
|
||||
if (rawTagName == null || rawTagName.isBlank()) {
|
||||
return "meta";
|
||||
|
||||
+4
-2
@@ -41,7 +41,8 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
private static final String AUTO_UPDATE_CRON = "0/10 * * * * ?";
|
||||
private static final long UPDATE_TRIGGER_INTERVAL = 60 * 60 * 1000;
|
||||
private static final int CONTEXT_RETAIN_DIVISOR = 6;
|
||||
private static final int DIALOG_ROLLING_TRIGGER_LIMIT = 36;
|
||||
private static final int FULL_ROLLING_RETAIN_DIVISOR = Integer.MAX_VALUE;
|
||||
private static final int DIALOG_ROLLING_TRIGGER_LIMIT = 72;
|
||||
|
||||
private final AtomicBoolean rolling = new AtomicBoolean(false);
|
||||
|
||||
@@ -119,7 +120,8 @@ public class DialogRolling extends AbstractAgentModule.Running<PartnerRunningFlo
|
||||
return;
|
||||
}
|
||||
|
||||
RollingResult result = buildRollingResult(chatIncrement, fullChatSnapshot.size(), CONTEXT_RETAIN_DIVISOR);
|
||||
int retainDivisor = refreshMemoryId ? FULL_ROLLING_RETAIN_DIVISOR : CONTEXT_RETAIN_DIVISOR;
|
||||
RollingResult result = buildRollingResult(chatIncrement, fullChatSnapshot.size(), retainDivisor);
|
||||
applyRolling(result);
|
||||
afterRollingRegistry.trigger(result);
|
||||
|
||||
|
||||
+159
-122
@@ -1,166 +1,203 @@
|
||||
package work.slhaf.partner.module.communication
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import work.slhaf.partner.api.InteractionEvent.EventStatus
|
||||
import work.slhaf.partner.api.ReplyEvent
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentRuntime
|
||||
import work.slhaf.partner.framework.agent.interaction.data.InteractionEvent.EventStatus
|
||||
import work.slhaf.partner.framework.agent.interaction.data.Reply
|
||||
import work.slhaf.partner.framework.agent.model.StreamChatMessageConsumer
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object ReplyDispatcher {
|
||||
|
||||
private const val AGGREGATE_WINDOW_MILLIS = 100L
|
||||
private const val NO_REPLY_MARKER = "NO_REPLY"
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val collectorChannel = Channel<ReplyChunk>(Channel.UNLIMITED)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
var pendingChunk: ReplyChunk? = null
|
||||
while (true) {
|
||||
val firstChunk = pendingChunk ?: collectorChannel.receiveCatching().getOrNull() ?: break
|
||||
pendingChunk = null
|
||||
val builder = StringBuilder(firstChunk.delta)
|
||||
|
||||
while (true) {
|
||||
val nextChunk = withTimeoutOrNull(AGGREGATE_WINDOW_MILLIS.milliseconds) {
|
||||
collectorChannel.receiveCatching()
|
||||
} ?: break
|
||||
|
||||
if (nextChunk.isClosed) {
|
||||
flush(builder.toString(), firstChunk.target)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val chunk = nextChunk.getOrNull() ?: break
|
||||
if (chunk.target == firstChunk.target) {
|
||||
builder.append(chunk.delta)
|
||||
} else {
|
||||
pendingChunk = chunk
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
flush(builder.toString(), firstChunk.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* flush 将推送至 AgentRuntime 的默认通道。
|
||||
*/
|
||||
private fun flush(content: String, target: String) {
|
||||
if (content.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val event = Reply(
|
||||
status = EventStatus.RUNNING,
|
||||
target = target,
|
||||
content = content,
|
||||
mode = Reply.ContentMode.APPEND,
|
||||
done = false
|
||||
)
|
||||
AgentRuntime.response(event)
|
||||
}
|
||||
|
||||
fun createConsumer(target: String): StreamChatMessageConsumer = ReplyConsumer(
|
||||
collectorChannel = collectorChannel,
|
||||
fun createConsumer(target: String, responseChannel: String?): StreamChatMessageConsumer = ReplyConsumer(
|
||||
target = target,
|
||||
responseChannel = responseChannel
|
||||
)
|
||||
|
||||
private data class ReplyChunk(
|
||||
val delta: String,
|
||||
val target: String,
|
||||
)
|
||||
}
|
||||
|
||||
private class ReplyConsumer(
|
||||
private val collectorChannel: Channel<ReplyChunk>,
|
||||
private val target: String,
|
||||
) : StreamChatMessageConsumer() {
|
||||
private class ReplyConsumer(
|
||||
private val target: String,
|
||||
private val responseChannel: String?
|
||||
) : StreamChatMessageConsumer {
|
||||
|
||||
private enum class VisibilityState {
|
||||
UNDECIDED,
|
||||
NO_REPLY,
|
||||
VISIBLE
|
||||
}
|
||||
private enum class VisibilityState {
|
||||
UNDECIDED,
|
||||
NO_REPLY,
|
||||
VISIBLE
|
||||
}
|
||||
|
||||
private val rawResponse = StringBuilder()
|
||||
private val undecidedBuffer = StringBuilder()
|
||||
private var visibilityState = VisibilityState.UNDECIDED
|
||||
private val rawResponse = StringBuilder()
|
||||
private val undecidedBuffer = StringBuilder()
|
||||
private val visibleBuffer = StringBuilder()
|
||||
private val lock = Any()
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
override fun onDelta(delta: String) {
|
||||
private var flushJob: Job? = null
|
||||
private var visibilityState = VisibilityState.UNDECIDED
|
||||
private var terminalEmitted = false
|
||||
|
||||
override fun onDelta(delta: String) {
|
||||
synchronized(lock) {
|
||||
rawResponse.append(delta)
|
||||
routeDelta(delta)
|
||||
}
|
||||
}
|
||||
|
||||
override fun collectResponse(): String {
|
||||
override fun onComplete() {
|
||||
synchronized(lock) {
|
||||
finalizeUndecidedBuffer()
|
||||
flushVisibleBufferLocked()
|
||||
dispatchTerminalLocked(EventStatus.DONE)
|
||||
shutdownScopeLocked()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(exception: AgentRuntimeException) {
|
||||
synchronized(lock) {
|
||||
ExceptionReporterHandler.report(exception)
|
||||
rawResponse.append(INTERRUPTED_MARKER)
|
||||
routeDelta(INTERRUPTED_MARKER)
|
||||
finalizeUndecidedBuffer()
|
||||
flushVisibleBufferLocked()
|
||||
dispatchTerminalLocked(EventStatus.ERROR)
|
||||
shutdownScopeLocked()
|
||||
}
|
||||
}
|
||||
|
||||
override fun collectResponse(): String {
|
||||
synchronized(lock) {
|
||||
finalizeUndecidedBuffer()
|
||||
flushVisibleBufferLocked()
|
||||
return rawResponse.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun routeDelta(delta: String) {
|
||||
when (visibilityState) {
|
||||
VisibilityState.NO_REPLY -> return
|
||||
VisibilityState.VISIBLE -> flushVisible(delta)
|
||||
VisibilityState.UNDECIDED -> {
|
||||
undecidedBuffer.append(delta)
|
||||
resolveVisibility()
|
||||
}
|
||||
private fun routeDelta(delta: String) {
|
||||
when (visibilityState) {
|
||||
VisibilityState.NO_REPLY -> return
|
||||
VisibilityState.VISIBLE -> appendVisibleLocked(delta)
|
||||
VisibilityState.UNDECIDED -> {
|
||||
undecidedBuffer.append(delta)
|
||||
resolveVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveVisibility() {
|
||||
val content = undecidedBuffer.toString()
|
||||
if (content.length <= NO_REPLY_MARKER.length) {
|
||||
if (NO_REPLY_MARKER.startsWith(content)) {
|
||||
return
|
||||
}
|
||||
revealBufferedContent()
|
||||
return
|
||||
}
|
||||
|
||||
if (!content.startsWith(NO_REPLY_MARKER)) {
|
||||
revealBufferedContent()
|
||||
return
|
||||
}
|
||||
|
||||
val suffixFirstChar = content[NO_REPLY_MARKER.length]
|
||||
if (suffixFirstChar == '\n' || suffixFirstChar == '\r') {
|
||||
visibilityState = VisibilityState.NO_REPLY
|
||||
undecidedBuffer.setLength(0)
|
||||
private fun resolveVisibility() {
|
||||
val content = undecidedBuffer.toString()
|
||||
if (content.length <= NO_REPLY_MARKER.length) {
|
||||
if (NO_REPLY_MARKER.startsWith(content)) {
|
||||
return
|
||||
}
|
||||
revealBufferedContent()
|
||||
return
|
||||
}
|
||||
|
||||
private fun finalizeUndecidedBuffer() {
|
||||
if (visibilityState != VisibilityState.UNDECIDED) {
|
||||
return
|
||||
}
|
||||
if (undecidedBuffer.toString() == NO_REPLY_MARKER) {
|
||||
visibilityState = VisibilityState.NO_REPLY
|
||||
undecidedBuffer.setLength(0)
|
||||
return
|
||||
}
|
||||
if (!content.startsWith(NO_REPLY_MARKER)) {
|
||||
revealBufferedContent()
|
||||
return
|
||||
}
|
||||
|
||||
private fun revealBufferedContent() {
|
||||
visibilityState = VisibilityState.VISIBLE
|
||||
if (undecidedBuffer.isNotEmpty()) {
|
||||
flushVisible(undecidedBuffer.toString())
|
||||
undecidedBuffer.setLength(0)
|
||||
val suffixFirstChar = content[NO_REPLY_MARKER.length]
|
||||
if (suffixFirstChar == '\n' || suffixFirstChar == '\r') {
|
||||
visibilityState = VisibilityState.NO_REPLY
|
||||
undecidedBuffer.setLength(0)
|
||||
return
|
||||
}
|
||||
revealBufferedContent()
|
||||
}
|
||||
|
||||
private fun finalizeUndecidedBuffer() {
|
||||
if (visibilityState != VisibilityState.UNDECIDED) {
|
||||
return
|
||||
}
|
||||
if (undecidedBuffer.toString() == NO_REPLY_MARKER) {
|
||||
visibilityState = VisibilityState.NO_REPLY
|
||||
undecidedBuffer.setLength(0)
|
||||
return
|
||||
}
|
||||
revealBufferedContent()
|
||||
}
|
||||
|
||||
private fun revealBufferedContent() {
|
||||
visibilityState = VisibilityState.VISIBLE
|
||||
if (undecidedBuffer.isNotEmpty()) {
|
||||
appendVisibleLocked(undecidedBuffer.toString())
|
||||
undecidedBuffer.setLength(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendVisibleLocked(delta: String) {
|
||||
if (delta.isEmpty()) {
|
||||
return
|
||||
}
|
||||
visibleBuffer.append(delta)
|
||||
scheduleFlushLocked()
|
||||
}
|
||||
|
||||
private fun scheduleFlushLocked() {
|
||||
flushJob?.cancel()
|
||||
flushJob = scope.launch {
|
||||
delay(AGGREGATE_WINDOW_MILLIS.milliseconds)
|
||||
synchronized(lock) {
|
||||
flushVisibleBufferLocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushVisible(delta: String) {
|
||||
collectorChannel.trySend(ReplyChunk(delta, target)).isSuccess
|
||||
private fun flushVisibleBufferLocked() {
|
||||
flushJob?.cancel()
|
||||
flushJob = null
|
||||
if (visibleBuffer.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val content = visibleBuffer.toString()
|
||||
visibleBuffer.setLength(0)
|
||||
dispatchLocked(
|
||||
ReplyEvent(
|
||||
status = EventStatus.RUNNING,
|
||||
target = target,
|
||||
content = content,
|
||||
mode = ReplyEvent.ContentMode.APPEND
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun consumeDelta(delta: String?) {
|
||||
private fun dispatchTerminalLocked(status: EventStatus) {
|
||||
if (terminalEmitted) {
|
||||
return
|
||||
}
|
||||
terminalEmitted = true
|
||||
dispatchLocked(
|
||||
ReplyEvent(
|
||||
status = status,
|
||||
target = target,
|
||||
content = "",
|
||||
mode = ReplyEvent.ContentMode.APPEND
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun dispatchLocked(event: ReplyEvent) {
|
||||
if (responseChannel == null) {
|
||||
AgentRuntime.response(event)
|
||||
} else {
|
||||
AgentRuntime.response(event, responseChannel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shutdownScopeLocked() {
|
||||
flushJob?.cancel()
|
||||
flushJob = null
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val AGGREGATE_WINDOW_MILLIS = 100L
|
||||
private const val INTERRUPTED_MARKER = "\n[response interrupted due to internal exception]"
|
||||
private const val NO_REPLY_MARKER = "NO_REPLY"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ public class MessageSummarizer extends AbstractAgentModule.Sub<List<Message>, Re
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
document.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
|
||||
root.appendChild(document.importNode(cognitionCapability.messageNotesElement(), true));
|
||||
appendListElement(document, root, "chat_messages", "message", messages, (element, message) -> {
|
||||
element.setAttribute("role", message.roleValue());
|
||||
element.setTextContent(message.getContent());
|
||||
|
||||
@@ -96,6 +96,9 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
}
|
||||
|
||||
public String fixTopicPath(String topicPath) {
|
||||
if (topicPath == null || topicPath.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String[] parts = topicPath.split("->");
|
||||
List<String> cleanedParts = new ArrayList<>();
|
||||
for (String part : parts) {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package work.slhaf.partner.module.memory.selector;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class MemoryInputEntry {
|
||||
private LocalDateTime receivedDateTime;
|
||||
private List<RunningFlowContext.InputEntry> inputs;
|
||||
}
|
||||
+116
-5
@@ -5,6 +5,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
@@ -23,7 +25,11 @@ import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorResul
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFlowContext> {
|
||||
@@ -33,6 +39,7 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
private final AtomicBoolean memoryCalling = new AtomicBoolean(false);
|
||||
|
||||
@InjectModule
|
||||
private MemoryRuntime memoryRuntime;
|
||||
@@ -40,24 +47,125 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
|
||||
private MemoryRecallEvaluator memoryRecallEvaluator;
|
||||
@InjectModule
|
||||
private MemoryRecallCueExtractor memoryRecallCueExtractor;
|
||||
private final Lock inputsLock = new ReentrantLock();
|
||||
private final List<MemoryInputEntry> collectedInputs = new ArrayList<>();
|
||||
@InjectCapability
|
||||
private ActionCapability actionCapability;
|
||||
|
||||
@Override
|
||||
protected void doExecute(@NotNull PartnerRunningFlowContext runningFlowContext) {
|
||||
List<RunningFlowContext.InputEntry> snapshotInputs = List.copyOf(runningFlowContext.getInputs());
|
||||
collectInputs(runningFlowContext);
|
||||
tryStartMemoryRecallWorker();
|
||||
}
|
||||
|
||||
private void collectInputs(PartnerRunningFlowContext runningFlowContext) {
|
||||
inputsLock.lock();
|
||||
try {
|
||||
collectedInputs.add(new MemoryInputEntry(
|
||||
runningFlowContext.getFirstInputDateTime(),
|
||||
List.copyOf(runningFlowContext.getInputs())
|
||||
));
|
||||
} finally {
|
||||
inputsLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void tryStartMemoryRecallWorker() {
|
||||
if (!memoryCalling.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionCapability.getExecutor(ActionCore.ExecutorType.VIRTUAL).execute(() -> {
|
||||
try {
|
||||
drainMemoryRecall();
|
||||
} finally {
|
||||
memoryCalling.set(false);
|
||||
if (hasCollectedInputs()) {
|
||||
tryStartMemoryRecallWorker();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void drainMemoryRecall() {
|
||||
while (true) {
|
||||
List<MemoryInputEntry> snapshotInputs = drainCollectedInputs();
|
||||
if (snapshotInputs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
recallMemory(snapshotInputs);
|
||||
} catch (Exception e) {
|
||||
log.error("[MemorySelector] 记忆召回任务执行失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void recallMemory(List<MemoryInputEntry> memoryInputEntries) {
|
||||
ExtractorInput input = new ExtractorInput(
|
||||
snapshotInputs,
|
||||
memoryRuntime.getTopicTree(),
|
||||
runningFlowContext.getFirstInputDateTime().toLocalDate()
|
||||
memoryInputEntries,
|
||||
memoryRuntime.getTopicTree()
|
||||
);
|
||||
|
||||
ExtractorResult extractorResult = memoryRecallCueExtractor.execute(input);
|
||||
if (extractorResult.getMatches().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<ActivatedMemorySlice> activatedSlices = selectAndEvaluateMemory(snapshotInputs, extractorResult);
|
||||
List<ActivatedMemorySlice> activatedSlices = selectAndEvaluateMemory(flattenInputs(memoryInputEntries), extractorResult);
|
||||
updateMemoryContext(activatedSlices);
|
||||
}
|
||||
|
||||
private List<MemoryInputEntry> drainCollectedInputs() {
|
||||
inputsLock.lock();
|
||||
try {
|
||||
if (collectedInputs.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<MemoryInputEntry> snapshot = new ArrayList<>(collectedInputs);
|
||||
collectedInputs.clear();
|
||||
snapshot.sort(Comparator.comparing(MemoryInputEntry::getReceivedDateTime));
|
||||
return snapshot;
|
||||
} finally {
|
||||
inputsLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasCollectedInputs() {
|
||||
inputsLock.lock();
|
||||
try {
|
||||
return !collectedInputs.isEmpty();
|
||||
} finally {
|
||||
inputsLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private List<RunningFlowContext.InputEntry> flattenInputs(List<MemoryInputEntry> memoryInputEntries) {
|
||||
if (memoryInputEntries.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
long firstEpochMillis = memoryInputEntries.stream()
|
||||
.map(MemoryInputEntry::getReceivedDateTime)
|
||||
.mapToLong(this::toEpochMillis)
|
||||
.min()
|
||||
.orElseThrow();
|
||||
|
||||
return memoryInputEntries.stream()
|
||||
.flatMap(entry -> {
|
||||
long entryEpochMillis = toEpochMillis(entry.getReceivedDateTime());
|
||||
return entry.getInputs().stream()
|
||||
.map(input -> new RunningFlowContext.InputEntry(
|
||||
entryEpochMillis + input.getOffsetMillis() - firstEpochMillis,
|
||||
input.getContent()
|
||||
));
|
||||
})
|
||||
.sorted(Comparator.comparingLong(RunningFlowContext.InputEntry::getOffsetMillis))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private long toEpochMillis(java.time.LocalDateTime dateTime) {
|
||||
return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
private void updateMemoryContext(List<ActivatedMemorySlice> activatedSlices) {
|
||||
cognitionCapability.contextWorkspace().register(new ContextBlock(
|
||||
buildMemoryFullBlock(activatedSlices),
|
||||
@@ -155,6 +263,9 @@ public class MemorySelector extends AbstractAgentModule.Running<PartnerRunningFl
|
||||
|
||||
private void setMemoryCandidates(LinkedHashMap<String, ActivatedMemorySlice> candidates, List<ExtractorResult.ExtractorMatchData> matches) {
|
||||
for (ExtractorResult.ExtractorMatchData match : matches) {
|
||||
if (match == null || match.getType() == null || match.getText() == null || match.getText().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
List<ActivatedMemorySlice> recalledSlices = switch (match.getType()) {
|
||||
case ExtractorResult.ExtractorMatchData.Constant.TOPIC ->
|
||||
|
||||
+51
-22
@@ -18,6 +18,8 @@ import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorInput
|
||||
import work.slhaf.partner.module.memory.selector.extractor.entity.ExtractorResult;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorInput, ExtractorResult> implements ActivateModel {
|
||||
@@ -28,12 +30,11 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
|
||||
你会收到:
|
||||
- 一条结构化上下文消息,其中包含当前活跃的 communication 域与 memory 域内容;
|
||||
- 一条任务消息,其中包含:
|
||||
- new_inputs:一组按时间顺序累积的新输入,每条输入附带 interval-to-first;
|
||||
- current_date:当前日期;
|
||||
- memory_input_entries:一组按接收时间排序的新输入批次;每个批次包含 received_date_time 与该批次内按时间顺序排列的 inputs;
|
||||
- memory_topic_tree:当前可用的记忆主题树结构。
|
||||
|
||||
你的任务:
|
||||
- 基于 new_inputs、当前语境与已有记忆主题树,提取本次记忆召回最值得尝试的匹配项;
|
||||
- 基于 memory_input_entries、当前语境与已有记忆主题树,提取本次记忆召回最值得尝试的匹配项;
|
||||
- 匹配项只允许有两类:topic 或 date;
|
||||
- topic 用于表示应优先检索的记忆主题路径;
|
||||
- date 用于表示应优先检索的具体日期;
|
||||
@@ -41,7 +42,7 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
|
||||
|
||||
提取原则:
|
||||
- 你的目标是提取“可用于后续召回”的线索,而不是复述输入内容本身。
|
||||
- new_inputs 应整体理解,不要只抓最后一句;如果多条输入共同收敛到同一记忆方向,应提取更稳定的主题线索。
|
||||
- memory_input_entries 应整体理解,不要只抓最后一个批次或最后一句;如果多个批次共同收敛到同一记忆方向,应提取更稳定的主题线索。
|
||||
- communication 域用于判断当前输入是否在承接近期某段对话、某个旧话题或某个已出现过的指代对象。
|
||||
- memory 域用于辅助判断当前输入与哪些已激活记忆方向明显相关;只有在这种相关性明确时才使用,不要机械复述 memory 域内容。
|
||||
- memory_topic_tree 是 topic 提取的主要参照;topic 应尽量贴近主题树中已有的层级与命名,不要随意发明与主题树无关的新路径。
|
||||
@@ -59,8 +60,8 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
|
||||
关于 date:
|
||||
- date 表示一个明确的记忆日期。
|
||||
- date 的 text 必须是可被 Java LocalDate.parse 正常解析的日期文本,即 yyyy-MM-dd。
|
||||
- 只有在输入中存在明确日期,或结合 current_date 后可以稳定推断出具体某一天时,才输出 date。
|
||||
- 像“今天”“昨天”“前天”“上周六”这类表达,只有在能够稳定落到某个具体日期时才可输出。
|
||||
- 只有在输入中存在明确日期,或结合相关输入批次的 received_date_time 后可以稳定推断出具体某一天时,才输出 date。
|
||||
- 像“今天”“昨天”“前天”“上周六”这类表达,只有在能够根据对应批次 received_date_time 稳定落到某个具体日期时才可输出。
|
||||
- 对于“最近”“前几天”“那段时间”“之前”“上次”这类无法稳定定位到某一天的表达,不要输出 date。
|
||||
|
||||
何时应提取 topic:
|
||||
@@ -71,7 +72,7 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
|
||||
|
||||
何时应提取 date:
|
||||
- 当前输入明确提到了某个具体日期;
|
||||
- 当前输入使用相对日期表达,但结合 current_date 可以稳定还原到具体某一天;
|
||||
- 当前输入使用相对日期表达,但结合对应批次的 received_date_time 可以稳定还原到具体某一天;
|
||||
- 当前输入中的回忆目标明显依赖某个特定日期,且该日期能够被明确确定。
|
||||
|
||||
何时不应轻易输出:
|
||||
@@ -134,15 +135,20 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
|
||||
return new TaskBlock() {
|
||||
@Override
|
||||
protected void fillXml(@NotNull Document document, @NotNull Element root) {
|
||||
appendChildElement(document, root, "new_inputs", (inputsElement) -> {
|
||||
appendListElement(document, inputsElement, "inputs", "input", input.getInputs(), (inputElement, entry) -> {
|
||||
inputElement.setAttribute("interval-to-first", String.valueOf(entry.getOffsetMillis()));
|
||||
inputElement.setTextContent(entry.getContent());
|
||||
appendChildElement(document, root, "memory_input_entries", (entriesElement) -> {
|
||||
appendRepeatedElements(document, entriesElement, "memory_input_entry", input.getMemoryInputEntries().stream()
|
||||
.sorted(Comparator.comparing(work.slhaf.partner.module.memory.selector.MemoryInputEntry::getReceivedDateTime))
|
||||
.toList(), (entryElement, memoryInputEntry) -> {
|
||||
appendTextElement(document, entryElement, "received_date_time", memoryInputEntry.getReceivedDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
appendListElement(document, entryElement, "inputs", "input", memoryInputEntry.getInputs(), (inputElement, entry) -> {
|
||||
inputElement.setAttribute("interval-to-first", String.valueOf(entry.getOffsetMillis()));
|
||||
inputElement.setTextContent(entry.getContent());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
appendTextElement(document, root, "current_date", input.getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
||||
appendTextElement(document, root, "memory_topic_tree", input.getTopic_tree());
|
||||
}
|
||||
}.encodeToMessage();
|
||||
@@ -155,17 +161,40 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
|
||||
}
|
||||
|
||||
private ExtractorResult fix(ExtractorResult extractorResult) {
|
||||
extractorResult.getMatches().forEach(m -> {
|
||||
if (m.getType().equals(ExtractorResult.ExtractorMatchData.Constant.DATE)) {
|
||||
return;
|
||||
}
|
||||
m.setText(memoryRuntime.fixTopicPath(m.getText()));
|
||||
});
|
||||
if (extractorResult.getMatches().isEmpty()) {
|
||||
return extractorResult;
|
||||
ExtractorResult safeResult = extractorResult == null ? new ExtractorResult() : extractorResult;
|
||||
List<ExtractorResult.ExtractorMatchData> rawMatches = safeResult.getMatches();
|
||||
if (rawMatches == null || rawMatches.isEmpty()) {
|
||||
safeResult.setMatches(List.of());
|
||||
return safeResult;
|
||||
}
|
||||
extractorResult.getMatches().removeIf(m -> m.getText().split("->")[0].isEmpty());
|
||||
return extractorResult;
|
||||
|
||||
List<ExtractorResult.ExtractorMatchData> normalizedMatches = new ArrayList<>();
|
||||
for (ExtractorResult.ExtractorMatchData match : rawMatches) {
|
||||
if (match == null) {
|
||||
continue;
|
||||
}
|
||||
String type = match.getType();
|
||||
String text = match.getText();
|
||||
if (text == null || text.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ExtractorResult.ExtractorMatchData.Constant.TOPIC.equals(type)) {
|
||||
text = memoryRuntime.fixTopicPath(text);
|
||||
if (text.isBlank() || text.split("->")[0].isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
match.setText(text);
|
||||
normalizedMatches.add(match);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ExtractorResult.ExtractorMatchData.Constant.DATE.equals(type)) {
|
||||
normalizedMatches.add(match);
|
||||
}
|
||||
}
|
||||
safeResult.setMatches(normalizedMatches);
|
||||
return safeResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-4
@@ -2,15 +2,13 @@ package work.slhaf.partner.module.memory.selector.extractor.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext;
|
||||
import work.slhaf.partner.module.memory.selector.MemoryInputEntry;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class ExtractorInput {
|
||||
private List<RunningFlowContext.InputEntry> inputs;
|
||||
private List<MemoryInputEntry> memoryInputEntries;
|
||||
private String topic_tree;
|
||||
private LocalDate date;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package work.slhaf.partner.runtime;
|
||||
|
||||
import work.slhaf.partner.common.vector.VectorClientRegistry;
|
||||
import work.slhaf.partner.framework.agent.Agent;
|
||||
import work.slhaf.partner.runtime.gateway.WebSocketGatewayRegistration;
|
||||
|
||||
public final class PartnerAgentBootstrap extends Agent.AgentBootstrap {
|
||||
|
||||
public PartnerAgentBootstrap(Agent.AgentApp agentApp) {
|
||||
super(agentApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bootstrap() {
|
||||
addGatewayRegistration(new WebSocketGatewayRegistration());
|
||||
addConfigurable(new VectorClientRegistry());
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,9 @@ import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext
|
||||
class PartnerRunningFlowContext private constructor(
|
||||
override val source: String,
|
||||
inputs: List<InputEntry>,
|
||||
firstInputEpochMillis: Long
|
||||
) : RunningFlowContext(inputs, firstInputEpochMillis) {
|
||||
firstInputEpochMillis: Long,
|
||||
responseChannel: String? = null
|
||||
) : RunningFlowContext(inputs, firstInputEpochMillis, responseChannel) {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -32,12 +33,18 @@ class PartnerRunningFlowContext private constructor(
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun fromUser(userId: String, input: String, receivedAtMillis: Long = System.currentTimeMillis()) =
|
||||
fun fromUser(
|
||||
userId: String,
|
||||
input: String,
|
||||
receivedAtMillis: Long = System.currentTimeMillis(),
|
||||
responseChannel: String? = null
|
||||
) =
|
||||
PartnerRunningFlowContext(
|
||||
SourceTag.buildUserSource(userId),
|
||||
listOf(InputEntry(0L, input)),
|
||||
receivedAtMillis
|
||||
)
|
||||
receivedAtMillis,
|
||||
responseChannel
|
||||
).apply { this.target = userId }
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@@ -45,7 +52,8 @@ class PartnerRunningFlowContext private constructor(
|
||||
PartnerRunningFlowContext(
|
||||
SourceTag.buildAgentSource(),
|
||||
listOf(InputEntry(0L, input)),
|
||||
receivedAtMillis
|
||||
receivedAtMillis,
|
||||
null
|
||||
).apply {
|
||||
putUserInfo(InfoKeys.PLATFORM, SOURCE_SELF_PLATFORM)
|
||||
putUserInfo(InfoKeys.NICKNAME, SOURCE_SELF_NICKNAME)
|
||||
@@ -56,7 +64,8 @@ class PartnerRunningFlowContext private constructor(
|
||||
return PartnerRunningFlowContext(
|
||||
source = source,
|
||||
inputs = inputs,
|
||||
firstInputEpochMillis = System.currentTimeMillis()
|
||||
firstInputEpochMillis = System.currentTimeMillis(),
|
||||
resopnseChannel
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+16
-17
@@ -1,5 +1,6 @@
|
||||
package work.slhaf.partner.runtime.gateway;
|
||||
|
||||
import com.alibaba.fastjson2.JSONException;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -8,11 +9,9 @@ import org.java_websocket.framing.Framedata;
|
||||
import org.java_websocket.handshake.ClientHandshake;
|
||||
import org.java_websocket.server.WebSocketServer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.api.InputData;
|
||||
import work.slhaf.partner.api.InteractionEvent;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGateway;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGatewayRegistration;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentRuntime;
|
||||
import work.slhaf.partner.framework.agent.interaction.data.InputData;
|
||||
import work.slhaf.partner.framework.agent.interaction.data.InteractionEvent;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
@@ -34,11 +33,13 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
|
||||
// 记录最后一次收到Pong的时间
|
||||
private final ConcurrentHashMap<WebSocket, Long> lastPongTimes = new ConcurrentHashMap<>();
|
||||
|
||||
public WebSocketGateway(int port, long heartbeatInterval) {
|
||||
super(new InetSocketAddress(port));
|
||||
public WebSocketGateway(int port, @NotNull String hostname, long heartbeatInterval) {
|
||||
super(new InetSocketAddress(hostname, port));
|
||||
this.heartbeatInterval = heartbeatInterval;
|
||||
this.setReuseAddr(true);
|
||||
this.executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
log.info("WebSocketGateway will start on {}: {}", hostname, port);
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
@@ -47,12 +48,11 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
|
||||
}
|
||||
this.start();
|
||||
startHeartbeatThread();
|
||||
AgentRuntime.INSTANCE.registerResponseChannel(getChannelName(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartnerRunningFlowContext parseRunningFlowContext(InputData inputData) {
|
||||
PartnerRunningFlowContext context = PartnerRunningFlowContext.fromUser(inputData.getSource(), inputData.getContent());
|
||||
PartnerRunningFlowContext context = PartnerRunningFlowContext.fromUser(inputData.getSource(), inputData.getContent(),System.currentTimeMillis(), getChannelName());
|
||||
inputData.getMeta().forEach(context::putUserInfo);
|
||||
return context;
|
||||
}
|
||||
@@ -119,14 +119,18 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String s) {
|
||||
InputData inputData = JSONObject.parseObject(s, InputData.class);
|
||||
userSessions.put(inputData.getSource(), webSocket); // 注册连接
|
||||
receive(inputData);
|
||||
try {
|
||||
InputData inputData = JSONObject.parseObject(s, InputData.class);
|
||||
userSessions.put(inputData.getSource(), webSocket); // 注册连接
|
||||
receive(inputData);
|
||||
} catch (JSONException ignored) {
|
||||
log.warn("Invalid input format: {}", s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(WebSocket webSocket, Exception e) {
|
||||
log.error(e.getLocalizedMessage());
|
||||
log.error("WebSocketGateway error", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,11 +138,6 @@ public class WebSocketGateway extends WebSocketServer implements AgentGateway<In
|
||||
log.info("WebSocketServer 已启动...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentGatewayRegistration registration() {
|
||||
return WebSocketGatewayRegistration.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getChannelName() {
|
||||
|
||||
+3
-2
@@ -3,16 +3,17 @@ package work.slhaf.partner.runtime.gateway
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGateway
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGatewayRegistration
|
||||
|
||||
object WebSocketGatewayRegistration : AgentGatewayRegistration {
|
||||
class WebSocketGatewayRegistration : AgentGatewayRegistration {
|
||||
|
||||
override val channelName: String = "websocket_channel"
|
||||
|
||||
override fun create(params: Map<String, String>): AgentGateway<*, *> {
|
||||
val port = params["port"]?.toIntOrNull() ?: 29600
|
||||
val heartbeatInterval = params["heartbeat_interval"]?.toLongOrNull() ?: 10_000L
|
||||
val hostname = params["hostname"] ?: "127.0.0.1"
|
||||
require(port > 0) { "port must be greater than 0" }
|
||||
require(heartbeatInterval > 0) { "heartbeat_interval must be greater than 0" }
|
||||
return WebSocketGateway(port, heartbeatInterval)
|
||||
return WebSocketGateway(port, hostname, heartbeatInterval)
|
||||
}
|
||||
|
||||
override fun shutdown(instance: AgentGateway<*, *>) {
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
<configuration>
|
||||
<!-- 文件输出(保持你的原配置) -->
|
||||
<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>./data/log/partner.log</file>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>./data/log/partner.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
<property name="PARTNER_HOME" value="${PARTNER_HOME:-${user.home}/.partner}"/>
|
||||
<property name="PARTNER_LOG_DIR" value="${PARTNER_HOME}/state/trace/log"/>
|
||||
<property name="LOG_LEVEL" value="${PARTNER_LOG_LEVEL:-${partner.log.level:-INFO}}"/>
|
||||
|
||||
<!-- 新增控制台输出 -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 同时输出到文件和控制台 -->
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="ROLLING_FILE"/>
|
||||
<appender-ref ref="CONSOLE"/> <!-- 关键:添加这一行 -->
|
||||
<appender name="PARTNER_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${PARTNER_LOG_DIR}/partner-core.log</file>
|
||||
<append>true</append>
|
||||
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${PARTNER_LOG_DIR}/partner-core.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||
<maxFileSize>20MB</maxFileSize>
|
||||
<maxHistory>14</maxHistory>
|
||||
<totalSizeCap>1GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="${LOG_LEVEL}">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="PARTNER_FILE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package experimental;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.DOMException;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class ExternalNodeImportTest {
|
||||
|
||||
private static Node importExternalNode(Document targetDocument, Node externalNode) {
|
||||
Node nodeToImport = externalNode;
|
||||
if (externalNode != null && externalNode.getNodeType() == Node.DOCUMENT_NODE) {
|
||||
nodeToImport = ((Document) externalNode).getDocumentElement();
|
||||
}
|
||||
if (nodeToImport == null) {
|
||||
throw new IllegalArgumentException("nodeToImport must not be null");
|
||||
}
|
||||
if (nodeToImport.getOwnerDocument() == targetDocument) {
|
||||
return nodeToImport.cloneNode(true);
|
||||
}
|
||||
return targetDocument.importNode(nodeToImport, true);
|
||||
}
|
||||
|
||||
private static Document newDocument() throws Exception {
|
||||
return DocumentBuilderFactory.newInstance()
|
||||
.newDocumentBuilder()
|
||||
.newDocument();
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendForeignElementDirectlyShouldThrow() throws Exception {
|
||||
Document targetDocument = newDocument();
|
||||
Element inputRoot = targetDocument.createElement("input");
|
||||
targetDocument.appendChild(inputRoot);
|
||||
|
||||
Document externalDocument = newDocument();
|
||||
Element externalInputs = externalDocument.createElement("inputs");
|
||||
externalDocument.appendChild(externalInputs);
|
||||
|
||||
assertThrows(DOMException.class, () -> inputRoot.appendChild(externalInputs));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importForeignElementShouldWork() throws Exception {
|
||||
Document targetDocument = newDocument();
|
||||
Element inputRoot = targetDocument.createElement("input");
|
||||
targetDocument.appendChild(inputRoot);
|
||||
|
||||
Document externalDocument = newDocument();
|
||||
Element externalInputs = externalDocument.createElement("inputs");
|
||||
externalDocument.appendChild(externalInputs);
|
||||
|
||||
Node imported = importExternalNode(targetDocument, externalInputs);
|
||||
inputRoot.appendChild(imported);
|
||||
|
||||
assertEquals("inputs", inputRoot.getFirstChild().getNodeName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importExternalDocumentShouldUseDocumentElement() throws Exception {
|
||||
Document targetDocument = newDocument();
|
||||
Element inputRoot = targetDocument.createElement("input");
|
||||
targetDocument.appendChild(inputRoot);
|
||||
|
||||
Document externalDocument = newDocument();
|
||||
Element externalInputs = externalDocument.createElement("inputs");
|
||||
externalDocument.appendChild(externalInputs);
|
||||
|
||||
Node imported = importExternalNode(targetDocument, externalDocument);
|
||||
inputRoot.appendChild(imported);
|
||||
|
||||
assertEquals("inputs", inputRoot.getFirstChild().getNodeName());
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import io.modelcontextprotocol.server.McpStatelessServerFeatures;
|
||||
import io.modelcontextprotocol.server.McpStatelessSyncServer;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
||||
import work.slhaf.partner.core.action.runner.mcp.InProcessMcpTransport;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
+32
-8
@@ -52,29 +52,51 @@ class BuiltinActionRegistryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInitRegistersMetaActionsAndMountsRunner() throws Exception {
|
||||
void testInitMountsRunner() throws Exception {
|
||||
ActionCapability actionCapability = mock(ActionCapability.class);
|
||||
RunnerClient runnerClient = mock(RunnerClient.class);
|
||||
when(actionCapability.runnerClient()).thenReturn(runnerClient);
|
||||
|
||||
BuiltinActionRegistry registry = spy(new BuiltinActionRegistry());
|
||||
doReturn(List.of(
|
||||
buildDefinition("echo", buildMetaActionInfo("echo"), params -> params.get("value").toString())
|
||||
)).when(registry).buildDefaultActionDefinitions();
|
||||
BuiltinActionRegistry registry = new BuiltinActionRegistry();
|
||||
injectActionCapability(registry, actionCapability);
|
||||
|
||||
registry.init();
|
||||
|
||||
verify(runnerClient).setBuiltinActionRegistry(registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRegisterProviderRegistersDefinitionsAndMetaActions() throws Exception {
|
||||
ActionCapability actionCapability = mock(ActionCapability.class);
|
||||
BuiltinActionRegistry registry = new BuiltinActionRegistry();
|
||||
injectActionCapability(registry, actionCapability);
|
||||
|
||||
BuiltinActionProvider provider = new BuiltinActionProvider() {
|
||||
@Override
|
||||
public List<BuiltinActionRegistry.BuiltinActionDefinition> provideBuiltinActions() {
|
||||
return List.of(buildDefinition("echo", buildMetaActionInfo("echo"), params -> params.get("value").toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createActionKey(String actionName) {
|
||||
return BUILTIN_LOCATION + "::" + actionName;
|
||||
}
|
||||
};
|
||||
|
||||
registry.register(provider);
|
||||
|
||||
verify(actionCapability).registerMetaActions(argThat(metaActions ->
|
||||
metaActions.containsKey("builtin::echo")
|
||||
&& "echo".equals(metaActions.get("builtin::echo").getDescription())
|
||||
));
|
||||
verify(runnerClient).setBuiltinActionRegistry(registry);
|
||||
Assertions.assertEquals("hello", registry.call("builtin::echo", Map.of("value", "hello")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallReturnsStringifiedResults() {
|
||||
void testCallReturnsStringifiedResults() throws Exception {
|
||||
ActionCapability actionCapability = mock(ActionCapability.class);
|
||||
BuiltinActionRegistry registry = new BuiltinActionRegistry();
|
||||
injectActionCapability(registry, actionCapability);
|
||||
registry.defineBuiltinAction("echo", buildMetaActionInfo("echo"), params -> params.get("value").toString());
|
||||
registry.defineBuiltinAction("json", buildMetaActionInfo("json"), params -> Map.of("ok", true).toString());
|
||||
registry.defineBuiltinAction("nil", buildMetaActionInfo("nil"), params -> null);
|
||||
@@ -91,8 +113,10 @@ class BuiltinActionRegistryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallPropagatesInvokerFailure() {
|
||||
void testCallPropagatesInvokerFailure() throws Exception {
|
||||
ActionCapability actionCapability = mock(ActionCapability.class);
|
||||
BuiltinActionRegistry registry = new BuiltinActionRegistry();
|
||||
injectActionCapability(registry, actionCapability);
|
||||
registry.defineBuiltinAction("boom", buildMetaActionInfo("boom"), params -> {
|
||||
throw new IllegalStateException("boom");
|
||||
});
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package work.slhaf.partner.module.action.planner;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ActionPlannerAssemblyHelperTest {
|
||||
|
||||
private static void injectField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private static Object getField(Object target, String fieldName) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return field.get(target);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFixNonZeroBasedOrdersWithoutInjectingNullActionKeyList() throws Exception {
|
||||
ActionCapability actionCapability = Mockito.mock(ActionCapability.class);
|
||||
Mockito.when(actionCapability.loadMetaActionInfo(Mockito.anyString()))
|
||||
.thenReturn(Result.success(new MetaActionInfo(
|
||||
false,
|
||||
null,
|
||||
Map.of(),
|
||||
"desc",
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
Set.of(),
|
||||
false,
|
||||
new JSONObject()
|
||||
)));
|
||||
|
||||
ActionPlanner planner = new ActionPlanner();
|
||||
injectField(planner, "actionCapability", actionCapability);
|
||||
|
||||
Object helper = getField(planner, "assemblyHelper");
|
||||
Method fixDependencies = helper.getClass().getDeclaredMethod("fixDependencies", Map.class);
|
||||
fixDependencies.setAccessible(true);
|
||||
|
||||
Map<Integer, List<String>> chain = new LinkedHashMap<>();
|
||||
chain.put(1, new ArrayList<>(List.of("action_a")));
|
||||
chain.put(2, new ArrayList<>(List.of("action_b")));
|
||||
|
||||
boolean fixed = (boolean) fixDependencies.invoke(helper, chain);
|
||||
|
||||
assertTrue(fixed);
|
||||
assertEquals(List.of(1, 2), new ArrayList<>(chain.keySet()));
|
||||
assertEquals(List.of("action_a"), chain.get(1));
|
||||
assertEquals(List.of("action_b"), chain.get(2));
|
||||
assertFalse(chain.containsValue(null));
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package work.slhaf.partner.module.action.planner.evaluator.entity;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class EvaluatorResultTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyMapWhenPrimaryActionChainIsNull() {
|
||||
EvaluatorResult result = new EvaluatorResult();
|
||||
Map<Integer, List<String>> chain = result.getPrimaryActionChainAsMap();
|
||||
assertNotNull(chain);
|
||||
assertTrue(chain.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeNullActionKeysToEmptyList() {
|
||||
EvaluatorResult result = new EvaluatorResult();
|
||||
EvaluatorResult.ChainElement element = new EvaluatorResult.ChainElement();
|
||||
element.setOrder(1);
|
||||
element.setActionKeys(null);
|
||||
result.setPrimaryActionChain(List.of(element));
|
||||
|
||||
Map<Integer, List<String>> chain = result.getPrimaryActionChainAsMap();
|
||||
assertEquals(1, chain.size());
|
||||
assertNotNull(chain.get(1));
|
||||
assertTrue(chain.get(1).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCopyActionKeyListDefensively() {
|
||||
EvaluatorResult result = new EvaluatorResult();
|
||||
EvaluatorResult.ChainElement element = new EvaluatorResult.ChainElement();
|
||||
element.setOrder(1);
|
||||
List<String> keys = new ArrayList<>(List.of("a"));
|
||||
element.setActionKeys(keys);
|
||||
result.setPrimaryActionChain(List.of(element));
|
||||
|
||||
Map<Integer, List<String>> chain = result.getPrimaryActionChainAsMap();
|
||||
keys.add("b");
|
||||
|
||||
assertEquals(new LinkedHashMap<>(Map.of(1, List.of("a"))), new LinkedHashMap<>(chain));
|
||||
}
|
||||
}
|
||||
+33
-1
@@ -14,7 +14,7 @@ import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class CommunicationProducerTest {
|
||||
|
||||
@@ -32,6 +32,20 @@ class CommunicationProducerTest {
|
||||
method.invoke(producer, context, response);
|
||||
}
|
||||
|
||||
private static String invokeBuildInputXml(
|
||||
CommunicationProducer producer,
|
||||
PartnerRunningFlowContext context,
|
||||
List<?> communicationBlocks
|
||||
) throws Exception {
|
||||
Method method = CommunicationProducer.class.getDeclaredMethod(
|
||||
"buildInputXml",
|
||||
PartnerRunningFlowContext.class,
|
||||
List.class
|
||||
);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(producer, context, communicationBlocks);
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
@@ -72,6 +86,24 @@ class CommunicationProducerTest {
|
||||
assertEquals("[[AGENT]: self]:\n\nnormal reply", chatMessages.get(1).getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildInputXmlWithoutExtraWrapper() throws Exception {
|
||||
StubCognitionCapability cognitionCapability = new StubCognitionCapability();
|
||||
CommunicationProducer producer = new CommunicationProducer();
|
||||
setField(producer, "cognitionCapability", cognitionCapability);
|
||||
|
||||
String xml = invokeBuildInputXml(
|
||||
producer,
|
||||
PartnerRunningFlowContext.fromUser("user-1", "hello"),
|
||||
List.of()
|
||||
);
|
||||
|
||||
assertTrue(xml.contains("<input>"));
|
||||
assertTrue(xml.contains("<inputs>"));
|
||||
assertTrue(xml.contains("<input interval-to-first=\"0\">hello</input>"));
|
||||
assertFalse(xml.contains("<wrapper>"));
|
||||
}
|
||||
|
||||
private static final class StubCognitionCapability implements CognitionCapability {
|
||||
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
|
||||
private final List<Message> chatMessages = new ArrayList<>();
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner-external-modules</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>partner-onebot-adapter</artifactId>
|
||||
<version>1.0.0</version>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner-core</artifactId>
|
||||
<version>${partner.runtime.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner-framework</artifactId>
|
||||
<version>${partner.runtime.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>work.slhaf.partner:partner-framework</exclude>
|
||||
<exclude>work.slhaf.partner:partner-core</exclude>
|
||||
<exclude>work.slhaf.partner:partner-external-modules</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>process-sources</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>${project.basedir}/src/main/java</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>${project.basedir}/src/test/java</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<jvmTarget>${maven.compiler.target}</jvmTarget>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package work.slhaf.partner.external.onebot;
|
||||
|
||||
import work.slhaf.partner.external.onebot.gateway.OnebotGatewayRegistration;
|
||||
import work.slhaf.partner.framework.agent.Agent;
|
||||
|
||||
public class OnebotAdapterBootstrap extends Agent.AgentBootstrap{
|
||||
|
||||
protected OnebotAdapterBootstrap(Agent.AgentApp agentApp) {
|
||||
super(agentApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bootstrap() {
|
||||
addGatewayRegistration(new OnebotGatewayRegistration());
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package work.slhaf.partner.external.onebot.gateway;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.java_websocket.handshake.ClientHandshake;
|
||||
import org.java_websocket.server.WebSocketServer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.api.InputData;
|
||||
import work.slhaf.partner.api.InteractionEvent;
|
||||
import work.slhaf.partner.external.onebot.v11.*;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGateway;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Slf4j
|
||||
public class OnebotGateway extends WebSocketServer implements AgentGateway<InputData, PartnerRunningFlowContext> {
|
||||
|
||||
private final AtomicReference<WebSocket> activeConnection = new AtomicReference<>();
|
||||
private final AtomicBoolean launched = new AtomicBoolean(false);
|
||||
private final String path;
|
||||
private final String accessToken;
|
||||
private final AtomicLong lastHeartbeatAt = new AtomicLong();
|
||||
private final OneBotV11ResponseDispatcher responseDispatcher = new OneBotV11ResponseDispatcher();
|
||||
|
||||
public OnebotGateway(int port, @NotNull String hostname, @NotNull String path, String accessToken) {
|
||||
super(new InetSocketAddress(hostname, port));
|
||||
this.setReuseAddr(true);
|
||||
this.path = path;
|
||||
this.accessToken = accessToken;
|
||||
|
||||
OneBotV11ActionExecutor.bindConnectionReference(this.activeConnection);
|
||||
log.info("Onebot will start with {}: {}, {}", hostname, port, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch() {
|
||||
if (!launched.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
this.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartnerRunningFlowContext parseRunningFlowContext(InputData inputData) {
|
||||
PartnerRunningFlowContext context = PartnerRunningFlowContext.fromUser(inputData.getSource(), inputData.getContent(),System.currentTimeMillis(), getChannelName());
|
||||
inputData.getMeta().forEach(context::putUserInfo);
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
responseDispatcher.close();
|
||||
for (WebSocket webSocket : getConnections()) {
|
||||
if (webSocket != null && webSocket.isOpen()) {
|
||||
webSocket.close(1001, "Server shutting down");
|
||||
}
|
||||
}
|
||||
if (launched.get()) {
|
||||
super.stop(1000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("关闭 OneBotGateway 失败", e);
|
||||
} finally {
|
||||
activeConnection.set(null);
|
||||
launched.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getChannelName() {
|
||||
return "onebot_channel";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void response(@NotNull InteractionEvent event) {
|
||||
WebSocket conn = activeConnection.get();
|
||||
if (conn == null || !conn.isOpen()) {
|
||||
log.warn("No active OneBot connection for response target: {}", event.getTarget());
|
||||
return;
|
||||
}
|
||||
responseDispatcher.accept(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||
String actualPath = handshake.getResourceDescriptor();
|
||||
|
||||
if (!actualPath.equals(path)) {
|
||||
conn.close(1008, "Unsupported OneBot path: " + actualPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifyAccessToken(handshake)) {
|
||||
conn.close(1008, "Invalid access token");
|
||||
return;
|
||||
}
|
||||
|
||||
WebSocket old = activeConnection.getAndSet(conn);
|
||||
if (old != null && old != conn && old.isOpen()) {
|
||||
old.close(1001, "Replaced by new OneBot connection");
|
||||
}
|
||||
|
||||
log.info("OneBot connected: {}", conn.getRemoteSocketAddress());
|
||||
}
|
||||
|
||||
private boolean verifyAccessToken(ClientHandshake handshake) {
|
||||
if (accessToken == null || accessToken.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String authorization = handshake.getFieldValue("Authorization");
|
||||
return ("Bearer " + accessToken).equals(authorization);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||
log.info("OneBot connection closed: {}, code={}, reason={}, remote={}",
|
||||
conn.getRemoteSocketAddress(), code, reason, remote);
|
||||
activeConnection.compareAndSet(conn, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket conn, String message) {
|
||||
try {
|
||||
JSONObject json = JSON.parseObject(message);
|
||||
if (json.containsKey("echo")) {
|
||||
handleActionResponse(json);
|
||||
return;
|
||||
}
|
||||
|
||||
OneBotV11Event event = OneBotV11EventCodec.parse(json);
|
||||
if (event == null) {
|
||||
log.debug("Ignore unsupported OneBot payload: {}", message);
|
||||
return;
|
||||
}
|
||||
handleEvent(event);
|
||||
} catch (Exception e) {
|
||||
log.warn("Invalid OneBot payload: {}", message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleActionResponse(JSONObject response) {
|
||||
log.debug("OneBot action response: {}", response);
|
||||
}
|
||||
|
||||
private void handleEvent(OneBotV11Event event) {
|
||||
if (event instanceof OneBotV11MetaEvent metaEvent) {
|
||||
if (metaEvent.isHeartbeat()) {
|
||||
lastHeartbeatAt.set(System.currentTimeMillis());
|
||||
log.debug("OneBot heartbeat received");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event instanceof OneBotV11MessageEvent messageEvent) {
|
||||
handleMessageEvent(messageEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Ignore unsupported OneBot event: {}", event);
|
||||
}
|
||||
|
||||
private void handleMessageEvent(OneBotV11MessageEvent event) {
|
||||
if (!event.isPrivateMessage()) {
|
||||
log.debug("Ignore non-private OneBot message, type={}", event.getMessageType().getDisplayName());
|
||||
return;
|
||||
}
|
||||
|
||||
InputData inputData = OneBotV11EventCodec.toInputData(event);
|
||||
if (inputData == null) {
|
||||
log.debug("Ignore empty OneBot private message");
|
||||
return;
|
||||
}
|
||||
receive(inputData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(WebSocket conn, Exception ex) {
|
||||
log.error("OneBotGateway error", ex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
log.info("OneBotGateway 已启动...");
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package work.slhaf.partner.external.onebot.gateway
|
||||
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGateway
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGatewayRegistration
|
||||
|
||||
class OnebotGatewayRegistration : AgentGatewayRegistration {
|
||||
override val channelName: String = "onebot_channel"
|
||||
|
||||
override fun create(params: Map<String, String>): AgentGateway<*, *> {
|
||||
val port = params["port"]?.toIntOrNull() ?: 29700
|
||||
val hostname = params["hostname"] ?: "127.0.0.1"
|
||||
val path = params["path"] ?: "/onebot/v11/ws"
|
||||
val token = params["token"]
|
||||
|
||||
require(port > 0) { "port must be greater than 0" }
|
||||
return OnebotGateway(port, hostname, path, token)
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package work.slhaf.partner.external.onebot.v11
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import org.java_websocket.WebSocket
|
||||
import java.util.*
|
||||
import kotlin.concurrent.atomics.AtomicReference
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
object OneBotV11ActionExecutor {
|
||||
|
||||
private lateinit var activeConnection: AtomicReference<WebSocket>
|
||||
|
||||
@JvmStatic
|
||||
fun bindConnectionReference(reference: AtomicReference<WebSocket>) {
|
||||
activeConnection = reference
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun sendMessage(target: String, message: String): Boolean {
|
||||
val action = buildSendMessageAction(target, message) ?: return false
|
||||
activeConnection.load().send(action.toJSONString())
|
||||
return true
|
||||
}
|
||||
|
||||
private fun buildSendMessageAction(target: String, message: String): JSONObject? {
|
||||
val segments = target.split(":")
|
||||
if (segments.size < 3 || segments[0] != "onebot") {
|
||||
return null
|
||||
}
|
||||
|
||||
val params = JSONObject()
|
||||
val actionName = when (segments[1]) {
|
||||
"private" -> {
|
||||
params["user_id"] = segments[2].toLongOrNull() ?: return null
|
||||
"send_private_msg"
|
||||
}
|
||||
|
||||
"group" -> {
|
||||
params["group_id"] = segments[2].toLongOrNull() ?: return null
|
||||
"send_group_msg"
|
||||
}
|
||||
|
||||
else -> return null
|
||||
}
|
||||
params["message"] = message
|
||||
|
||||
val action = JSONObject()
|
||||
action["action"] = actionName
|
||||
action["params"] = params
|
||||
action["echo"] = UUID.randomUUID().toString()
|
||||
return action
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package work.slhaf.partner.external.onebot.v11
|
||||
|
||||
sealed class OneBotV11Event {
|
||||
abstract val postType: OneBotV11PostType
|
||||
}
|
||||
|
||||
enum class OneBotV11PostType {
|
||||
MESSAGE,
|
||||
META_EVENT
|
||||
}
|
||||
|
||||
enum class OneBotV11MessageType(val displayName: String) {
|
||||
PRIVATE("私聊"),
|
||||
GROUP("群聊"),
|
||||
UNKNOWN("未知")
|
||||
}
|
||||
|
||||
data class OneBotV11MessageEvent(
|
||||
val messageType: OneBotV11MessageType,
|
||||
val userId: Long,
|
||||
val groupId: Long?,
|
||||
val message: Any?,
|
||||
val rawMessage: String?
|
||||
) : OneBotV11Event() {
|
||||
override val postType: OneBotV11PostType = OneBotV11PostType.MESSAGE
|
||||
|
||||
fun isPrivateMessage(): Boolean = messageType == OneBotV11MessageType.PRIVATE
|
||||
|
||||
fun isGroupMessage(): Boolean = messageType == OneBotV11MessageType.GROUP
|
||||
|
||||
fun routeTarget(): String? = when {
|
||||
isPrivateMessage() -> "onebot:private:$userId"
|
||||
isGroupMessage() && groupId != null -> "onebot:group:$groupId:$userId"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
data class OneBotV11MetaEvent(
|
||||
val metaEventType: String
|
||||
) : OneBotV11Event() {
|
||||
override val postType: OneBotV11PostType = OneBotV11PostType.META_EVENT
|
||||
|
||||
fun isHeartbeat(): Boolean = metaEventType == "heartbeat"
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package work.slhaf.partner.external.onebot.v11
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import work.slhaf.partner.api.InputData
|
||||
|
||||
object OneBotV11EventCodec {
|
||||
|
||||
@JvmStatic
|
||||
fun parse(json: JSONObject): OneBotV11Event? {
|
||||
return when (json.getString("post_type")) {
|
||||
"message" -> parseMessage(json)
|
||||
"meta_event" -> parseMetaEvent(json)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun toInputData(event: OneBotV11MessageEvent): InputData? {
|
||||
val target = event.routeTarget() ?: return null
|
||||
val content = extractText(event).takeIf { it.isNotBlank() } ?: return null
|
||||
val inputData = InputData(target, content)
|
||||
inputData.addMeta("message_type", event.messageType.displayName)
|
||||
event.groupId?.let { inputData.addMeta("group_id", it.toString()) }
|
||||
return inputData
|
||||
}
|
||||
|
||||
private fun extractText(event: OneBotV11MessageEvent): String {
|
||||
event.rawMessage?.takeIf { it.isNotBlank() }?.let { return it }
|
||||
val message = event.message ?: return ""
|
||||
return when (message) {
|
||||
is String -> message
|
||||
is JSONArray -> message.asSequence()
|
||||
.filterIsInstance<JSONObject>()
|
||||
.filter { it.getString("type") == "text" }
|
||||
.mapNotNull { it.getJSONObject("data")?.getString("text") }
|
||||
.joinToString("")
|
||||
|
||||
else -> message.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessage(json: JSONObject): OneBotV11MessageEvent {
|
||||
return OneBotV11MessageEvent(
|
||||
messageType = parseMessageType(json.getString("message_type")),
|
||||
userId = json.getLongValue("user_id"),
|
||||
groupId = json.getLong("group_id"),
|
||||
message = json["message"],
|
||||
rawMessage = json.getString("raw_message")
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseMetaEvent(json: JSONObject): OneBotV11MetaEvent {
|
||||
return OneBotV11MetaEvent(
|
||||
metaEventType = json.getString("meta_event_type") ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseMessageType(value: String?): OneBotV11MessageType {
|
||||
return when (value) {
|
||||
"private" -> OneBotV11MessageType.PRIVATE
|
||||
"group" -> OneBotV11MessageType.GROUP
|
||||
else -> OneBotV11MessageType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
package work.slhaf.partner.external.onebot.v11
|
||||
|
||||
import work.slhaf.partner.api.InteractionEvent
|
||||
import work.slhaf.partner.api.ModuleEvent
|
||||
import work.slhaf.partner.api.ReplyEvent
|
||||
import work.slhaf.partner.api.SystemEvent
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Converts Partner streaming interaction events into readable OneBot chat bubbles.
|
||||
*
|
||||
* OneBot/QQ messages are discrete chat messages, not frontend delta patches. This dispatcher buffers
|
||||
* ReplyEvent.APPEND chunks and sends complete natural blocks whenever possible.
|
||||
*/
|
||||
class OneBotV11ResponseDispatcher(
|
||||
private val actionExecutor: OneBotV11ActionExecutor = OneBotV11ActionExecutor,
|
||||
private val maxMessageChars: Int = 1200,
|
||||
private val softFlushChars: Int = 1000,
|
||||
) : AutoCloseable {
|
||||
|
||||
private val buffers = ConcurrentHashMap<String, StringBuilder>()
|
||||
|
||||
fun accept(event: InteractionEvent) {
|
||||
when (event) {
|
||||
is ReplyEvent -> acceptReply(event)
|
||||
is ModuleEvent -> {
|
||||
flush(event.target)
|
||||
sendStandalone(event.target, event.data.content)
|
||||
}
|
||||
|
||||
is SystemEvent -> {
|
||||
flush(event.target)
|
||||
sendStandalone(event.target, formatSystemEvent(event))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun flush(target: String) {
|
||||
val buffer = buffers[target] ?: return
|
||||
val content = buffer.toString().trim()
|
||||
buffer.clear()
|
||||
if (content.isNotBlank()) {
|
||||
sendSegments(target, content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
buffers.keys.toList().forEach(::flush)
|
||||
}
|
||||
|
||||
private fun acceptReply(event: ReplyEvent) {
|
||||
val content = event.content
|
||||
if (content.isNotBlank()) {
|
||||
val buffer = buffers.computeIfAbsent(event.target) { StringBuilder() }
|
||||
when (event.mode) {
|
||||
ReplyEvent.ContentMode.APPEND -> buffer.append(content)
|
||||
ReplyEvent.ContentMode.REPLACE -> {
|
||||
buffer.clear()
|
||||
buffer.append(content)
|
||||
}
|
||||
}
|
||||
flushCompletedBlocks(event.target, buffer)
|
||||
}
|
||||
|
||||
if (event.status == InteractionEvent.EventStatus.DONE || event.status == InteractionEvent.EventStatus.ERROR) {
|
||||
flush(event.target)
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushCompletedBlocks(target: String, buffer: StringBuilder) {
|
||||
while (buffer.isNotEmpty()) {
|
||||
if (hasUnclosedCodeFence(buffer)) {
|
||||
return
|
||||
}
|
||||
|
||||
val completedBlockEnd = findCompletedBlockEnd(buffer)
|
||||
if (completedBlockEnd > 0) {
|
||||
val block = buffer.substring(0, completedBlockEnd).trim()
|
||||
buffer.delete(0, completedBlockEnd)
|
||||
trimLeadingBlankLines(buffer)
|
||||
if (block.isNotBlank()) {
|
||||
sendSegments(target, block)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (buffer.length >= softFlushChars) {
|
||||
val splitAt = findNaturalSplit(buffer, maxMessageChars.coerceAtMost(buffer.length))
|
||||
if (splitAt != null && splitAt > 0) {
|
||||
val block = buffer.substring(0, splitAt).trim()
|
||||
buffer.delete(0, splitAt)
|
||||
trimLeadingBlankLines(buffer)
|
||||
if (block.isNotBlank()) {
|
||||
sendSegments(target, block)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun findCompletedBlockEnd(buffer: CharSequence): Int {
|
||||
val text = buffer.toString()
|
||||
val blankLine = findFirstBlankLine(text)
|
||||
if (blankLine <= 0) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val candidate = text.substring(0, blankLine).trim()
|
||||
if (candidate.isBlank()) {
|
||||
return blankLine
|
||||
}
|
||||
|
||||
val nextBlockStart = skipBlankLines(text, blankLine)
|
||||
if (isHeadingOnly(candidate) && nextBlockStart < text.length) {
|
||||
val nextBlankLine = findFirstBlankLine(text, nextBlockStart)
|
||||
if (nextBlankLine > 0) {
|
||||
return nextBlankLine
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
return blankLine
|
||||
}
|
||||
|
||||
private fun sendSegments(target: String, content: String) {
|
||||
splitForOneBot(content).forEach { segment ->
|
||||
actionExecutor.sendMessage(target, segment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendStandalone(target: String, content: String) {
|
||||
if (content.isBlank()) {
|
||||
return
|
||||
}
|
||||
sendSegments(target, content.trim())
|
||||
}
|
||||
|
||||
private fun splitForOneBot(content: String): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
val buffer = StringBuilder(content.trim())
|
||||
|
||||
while (buffer.length > maxMessageChars) {
|
||||
val splitAt = findNaturalSplit(buffer, maxMessageChars)
|
||||
?.takeIf { it > 0 }
|
||||
?: maxMessageChars
|
||||
val segment = buffer.substring(0, splitAt).trim()
|
||||
if (segment.isNotBlank()) {
|
||||
result.add(segment)
|
||||
}
|
||||
buffer.delete(0, splitAt)
|
||||
trimLeadingBlankLines(buffer)
|
||||
}
|
||||
|
||||
val rest = buffer.toString().trim()
|
||||
if (rest.isNotBlank()) {
|
||||
result.add(rest)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun findNaturalSplit(text: CharSequence, preferredMax: Int): Int? {
|
||||
val max = preferredMax.coerceAtMost(text.length)
|
||||
if (max <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
val searchStart = (max * 0.45).toInt().coerceAtLeast(1)
|
||||
val priorities = listOf(
|
||||
"\n\n", "\r\n\r\n",
|
||||
"。", "!", "?", "!", "?",
|
||||
";", ";",
|
||||
":", ":",
|
||||
",", ",",
|
||||
"\n",
|
||||
" "
|
||||
)
|
||||
|
||||
for (delimiter in priorities) {
|
||||
val idx = lastIndexOf(text, delimiter, max, searchStart)
|
||||
if (idx >= 0) {
|
||||
return idx + delimiter.length
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun lastIndexOf(text: CharSequence, delimiter: String, endExclusive: Int, startInclusive: Int): Int {
|
||||
var i = endExclusive - delimiter.length
|
||||
while (i >= startInclusive) {
|
||||
var matched = true
|
||||
for (j in delimiter.indices) {
|
||||
if (text[i + j] != delimiter[j]) {
|
||||
matched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
return i
|
||||
}
|
||||
i--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun hasUnclosedCodeFence(text: CharSequence): Boolean {
|
||||
var count = 0
|
||||
var index = 0
|
||||
val value = text.toString()
|
||||
while (true) {
|
||||
val found = value.indexOf("```", index)
|
||||
if (found < 0) {
|
||||
break
|
||||
}
|
||||
count++
|
||||
index = found + 3
|
||||
}
|
||||
return count % 2 != 0
|
||||
}
|
||||
|
||||
private fun findFirstBlankLine(text: String, startIndex: Int = 0): Int {
|
||||
val lf = text.indexOf("\n\n", startIndex)
|
||||
val crlf = text.indexOf("\r\n\r\n", startIndex)
|
||||
return when {
|
||||
lf < 0 -> crlf.takeIf { it >= 0 }?.plus(4) ?: -1
|
||||
crlf < 0 -> lf + 2
|
||||
lf < crlf -> lf + 2
|
||||
else -> crlf + 4
|
||||
}
|
||||
}
|
||||
|
||||
private fun skipBlankLines(text: String, index: Int): Int {
|
||||
var i = index
|
||||
while (i < text.length && (text[i] == '\n' || text[i] == '\r')) {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
private fun trimLeadingBlankLines(buffer: StringBuilder) {
|
||||
while (buffer.isNotEmpty() && (buffer.first() == '\n' || buffer.first() == '\r')) {
|
||||
buffer.deleteCharAt(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isHeadingOnly(text: String): Boolean {
|
||||
val lines = text.lines().filter { it.isNotBlank() }
|
||||
return lines.size == 1 && lines.first().trimStart().startsWith("#")
|
||||
}
|
||||
|
||||
private fun formatSystemEvent(event: SystemEvent): String {
|
||||
return buildString {
|
||||
append(event.title)
|
||||
if (event.content.isNotBlank()) {
|
||||
append('\n')
|
||||
append(event.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import work.slhaf.partner.Main;
|
||||
import work.slhaf.partner.framework.agent.Agent;
|
||||
|
||||
public class TestMain {
|
||||
public static void main(String[] args) {
|
||||
Agent.newAgent(Main.class).launch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>partner-external-modules</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>Partner-Onebot-Adapter</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
@@ -4,12 +4,13 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf</groupId>
|
||||
<artifactId>Partner</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>Partner-Framework</artifactId>
|
||||
<artifactId>partner-framework</artifactId>
|
||||
<version>0.9.0-preview</version>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -83,6 +84,11 @@
|
||||
<artifactId>openai-java</artifactId>
|
||||
<version>4.26.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner-interaction-api</artifactId>
|
||||
<version>${partner.interaction-api.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -2,6 +2,7 @@ package work.slhaf.partner.framework.agent;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.reflections.util.ClasspathHelper;
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter;
|
||||
import work.slhaf.partner.framework.agent.config.Configurable;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentStartupException;
|
||||
@@ -9,16 +10,20 @@ import work.slhaf.partner.framework.agent.exception.ExceptionReporter;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler;
|
||||
import work.slhaf.partner.framework.agent.factory.AgentRegisterFactory;
|
||||
import work.slhaf.partner.framework.agent.factory.context.AgentContext;
|
||||
import work.slhaf.partner.framework.agent.factory.context.AgentRegisterContext;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGatewayRegistration;
|
||||
import work.slhaf.partner.framework.agent.interaction.AgentGatewayRegistry;
|
||||
import work.slhaf.partner.framework.agent.log.LogAdviceProvider;
|
||||
import work.slhaf.partner.framework.agent.log.TraceRecorder;
|
||||
import work.slhaf.partner.framework.agent.log.TraceSinkRegistry;
|
||||
import work.slhaf.partner.framework.agent.model.ModelRuntimeRegistry;
|
||||
import work.slhaf.partner.framework.agent.state.StateCenter;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* <h2>Agent 启动入口</h2>
|
||||
@@ -33,7 +38,8 @@ public final class Agent {
|
||||
|
||||
public static class AgentApp {
|
||||
|
||||
private final Class<?> applicationClass;
|
||||
private final Set<String> scanPackages = new LinkedHashSet<>();
|
||||
private final Set<String> scanDirs = new LinkedHashSet<>();
|
||||
private final Set<AgentGatewayRegistration> gatewayRegistrations = new LinkedHashSet<>();
|
||||
private final Set<ExceptionReporter> exceptionReporters = new LinkedHashSet<>();
|
||||
private final Set<Configurable> configurables = new LinkedHashSet<>();
|
||||
@@ -41,40 +47,47 @@ public final class Agent {
|
||||
private final Set<LifecycleHook> postShutdownHooks = new LinkedHashSet<>();
|
||||
|
||||
private AgentApp(Class<?> clazz) {
|
||||
this.applicationClass = clazz;
|
||||
this.scanPackages.add(clazz.getPackageName());
|
||||
}
|
||||
|
||||
public AgentApp addGatewayRegistration(AgentGatewayRegistration... registrations) {
|
||||
private void addScanPackage(String packageName) {
|
||||
if (packageName != null && !packageName.isBlank()) {
|
||||
this.scanPackages.add(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
private void addScanDir(String scanDir) {
|
||||
if (scanDir != null && !scanDir.isBlank()) {
|
||||
this.scanDirs.add(scanDir);
|
||||
}
|
||||
}
|
||||
|
||||
private void addGatewayRegistration(AgentGatewayRegistration... registrations) {
|
||||
this.gatewayRegistrations.addAll(Set.of(registrations));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AgentApp addConfigurable(Configurable configurable) {
|
||||
private void addConfigurable(Configurable configurable) {
|
||||
this.configurables.add(configurable);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AgentApp addExceptionReporter(ExceptionReporter... exceptionReporters) {
|
||||
private void addExceptionReporter(ExceptionReporter... exceptionReporters) {
|
||||
this.exceptionReporters.addAll(Set.of(exceptionReporters));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AgentApp addPreShutdownHook(String name, Runnable action) {
|
||||
return addPreShutdownHook(name, 0, action);
|
||||
private void addPreShutdownHook(String name, Runnable action) {
|
||||
addPreShutdownHook(name, 0, action);
|
||||
}
|
||||
|
||||
public AgentApp addPreShutdownHook(String name, int order, Runnable action) {
|
||||
private void addPreShutdownHook(String name, int order, Runnable action) {
|
||||
this.preShutdownHooks.add(new LifecycleHook(name, order, action));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AgentApp addPostShutdownHook(String name, Runnable action) {
|
||||
return addPostShutdownHook(name, 0, action);
|
||||
private void addPostShutdownHook(String name, Runnable action) {
|
||||
addPostShutdownHook(name, 0, action);
|
||||
}
|
||||
|
||||
public AgentApp addPostShutdownHook(String name, int order, Runnable action) {
|
||||
private void addPostShutdownHook(String name, int order, Runnable action) {
|
||||
this.postShutdownHooks.add(new LifecycleHook(name, order, action));
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean launch() {
|
||||
@@ -83,6 +96,13 @@ public final class Agent {
|
||||
ConfigCenter.INSTANCE.toString();
|
||||
StateCenter.INSTANCE.toString();
|
||||
|
||||
Path externalModuleDir = ConfigCenter.INSTANCE.getPaths().getResourcesDir().resolve("module");
|
||||
addScanDir(externalModuleDir.toString());
|
||||
|
||||
AgentRegisterContext bootstrapContext = buildRegisterContext();
|
||||
runBootstraps(bootstrapContext);
|
||||
AgentRegisterContext registerContext = buildRegisterContext();
|
||||
|
||||
// Keep startup order explicit so registries are ready before component scanning.
|
||||
for (ExceptionReporter exceptionReporter : exceptionReporters) {
|
||||
exceptionReporter.register();
|
||||
@@ -102,9 +122,7 @@ public final class Agent {
|
||||
|
||||
registerShutdownHooks();
|
||||
|
||||
Path externalModuleDir = ConfigCenter.INSTANCE.getPaths().getResourcesDir().resolve("module");
|
||||
AgentRegisterFactory.addScanDir(externalModuleDir.toString());
|
||||
AgentRegisterFactory.launch(applicationClass.getPackageName());
|
||||
AgentRegisterFactory.launch(registerContext);
|
||||
|
||||
// Try to init configurable, and start config listening
|
||||
ConfigCenter.INSTANCE.initAll();
|
||||
@@ -120,6 +138,78 @@ public final class Agent {
|
||||
}
|
||||
}
|
||||
|
||||
private AgentRegisterContext buildRegisterContext() {
|
||||
return new AgentRegisterContext(new ArrayList<>(buildScanUrls()));
|
||||
}
|
||||
|
||||
private Set<URL> buildScanUrls() {
|
||||
Set<URL> urls = new LinkedHashSet<>();
|
||||
for (String packageName : scanPackages) {
|
||||
urls.addAll(ClasspathHelper.forPackage(packageName));
|
||||
}
|
||||
for (String scanDir : scanDirs) {
|
||||
urls.addAll(scanDirToUrls(scanDir));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
private Set<URL> scanDirToUrls(String scanDir) {
|
||||
Set<URL> urls = new LinkedHashSet<>();
|
||||
File file = new File(scanDir);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
return urls;
|
||||
}
|
||||
try {
|
||||
File[] files = file.listFiles();
|
||||
if (files == null) {
|
||||
return urls;
|
||||
}
|
||||
for (File item : files) {
|
||||
if (item.getName().endsWith(".jar")) {
|
||||
urls.add(item.toURI().toURL());
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
} catch (Exception e) {
|
||||
throw new AgentStartupException("Failed to load scan dir URLs from: " + scanDir, "agent-bootstrap", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void runBootstraps(AgentRegisterContext context) {
|
||||
context.getReflections().getSubTypesOf(AgentBootstrap.class).stream()
|
||||
.filter(this::isConcreteBootstrap)
|
||||
.map(this::instantiateBootstrap)
|
||||
.sorted(Comparator.comparingInt(AgentBootstrap::order))
|
||||
.forEach(AgentBootstrap::bootstrap);
|
||||
}
|
||||
|
||||
private boolean isConcreteBootstrap(Class<? extends AgentBootstrap> bootstrapClass) {
|
||||
int modifiers = bootstrapClass.getModifiers();
|
||||
return !bootstrapClass.isInterface()
|
||||
&& !bootstrapClass.isAnnotation()
|
||||
&& !bootstrapClass.isEnum()
|
||||
&& !bootstrapClass.isArray()
|
||||
&& !bootstrapClass.isPrimitive()
|
||||
&& !Modifier.isAbstract(modifiers)
|
||||
&& !bootstrapClass.isSynthetic()
|
||||
&& !bootstrapClass.isAnonymousClass()
|
||||
&& !bootstrapClass.isLocalClass();
|
||||
}
|
||||
|
||||
private AgentBootstrap instantiateBootstrap(Class<? extends AgentBootstrap> bootstrapClass) {
|
||||
try {
|
||||
Constructor<? extends AgentBootstrap> constructor = bootstrapClass.getDeclaredConstructor(AgentApp.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(this);
|
||||
} catch (Exception e) {
|
||||
throw new AgentStartupException(
|
||||
"Failed to instantiate AgentBootstrap: " + bootstrapClass.getName(),
|
||||
"agent-bootstrap",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerShutdownHooks() {
|
||||
AgentContext.INSTANCE.addPreShutdownHook(
|
||||
"agent-gateway-registry-close",
|
||||
@@ -135,9 +225,9 @@ public final class Agent {
|
||||
StateCenter.INSTANCE::save
|
||||
);
|
||||
AgentContext.INSTANCE.addPostShutdownHook(
|
||||
"trace-recorder-close",
|
||||
"trace-sink-registry-close",
|
||||
90,
|
||||
TraceRecorder.INSTANCE::close
|
||||
TraceSinkRegistry.INSTANCE::close
|
||||
);
|
||||
AgentContext.INSTANCE.addPostShutdownHook(
|
||||
"config-center-close",
|
||||
@@ -153,4 +243,55 @@ public final class Agent {
|
||||
private record LifecycleHook(String name, int order, Runnable action) {
|
||||
}
|
||||
|
||||
public static abstract class AgentBootstrap {
|
||||
|
||||
private final AgentApp agentApp;
|
||||
|
||||
protected AgentBootstrap(AgentApp agentApp) {
|
||||
this.agentApp = Objects.requireNonNull(agentApp, "agentApp");
|
||||
}
|
||||
|
||||
public int order() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected abstract void bootstrap();
|
||||
|
||||
protected final void addScanPackage(String packageName) {
|
||||
agentApp.addScanPackage(packageName);
|
||||
}
|
||||
|
||||
protected final void addScanDir(String scanDir) {
|
||||
agentApp.addScanDir(scanDir);
|
||||
}
|
||||
|
||||
protected final void addPreShutdownHook(String name, Runnable action) {
|
||||
agentApp.addPreShutdownHook(name, action);
|
||||
}
|
||||
|
||||
protected final void addPreShutdownHook(String name, int order, Runnable action) {
|
||||
agentApp.addPreShutdownHook(name, order, action);
|
||||
}
|
||||
|
||||
protected final void addPostShutdownHook(String name, Runnable action) {
|
||||
agentApp.addPostShutdownHook(name, action);
|
||||
}
|
||||
|
||||
protected final void addPostShutdownHook(String name, int order, Runnable action) {
|
||||
agentApp.addPostShutdownHook(name, order, action);
|
||||
}
|
||||
|
||||
protected final void addExceptionReporter(ExceptionReporter... exceptionReporters) {
|
||||
agentApp.addExceptionReporter(exceptionReporters);
|
||||
}
|
||||
|
||||
protected final void addConfigurable(Configurable configurable) {
|
||||
agentApp.addConfigurable(configurable);
|
||||
}
|
||||
|
||||
protected final void addGatewayRegistration(AgentGatewayRegistration... gatewayRegistrations) {
|
||||
agentApp.addGatewayRegistration(gatewayRegistrations);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+26
-6
@@ -1,6 +1,5 @@
|
||||
package work.slhaf.partner.framework.agent.config
|
||||
|
||||
import com.alibaba.fastjson2.JSON
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException
|
||||
@@ -88,23 +87,41 @@ object ConfigCenter : AutoCloseable {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun initAll() {
|
||||
val errorConfig = mutableMapOf<Path, String>()
|
||||
registrations.forEach { (path, registration) ->
|
||||
val pair = loadConfig(path, registration)
|
||||
val configFile = paths.configDir.resolve(path).normalize()
|
||||
val pair = loadConfig(configFile, registration)
|
||||
if (pair != null) {
|
||||
(registration as ConfigRegistration<Config>).init(pair.first, pair.second)
|
||||
return
|
||||
return@forEach
|
||||
}
|
||||
val defaultConfig = registration.defaultConfig()
|
||||
if (defaultConfig != null) {
|
||||
(registration as ConfigRegistration<Config>).init(defaultConfig, null)
|
||||
return@forEach
|
||||
}
|
||||
val configDoc = resolveConfigDoc(registration.type())
|
||||
errorConfig[path] = configDoc
|
||||
}
|
||||
if (errorConfig.isNotEmpty()) {
|
||||
throw AgentStartupException(
|
||||
"Failed to init config, related path: $path, config definition: $configDoc",
|
||||
"Failed to init configurations: ${buildErrorConfigReport(errorConfig)}",
|
||||
COMPONENT_NAME
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildErrorConfigReport(errorConfig: MutableMap<Path, String>): String {
|
||||
return buildString {
|
||||
appendLine()
|
||||
append("\n=====Config Missing=====\n")
|
||||
errorConfig.forEach { (path, config) ->
|
||||
appendLine("\n>>>\t$path\t>>>\n")
|
||||
append(config)
|
||||
appendLine()
|
||||
appendLine("\n<<<\t$path\t<<<")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveConfigDoc(type: Class<out Config>): String {
|
||||
@@ -159,6 +176,7 @@ object ConfigCenter : AutoCloseable {
|
||||
return field.isSynthetic || Modifier.isStatic(field.modifiers)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun handleUpsert(thisDir: Path, context: Path?) {
|
||||
if (context == null || !Files.isRegularFile(context) || !isJsonFile(context)) {
|
||||
return
|
||||
@@ -166,6 +184,7 @@ object ConfigCenter : AutoCloseable {
|
||||
reloadIfRegistered(context)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun handleDelete(thisDir: Path, context: Path?) {
|
||||
if (context == null || !isJsonFile(context)) {
|
||||
return
|
||||
@@ -174,7 +193,7 @@ object ConfigCenter : AutoCloseable {
|
||||
if (!registrations.containsKey(relativePath)) {
|
||||
return
|
||||
}
|
||||
log.info("Config deleted, skipped reload: {}", relativePath)
|
||||
log.debug("Config deleted, skipped reload: {}", relativePath)
|
||||
}
|
||||
|
||||
private fun reconcileAll() {
|
||||
@@ -205,10 +224,11 @@ object ConfigCenter : AutoCloseable {
|
||||
|
||||
private fun loadConfig(file: Path, registration: ConfigRegistration<out Config>): Pair<Config, JSONObject>? {
|
||||
return try {
|
||||
val json = JSON.parseObject(Files.readString(file, StandardCharsets.UTF_8))
|
||||
val json = JSONObject.parseObject(Files.readString(file, StandardCharsets.UTF_8))
|
||||
val config = json.toJavaObject(registration.type())
|
||||
config to json
|
||||
} catch (_: Exception) {
|
||||
log.warn("Unable to load config from ${file.toAbsolutePath()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -2,6 +2,8 @@ package work.slhaf.partner.framework.agent.exception
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import work.slhaf.partner.framework.agent.log.TraceEvent
|
||||
import work.slhaf.partner.framework.agent.log.TraceRecorder
|
||||
|
||||
abstract class AgentException @JvmOverloads constructor(
|
||||
message: String,
|
||||
@@ -11,7 +13,7 @@ abstract class AgentException @JvmOverloads constructor(
|
||||
return ExceptionReport(
|
||||
this::class.java.simpleName,
|
||||
message ?: "",
|
||||
this
|
||||
cause
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -98,6 +100,7 @@ object LoggerExceptionReporter : ExceptionReporter {
|
||||
|
||||
override fun report(exception: AgentException) {
|
||||
val exceptionReport = exception.toReport().toDetailedString()
|
||||
TraceRecorder.record(TraceEvent("exception", JSONObject.of("exception", exceptionReport)))
|
||||
log.error("exception occurred: $exceptionReport")
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -33,7 +33,11 @@ object AgentRegisterFactory {
|
||||
@JvmStatic
|
||||
fun launch(packageName: String) {
|
||||
urls.addAll(packageNameToURL(packageName))
|
||||
val registerContext = AgentRegisterContext(urls)
|
||||
launch(AgentRegisterContext(urls))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun launch(registerContext: AgentRegisterContext) {
|
||||
// 1. 校验 Component 级别注解是否合规,避免注入到异常位置
|
||||
ComponentAnnotationValidatorFactory().execute(registerContext)
|
||||
// 2. 收集所有的 AgentComponent 实例
|
||||
|
||||
+6
@@ -13,6 +13,7 @@ import java.lang.reflect.Modifier
|
||||
*
|
||||
* 注入关系:
|
||||
* - `sub + standalone -> running`
|
||||
* - `standalone -> sub`
|
||||
* - `sub + standalone -> standalone`
|
||||
* - `sub + standalone -> additionalComponent`
|
||||
*
|
||||
@@ -44,6 +45,11 @@ class ComponentInjectorFactory : AgentBaseFactory() {
|
||||
standaloneModules.forEach { it.injectTarget.add(running.instance) }
|
||||
}
|
||||
|
||||
subModules.forEach { sub ->
|
||||
injectIntoTarget(sub.instance, standaloneInstances)
|
||||
standaloneModules.forEach { it.injectTarget.add(sub.instance) }
|
||||
}
|
||||
|
||||
standaloneModules.forEach { standalone ->
|
||||
val providersForStandalone = subInstances + standaloneInstances.filter { it !== standalone.instance }
|
||||
injectIntoTarget(standalone.instance, providersForStandalone)
|
||||
|
||||
+4
-2
@@ -13,6 +13,8 @@ import java.time.ZonedDateTime
|
||||
|
||||
object AgentContext {
|
||||
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
private val _modules =
|
||||
mutableMapOf<String, ModuleContextData<AbstractAgentModule>>()
|
||||
|
||||
@@ -156,7 +158,6 @@ object AgentContext {
|
||||
}
|
||||
|
||||
fun trigger(hooks: List<ShutdownHookDesc>, instances: Instances) {
|
||||
val log = LoggerFactory.getLogger(AgentContext::class.java)
|
||||
hooks.sortedBy { it.order }
|
||||
.forEach {
|
||||
try {
|
||||
@@ -168,7 +169,6 @@ object AgentContext {
|
||||
}
|
||||
|
||||
fun triggerLifecycleHooks(hooks: List<LifecycleShutdownHookDesc>) {
|
||||
val log = LoggerFactory.getLogger(AgentContext::class.java)
|
||||
hooks.sortedBy { it.order }
|
||||
.forEach {
|
||||
try {
|
||||
@@ -180,6 +180,7 @@ object AgentContext {
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
log.info("Shutdown hooks triggering...")
|
||||
val instances = computeInstances()
|
||||
triggerLifecycleHooks(preShutdownHooks)
|
||||
shutdownHooks[ShutdownHookDesc.Type.RUNNING]?.let { trigger(it, instances) }
|
||||
@@ -188,6 +189,7 @@ object AgentContext {
|
||||
shutdownHooks[ShutdownHookDesc.Type.SUB]?.let { trigger(it, instances) }
|
||||
shutdownHooks[ShutdownHookDesc.Type.CAPABILITY]?.let { trigger(it, instances) }
|
||||
triggerLifecycleHooks(postShutdownHooks)
|
||||
log.info("Shutdown hooks triggered...")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -5,15 +5,22 @@ import org.reflections.scanners.Scanners
|
||||
import org.reflections.util.ConfigurationBuilder
|
||||
import java.lang.reflect.Method
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
|
||||
class AgentRegisterContext(urls: List<URL>) {
|
||||
|
||||
private val classLoader = URLClassLoader(
|
||||
urls.toTypedArray(),
|
||||
Thread.currentThread().contextClassLoader
|
||||
)
|
||||
|
||||
val reflections: Reflections = Reflections(
|
||||
ConfigurationBuilder().setScanners(
|
||||
Scanners.FieldsAnnotated,
|
||||
Scanners.SubTypes,
|
||||
Scanners.MethodsAnnotated,
|
||||
Scanners.TypesAnnotated
|
||||
).setUrls(urls)
|
||||
).setUrls(urls).addClassLoaders(classLoader)
|
||||
)
|
||||
|
||||
val capabilityFactoryContext: CapabilityFactoryContext = CapabilityFactoryContext()
|
||||
|
||||
+1
-8
@@ -1,19 +1,12 @@
|
||||
package work.slhaf.partner.framework.agent.interaction;
|
||||
|
||||
import work.slhaf.partner.framework.agent.interaction.data.InputData;
|
||||
import work.slhaf.partner.api.InputData;
|
||||
import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext;
|
||||
|
||||
public interface AgentGateway<I extends InputData, C extends RunningFlowContext> extends ResponseChannel, AutoCloseable {
|
||||
|
||||
void launch();
|
||||
|
||||
AgentGatewayRegistration registration();
|
||||
|
||||
@Override
|
||||
default void register() {
|
||||
registration().register();
|
||||
}
|
||||
|
||||
default void receive(I inputData) {
|
||||
C parsedContext = parseRunningFlowContext(inputData);
|
||||
AgentRuntime.INSTANCE.submit(parsedContext);
|
||||
|
||||
+4
-1
@@ -68,7 +68,10 @@ object AgentGatewayRegistry : Configurable, ConfigRegistration<AgentGatewayRegis
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultConfig(): AgentGatewayRegistryConfig? = null
|
||||
override fun defaultConfig(): AgentGatewayRegistryConfig = AgentGatewayRegistryConfig(
|
||||
"websocket_channel",
|
||||
listOf(AgentGatewayChannelConfig("websocket_channel", emptyMap()))
|
||||
)
|
||||
|
||||
override fun close() = registryLock.withLock {
|
||||
val currentChannels = runningChannels.keys.toList()
|
||||
|
||||
+1
-1
@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONObject
|
||||
import com.alibaba.fastjson2.annotation.JSONField
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import work.slhaf.partner.api.InteractionEvent
|
||||
import work.slhaf.partner.framework.agent.config.Config
|
||||
import work.slhaf.partner.framework.agent.config.ConfigDoc
|
||||
import work.slhaf.partner.framework.agent.config.ConfigRegistration
|
||||
@@ -12,7 +13,6 @@ import work.slhaf.partner.framework.agent.exception.ExceptionReporterHandler
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule
|
||||
import work.slhaf.partner.framework.agent.factory.context.AgentContext
|
||||
import work.slhaf.partner.framework.agent.factory.context.ModuleContextData
|
||||
import work.slhaf.partner.framework.agent.interaction.data.InteractionEvent
|
||||
import work.slhaf.partner.framework.agent.interaction.flow.RunningFlowContext
|
||||
import work.slhaf.partner.framework.agent.support.Result
|
||||
import java.nio.file.Path
|
||||
|
||||
+1
-8
@@ -2,7 +2,7 @@ package work.slhaf.partner.framework.agent.interaction
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import work.slhaf.partner.framework.agent.interaction.data.InteractionEvent
|
||||
import work.slhaf.partner.api.InteractionEvent
|
||||
|
||||
interface ResponseChannel {
|
||||
|
||||
@@ -10,9 +10,6 @@ interface ResponseChannel {
|
||||
|
||||
fun response(event: InteractionEvent)
|
||||
|
||||
fun register() {
|
||||
AgentRuntime.registerResponseChannel(channelName, this)
|
||||
}
|
||||
}
|
||||
|
||||
object LogChannel : ResponseChannel {
|
||||
@@ -22,10 +19,6 @@ object LogChannel : ResponseChannel {
|
||||
override val channelName: String
|
||||
get() = "log_channel"
|
||||
|
||||
init {
|
||||
register()
|
||||
}
|
||||
|
||||
override fun response(event: InteractionEvent) {
|
||||
log.info(JSONObject.toJSONString(event))
|
||||
}
|
||||
|
||||
+2
-1
@@ -12,7 +12,8 @@ import kotlin.math.min
|
||||
*/
|
||||
abstract class RunningFlowContext protected constructor(
|
||||
inputs: List<InputEntry>,
|
||||
private var firstInputEpochMillis: Long
|
||||
private var firstInputEpochMillis: Long,
|
||||
val resopnseChannel: String? = null
|
||||
) {
|
||||
/**
|
||||
* 消息来源: 由谁发出
|
||||
|
||||
+8
-8
@@ -4,25 +4,19 @@ import com.alibaba.fastjson2.JSONException
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import work.slhaf.partner.framework.agent.config.Config
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter
|
||||
import work.slhaf.partner.framework.agent.config.ConfigRegistration
|
||||
import work.slhaf.partner.framework.agent.config.Configurable
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
object LogAdviceProvider : Configurable, ConfigRegistration<AdviceLoggingConfig> {
|
||||
|
||||
private val logPath = ConfigCenter.paths.stateDir.resolve("trace").normalize().toAbsolutePath()
|
||||
private val _adviceRegistry = mutableSetOf<LogAdvice<*, *>>()
|
||||
val adviceRegistry: Set<LogAdvice<*, *>>
|
||||
get() = _adviceRegistry
|
||||
|
||||
var logLevel = AdviceLoggingConfig.LogLevel.NONE
|
||||
|
||||
init {
|
||||
Files.createDirectories(logPath)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun <I, O> createAdvice(
|
||||
@@ -54,8 +48,7 @@ object LogAdviceProvider : Configurable, ConfigRegistration<AdviceLoggingConfig>
|
||||
}
|
||||
|
||||
internal fun record(result: AdviceResult) {
|
||||
val path = logPath.resolve(result.adviceTarget).normalize().toAbsolutePath()
|
||||
val traceEvent = TraceEvent(path, result.toJSON(), result.finishTime.toInstant().toEpochMilli())
|
||||
val traceEvent = TraceEvent(result.adviceTarget, result.toJSON(), result.finishTime.toInstant().toEpochMilli())
|
||||
TraceRecorder.record(traceEvent)
|
||||
}
|
||||
|
||||
@@ -70,6 +63,13 @@ object LogAdviceProvider : Configurable, ConfigRegistration<AdviceLoggingConfig>
|
||||
logLevel = config.logLevel
|
||||
}
|
||||
|
||||
override fun onReload(
|
||||
config: AdviceLoggingConfig,
|
||||
json: JSONObject?
|
||||
) {
|
||||
this.logLevel = config.logLevel
|
||||
}
|
||||
|
||||
override fun defaultConfig(): AdviceLoggingConfig = AdviceLoggingConfig(AdviceLoggingConfig.LogLevel.NONE)
|
||||
}
|
||||
|
||||
|
||||
+98
-9
@@ -4,7 +4,7 @@ import com.alibaba.fastjson2.JSONObject
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import org.slf4j.LoggerFactory
|
||||
import work.slhaf.partner.framework.agent.factory.context.AgentContext
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter
|
||||
import java.io.BufferedWriter
|
||||
import java.io.OutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -16,11 +16,86 @@ import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
object TraceSinkRegistry {
|
||||
|
||||
private val log = LoggerFactory.getLogger(TraceSinkRegistry::class.java)
|
||||
private val sinks = CopyOnWriteArrayList<TraceSink>()
|
||||
private val closed = AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
FileTraceSink.register()
|
||||
}
|
||||
|
||||
internal fun register(sink: TraceSink) {
|
||||
if (closed.get()) {
|
||||
log.warn("TraceSinkRegistry is closed, skip trace sink: {}", sink.javaClass.name)
|
||||
return
|
||||
}
|
||||
if (!sinks.contains(sink)) {
|
||||
sinks.add(sink)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun unregister(sink: TraceSink) {
|
||||
sinks.remove(sink)
|
||||
}
|
||||
|
||||
fun publish(event: TraceEvent) {
|
||||
for (sink in sinks) {
|
||||
runCatching {
|
||||
sink.consume(event)
|
||||
}.onFailure {
|
||||
log.error("Trace sink failed: {}", sink.javaClass.name, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
sinks.forEach { sink ->
|
||||
runCatching {
|
||||
sink.close()
|
||||
}.onFailure {
|
||||
log.error("Failed to close trace sink: {}", sink.javaClass.name, it)
|
||||
}
|
||||
}
|
||||
sinks.clear()
|
||||
}
|
||||
}
|
||||
|
||||
interface TraceSink : AutoCloseable {
|
||||
|
||||
fun register() {
|
||||
TraceSinkRegistry.register(this)
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
TraceSinkRegistry.unregister(this)
|
||||
}
|
||||
|
||||
|
||||
fun consume(event: TraceEvent)
|
||||
|
||||
override fun close() {
|
||||
}
|
||||
}
|
||||
|
||||
object TraceRecorder {
|
||||
|
||||
@JvmStatic
|
||||
fun record(event: TraceEvent) {
|
||||
TraceSinkRegistry.publish(event)
|
||||
}
|
||||
}
|
||||
|
||||
object FileTraceSink : TraceSink {
|
||||
|
||||
private const val ACTIVE_FILE_NAME = "active.jsonl"
|
||||
private const val HISTORICAL_DIR_NAME = "historical"
|
||||
private const val ARCHIVED_DIR_NAME = "archived"
|
||||
@@ -40,7 +115,6 @@ object TraceRecorder {
|
||||
private val writerJob: Job
|
||||
|
||||
init {
|
||||
AgentContext.addPostShutdownHook("trace-recorder-close") { close() }
|
||||
writerJob = scope.launch {
|
||||
try {
|
||||
for (event in channel) {
|
||||
@@ -54,18 +128,18 @@ object TraceRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
fun record(event: TraceEvent) {
|
||||
override fun consume(event: TraceEvent) {
|
||||
if (closed.get()) {
|
||||
log.warn("TraceRecorder is closed, skip event for path: {}", event.path)
|
||||
log.warn("FileTraceSink is closed, skip event for key: {}", event.key)
|
||||
return
|
||||
}
|
||||
val result = channel.trySend(event)
|
||||
if (result.isFailure) {
|
||||
log.error("Failed to enqueue trace event for path: {}", event.path, result.exceptionOrNull())
|
||||
log.error("Failed to enqueue trace event for key: {}", event.key, result.exceptionOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
override fun close() {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
@@ -77,7 +151,7 @@ object TraceRecorder {
|
||||
}
|
||||
|
||||
private fun handleEvent(event: TraceEvent) {
|
||||
val basePath = event.path.normalize().toAbsolutePath()
|
||||
val basePath = resolveBasePath(event.key)
|
||||
runCatching {
|
||||
val state = writerStates.getOrPut(basePath) { openWriterState(basePath) }
|
||||
writeEvent(state, event)
|
||||
@@ -85,12 +159,27 @@ object TraceRecorder {
|
||||
rotateActiveFile(state)
|
||||
}
|
||||
}.onFailure {
|
||||
log.error("Failed to persist trace event for path: {}", basePath, it)
|
||||
log.error("Failed to persist trace event for key: {}, path: {}", event.key, basePath, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveBasePath(key: String): Path {
|
||||
val traceRoot = ConfigCenter.paths.stateDir.resolve("trace").normalize().toAbsolutePath()
|
||||
val normalizedKey = key.trim().ifBlank { "default" }
|
||||
val candidate = traceRoot.resolve(normalizedKey).normalize().toAbsolutePath()
|
||||
if (candidate.startsWith(traceRoot)) {
|
||||
return candidate
|
||||
}
|
||||
return traceRoot.resolve(sanitizeKey(normalizedKey)).normalize().toAbsolutePath()
|
||||
}
|
||||
|
||||
private fun sanitizeKey(key: String): String {
|
||||
return key.replace(Regex("[^A-Za-z0-9._-]+"), "_").trim('_').ifBlank { "default" }
|
||||
}
|
||||
|
||||
private fun writeEvent(state: WriterState, event: TraceEvent) {
|
||||
val json = JSONObject(event.payload)
|
||||
json["traceKey"] = event.key
|
||||
json["timestamp"] = event.timestamp
|
||||
val line = json.toJSONString()
|
||||
state.writer.write(line)
|
||||
@@ -327,7 +416,7 @@ object TraceRecorder {
|
||||
}
|
||||
|
||||
data class TraceEvent @JvmOverloads constructor(
|
||||
val path: Path,
|
||||
val key: String,
|
||||
val payload: JSONObject,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
+4
-3
@@ -15,11 +15,12 @@ interface ActivateModel {
|
||||
|
||||
fun streamChat(
|
||||
messages: List<Message>,
|
||||
handler: StreamChatMessageConsumer
|
||||
consumer: StreamChatMessageConsumer
|
||||
): Result<Unit> {
|
||||
return ModelRuntimeRegistry.resolveProvider(modelKey())
|
||||
.streamChat(mergeMessages(messages), handler)
|
||||
.onFailure { ExceptionReporterHandler.report(it) }
|
||||
.streamChat(mergeMessages(messages), consumer)
|
||||
.onSuccess { consumer.onComplete() }
|
||||
.onFailure { consumer.onError(it) }
|
||||
}
|
||||
|
||||
fun <T : Any> formattedChat(messages: List<Message>, responseType: Class<T>): Result<T> {
|
||||
|
||||
+37
-32
@@ -13,7 +13,6 @@ import work.slhaf.partner.framework.agent.model.provider.ModelProvider
|
||||
import work.slhaf.partner.framework.agent.model.provider.ProviderOverride
|
||||
import work.slhaf.partner.framework.agent.model.provider.openai.OpenAiCompatibleProvider
|
||||
import java.nio.file.Path
|
||||
import java.util.Locale.getDefault
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
@@ -86,8 +85,15 @@ object ModelRuntimeRegistry : Configurable, ConfigRegistration<ModelRuntimeRegis
|
||||
override fun type(): Class<ModelRuntimeRegistryConfig> = ModelRuntimeRegistryConfig::class.java
|
||||
|
||||
override fun init(config: ModelRuntimeRegistryConfig, json: JSONObject?) = providerLock.withLock {
|
||||
val acceptableConfig = try {
|
||||
parseJsonConfig(json)
|
||||
} catch (e: Exception) {
|
||||
log.warn("Unable to load model config", e)
|
||||
config
|
||||
}
|
||||
|
||||
try {
|
||||
applyConfig(config)
|
||||
applyConfig(acceptableConfig)
|
||||
} catch (e: ModelRegistryException) {
|
||||
throw ModelRegistryStartupException(
|
||||
e.message ?: "Failed to apply model runtime config",
|
||||
@@ -104,36 +110,9 @@ object ModelRuntimeRegistry : Configurable, ConfigRegistration<ModelRuntimeRegis
|
||||
val baseProviderSnapshot = baseProvider.toMap()
|
||||
val runtimeProviderSnapshot = runtimeProvider.toMap()
|
||||
try {
|
||||
val providerSetJson = root.getJSONArray("providerConfigSet")
|
||||
?: throw runtimeModelException("providerConfigSet is missing or not an array")
|
||||
baseProvider.clear()
|
||||
for (i in providerSetJson.indices) {
|
||||
val providerJson = providerSetJson.getJSONObject(i)
|
||||
?: throw runtimeModelException("providerConfigSet[$i] is not an object")
|
||||
val typeText = providerJson.getString("type")
|
||||
?: throw runtimeModelException(
|
||||
"providerConfigSet[$i].type is missing",
|
||||
providerJson.getString("name") ?: COMPONENT_NAME
|
||||
)
|
||||
val providerType = try {
|
||||
ProviderConfig.ProviderType.valueOf(typeText.uppercase(getDefault()))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw runtimeModelException(
|
||||
"Unsupported provider type: $typeText",
|
||||
providerJson.getString("name") ?: COMPONENT_NAME,
|
||||
cause = e
|
||||
)
|
||||
}
|
||||
val concreteProviderConfig = when (providerType) {
|
||||
OPENAI_COMPATIBLE -> providerJson.toJavaObject(OpenAiCompatibleProviderConfig::class.java)
|
||||
}
|
||||
registerProvider(concreteProviderConfig)
|
||||
}
|
||||
if (!baseProvider.containsKey(DEFAULT_PROVIDER)) {
|
||||
throw runtimeModelException("Provider default not found", DEFAULT_PROVIDER)
|
||||
}
|
||||
runtimeProvider.clear()
|
||||
config.runtimeConfigSet.forEach { forkProvider(it) }
|
||||
val parsedConfig = parseJsonConfig(root)
|
||||
applyConfig(parsedConfig)
|
||||
log.info("Model config updated")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error while loading runtime provider config", e)
|
||||
baseProvider.clear()
|
||||
@@ -143,6 +122,32 @@ object ModelRuntimeRegistry : Configurable, ConfigRegistration<ModelRuntimeRegis
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseJsonConfig(json: JSONObject?): ModelRuntimeRegistryConfig {
|
||||
if (json == null) {
|
||||
throw runtimeModelException("Unable to find model config")
|
||||
}
|
||||
val providerConfigSet = json.getJSONArray("providerConfigSet").filterIsInstance<JSONObject>()
|
||||
.map { config ->
|
||||
val type = config.getString("type")
|
||||
val config = if (type.equals(OPENAI_COMPATIBLE.name.uppercase())) {
|
||||
config.toJavaObject(OpenAiCompatibleProviderConfig::class.java)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (config == null) {
|
||||
throw runtimeModelException("Unknown config type: $type")
|
||||
}
|
||||
config
|
||||
}.toSet()
|
||||
|
||||
val runtimeConfigSet = json.getJSONArray("runtimeConfigSet").filterIsInstance<JSONObject>()
|
||||
.map { config ->
|
||||
config.toJavaObject(RuntimeProviderConfig::class.java)
|
||||
}.toSet()
|
||||
|
||||
return ModelRuntimeRegistryConfig(providerConfigSet, runtimeConfigSet)
|
||||
}
|
||||
|
||||
override fun defaultConfig(): ModelRuntimeRegistryConfig? {
|
||||
val defaultBaseUrl = System.getenv("PARTNER_DEFAULT_BASE_URL") ?: return null
|
||||
val defaultApiKey = System.getenv("PARTNER_DEFAULT_API_KEY") ?: return null
|
||||
|
||||
+8
-8
@@ -1,16 +1,16 @@
|
||||
package work.slhaf.partner.framework.agent.model;
|
||||
|
||||
public abstract class StreamChatMessageConsumer {
|
||||
private final StringBuilder responseText = new StringBuilder();
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
|
||||
public void onDelta(String delta) {
|
||||
consumeDelta(delta);
|
||||
responseText.append(delta);
|
||||
public interface StreamChatMessageConsumer {
|
||||
|
||||
void onDelta(String delta);
|
||||
|
||||
default void onComplete() {
|
||||
}
|
||||
|
||||
public String collectResponse() {
|
||||
return responseText.toString();
|
||||
default void onError(AgentRuntimeException exception) {
|
||||
}
|
||||
|
||||
protected abstract void consumeDelta(String delta);
|
||||
String collectResponse();
|
||||
}
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package work.slhaf.partner.framework.agent.model.provider.openai;
|
||||
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
|
||||
final class JsonShapeInstructionBuilder {
|
||||
|
||||
private static final int MAX_DEPTH = 4;
|
||||
|
||||
private JsonShapeInstructionBuilder() {
|
||||
}
|
||||
|
||||
static String build(Class<?> responseType) {
|
||||
return "Return only a valid JSON object.\n"
|
||||
+ "The JSON object must directly match this exact output shape for " + responseType.getSimpleName() + ":\n"
|
||||
+ buildJsonShape(responseType, 0, new HashSet<>()) + "\n\n"
|
||||
+ "Rules:\n"
|
||||
+ "- The top-level object must directly match the shape above.\n"
|
||||
+ "- Do not wrap it in \"" + responseType.getSimpleName() + "\" or any other class name.\n"
|
||||
+ "- Do not rename fields or invent alternative field names.\n"
|
||||
+ "- Do not output markdown, comments, explanations, or code fences.";
|
||||
}
|
||||
|
||||
private static String buildJsonShape(Type type, int depth, Set<Type> visiting) {
|
||||
if (depth > MAX_DEPTH) {
|
||||
return "{}";
|
||||
}
|
||||
if (type instanceof ParameterizedType parameterizedType) {
|
||||
Type rawType = parameterizedType.getRawType();
|
||||
if (rawType instanceof Class<?> rawClass && Collection.class.isAssignableFrom(rawClass)) {
|
||||
Type[] arguments = parameterizedType.getActualTypeArguments();
|
||||
if (arguments.length == 0) {
|
||||
return "[]";
|
||||
}
|
||||
return arrayShape(buildJsonShape(arguments[0], depth + 1, visiting), depth);
|
||||
}
|
||||
if (rawType instanceof Class<?> rawClass && Map.class.isAssignableFrom(rawClass)) {
|
||||
return "{}";
|
||||
}
|
||||
return buildJsonShape(rawType, depth, visiting);
|
||||
}
|
||||
if (type instanceof GenericArrayType genericArrayType) {
|
||||
return arrayShape(buildJsonShape(genericArrayType.getGenericComponentType(), depth + 1, visiting), depth);
|
||||
}
|
||||
if (!(type instanceof Class<?> clazz)) {
|
||||
return "null";
|
||||
}
|
||||
if (clazz.isArray()) {
|
||||
return arrayShape(buildJsonShape(clazz.getComponentType(), depth + 1, visiting), depth);
|
||||
}
|
||||
if (clazz == String.class || clazz == Character.class || clazz == char.class) {
|
||||
return "\"\"";
|
||||
}
|
||||
if (clazz == boolean.class || clazz == Boolean.class) {
|
||||
return "false";
|
||||
}
|
||||
if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive()) {
|
||||
return "0";
|
||||
}
|
||||
if (clazz.isEnum()) {
|
||||
Object[] constants = clazz.getEnumConstants();
|
||||
return constants == null || constants.length == 0 ? "\"\"" : "\"" + constants[0] + "\"";
|
||||
}
|
||||
if (Collection.class.isAssignableFrom(clazz)) {
|
||||
return "[]";
|
||||
}
|
||||
if (Map.class.isAssignableFrom(clazz)) {
|
||||
return "{}";
|
||||
}
|
||||
if (clazz.getName().startsWith("java.")) {
|
||||
return "\"\"";
|
||||
}
|
||||
if (visiting.contains(clazz)) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
visiting.add(clazz);
|
||||
List<Field> fields = Arrays.stream(clazz.getDeclaredFields())
|
||||
.filter(field -> !field.isSynthetic())
|
||||
.filter(field -> !Modifier.isStatic(field.getModifiers()))
|
||||
.filter(field -> !Modifier.isTransient(field.getModifiers()))
|
||||
.toList();
|
||||
if (fields.isEmpty()) {
|
||||
visiting.remove(clazz);
|
||||
return "{}";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("{\n");
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
Field field = fields.get(i);
|
||||
builder.append(indent(depth + 1))
|
||||
.append("\"")
|
||||
.append(field.getName())
|
||||
.append("\": ")
|
||||
.append(buildJsonShape(field.getGenericType(), depth + 1, visiting));
|
||||
if (i < fields.size() - 1) {
|
||||
builder.append(",");
|
||||
}
|
||||
builder.append("\n");
|
||||
}
|
||||
builder.append(indent(depth)).append("}");
|
||||
visiting.remove(clazz);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String arrayShape(String itemShape, int depth) {
|
||||
return "[\n" + indent(depth + 1) + itemShape.replace("\n", "\n" + indent(depth + 1)) + "\n" + indent(depth) + "]";
|
||||
}
|
||||
|
||||
private static String indent(int depth) {
|
||||
return " ".repeat(Math.max(0, depth));
|
||||
}
|
||||
}
|
||||
+107
-10
@@ -1,5 +1,6 @@
|
||||
package work.slhaf.partner.framework.agent.model.provider.openai;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
@@ -17,14 +18,14 @@ import work.slhaf.partner.framework.agent.model.provider.ProviderOverride;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
public class OpenAiCompatibleProvider extends ModelProvider {
|
||||
|
||||
private static final int MAX_ATTEMPTS = 3;
|
||||
private static final ConcurrentMap<StructuredOutputCacheKey, StructuredOutputMode> STRUCTURED_OUTPUT_MODE_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
private final String baseUrl;
|
||||
private final String apiKey;
|
||||
@@ -102,15 +103,102 @@ public class OpenAiCompatibleProvider extends ModelProvider {
|
||||
public <T> @NotNull Result<T> formattedChat(@NotNull List<Message> messages, @NotNull Class<T> responseType) {
|
||||
return executeWithRetry(
|
||||
"OpenAI-compatible provider failed to complete the structured chat request after 3 attempts.",
|
||||
() -> {
|
||||
StructuredChatCompletionCreateParams<T> params = buildParams(messages).toBuilder()
|
||||
.responseFormat(responseType)
|
||||
.build();
|
||||
return extractStructured(client.chat().completions().create(params));
|
||||
}
|
||||
() -> formattedChatByCachedMode(messages, responseType)
|
||||
);
|
||||
}
|
||||
|
||||
private <T> T formattedChatByCachedMode(List<Message> messages, Class<T> responseType) {
|
||||
StructuredOutputCacheKey cacheKey = new StructuredOutputCacheKey(baseUrl, model);
|
||||
StructuredOutputMode mode = STRUCTURED_OUTPUT_MODE_CACHE.getOrDefault(cacheKey, StructuredOutputMode.UNKNOWN);
|
||||
if (mode == StructuredOutputMode.PROMPT_ONLY_JSON) {
|
||||
return promptOnlyFormattedChat(messages, responseType);
|
||||
}
|
||||
return strictThenPromptFallback(messages, responseType, cacheKey);
|
||||
}
|
||||
|
||||
private <T> T strictThenPromptFallback(List<Message> messages, Class<T> responseType, StructuredOutputCacheKey cacheKey) {
|
||||
try {
|
||||
T result = strictFormattedChat(messages, responseType);
|
||||
STRUCTURED_OUTPUT_MODE_CACHE.put(cacheKey, StructuredOutputMode.STRICT_RESPONSE_FORMAT);
|
||||
return result;
|
||||
} catch (Exception structuredFailure) {
|
||||
try {
|
||||
T result = promptOnlyFormattedChat(messages, responseType);
|
||||
if (StructuredOutputFailureClassifier.shouldDowngradeToPromptOnlyJson(structuredFailure)) {
|
||||
STRUCTURED_OUTPUT_MODE_CACHE.put(cacheKey, StructuredOutputMode.PROMPT_ONLY_JSON);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception fallbackFailure) {
|
||||
structuredFailure.addSuppressed(fallbackFailure);
|
||||
throw structuredFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T strictFormattedChat(List<Message> messages, Class<T> responseType) {
|
||||
StructuredChatCompletionCreateParams<T> params = buildParams(ensureJsonInstruction(messages, responseType)).toBuilder()
|
||||
.responseFormat(responseType)
|
||||
.build();
|
||||
return extractStructured(client.chat().completions().create(params));
|
||||
}
|
||||
|
||||
private <T> T promptOnlyFormattedChat(List<Message> messages, Class<T> responseType) {
|
||||
ChatCompletionCreateParams params = buildParams(ensureJsonInstruction(messages, responseType));
|
||||
String rawText = extractText(client.chat().completions().create(params));
|
||||
String jsonText = extractJsonObject(rawText);
|
||||
return JSON.parseObject(jsonText, responseType);
|
||||
}
|
||||
|
||||
private List<Message> ensureJsonInstruction(List<Message> messages, Class<?> responseType) {
|
||||
String jsonInstruction = JsonShapeInstructionBuilder.build(responseType);
|
||||
|
||||
List<Message> patched = new ArrayList<>(messages.size() + 1);
|
||||
boolean merged = false;
|
||||
for (Message message : messages) {
|
||||
if (!merged && message.getRole() == Message.Character.SYSTEM) {
|
||||
String separator = message.getContent().isBlank() ? "" : "\n\n";
|
||||
patched.add(new Message(
|
||||
Message.Character.SYSTEM,
|
||||
message.getContent() + separator + jsonInstruction
|
||||
));
|
||||
merged = true;
|
||||
continue;
|
||||
}
|
||||
patched.add(message);
|
||||
}
|
||||
if (!merged) {
|
||||
patched.addFirst(new Message(Message.Character.SYSTEM, jsonInstruction));
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
|
||||
|
||||
private String extractJsonObject(String text) {
|
||||
String trimmed = text == null ? "" : text.trim();
|
||||
if (trimmed.isBlank()) {
|
||||
throw invokeException("OpenAI-compatible provider returned empty content in prompt-only JSON fallback.", null);
|
||||
}
|
||||
trimmed = stripMarkdownFence(trimmed);
|
||||
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
||||
return trimmed;
|
||||
}
|
||||
int start = trimmed.indexOf('{');
|
||||
int end = trimmed.lastIndexOf('}');
|
||||
if (start >= 0 && end > start) {
|
||||
return trimmed.substring(start, end + 1).trim();
|
||||
}
|
||||
throw invokeException("OpenAI-compatible provider prompt-only JSON fallback returned no JSON object.", null);
|
||||
}
|
||||
|
||||
private String stripMarkdownFence(String text) {
|
||||
String trimmed = text.trim();
|
||||
if (!trimmed.startsWith("```")) {
|
||||
return trimmed;
|
||||
}
|
||||
String withoutOpeningFence = trimmed.replaceFirst("^```[a-zA-Z0-9_-]*\\s*", "");
|
||||
return withoutOpeningFence.replaceFirst("\\s*```$", "").trim();
|
||||
}
|
||||
|
||||
private ChatCompletionCreateParams buildParams(List<Message> messages) {
|
||||
ChatCompletionCreateParams.Builder paramsBuilder = ChatCompletionCreateParams.builder()
|
||||
.model(model)
|
||||
@@ -204,6 +292,15 @@ public class OpenAiCompatibleProvider extends ModelProvider {
|
||||
return result;
|
||||
}
|
||||
|
||||
private enum StructuredOutputMode {
|
||||
UNKNOWN,
|
||||
STRICT_RESPONSE_FORMAT,
|
||||
PROMPT_ONLY_JSON
|
||||
}
|
||||
|
||||
private record StructuredOutputCacheKey(String baseUrl, String model) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ThrowingSupplier<T> {
|
||||
T get() throws Exception;
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package work.slhaf.partner.framework.agent.model.provider.openai;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
final class StructuredOutputFailureClassifier {
|
||||
|
||||
private static final List<String> TRANSIENT_FAILURE_PATTERNS = List.of(
|
||||
"timeout",
|
||||
"error reading response",
|
||||
"streamresetexception",
|
||||
"interruptedioexception",
|
||||
"sockettimeoutexception",
|
||||
"connectexception",
|
||||
"connection reset",
|
||||
"503",
|
||||
"502",
|
||||
"504",
|
||||
"429",
|
||||
"internalserverexception",
|
||||
"provider returned error",
|
||||
"openaiioexception",
|
||||
"request failed"
|
||||
);
|
||||
private static final List<String> AUTH_OR_CONFIG_FAILURE_PATTERNS = List.of(
|
||||
"没有权限",
|
||||
"not activated",
|
||||
"permission",
|
||||
"unauthorized",
|
||||
"forbidden",
|
||||
"invalid api key",
|
||||
"sslexception",
|
||||
"unsupported or unrecognized ssl message"
|
||||
);
|
||||
private static final List<String> STRUCTURED_COMPATIBILITY_FAILURE_PATTERNS = List.of(
|
||||
"response_format type is unavailable",
|
||||
"messages must contain the word 'json'",
|
||||
"messages must contain the word json",
|
||||
"structured chat completion returned empty content",
|
||||
"error parsing json:",
|
||||
"unrecognizedpropertyexception",
|
||||
"mismatchedinputexception",
|
||||
"invalidformatexception"
|
||||
);
|
||||
|
||||
private StructuredOutputFailureClassifier() {
|
||||
}
|
||||
|
||||
static boolean shouldDowngradeToPromptOnlyJson(Throwable failure) {
|
||||
FailureSnapshot snapshot = FailureSnapshot.from(failure);
|
||||
if (snapshot.containsAny(TRANSIENT_FAILURE_PATTERNS)) {
|
||||
return false;
|
||||
}
|
||||
if (snapshot.containsAny(AUTH_OR_CONFIG_FAILURE_PATTERNS)) {
|
||||
return false;
|
||||
}
|
||||
return snapshot.containsAny(STRUCTURED_COMPATIBILITY_FAILURE_PATTERNS);
|
||||
}
|
||||
|
||||
private record FailureSnapshot(String text) {
|
||||
|
||||
static FailureSnapshot from(Throwable failure) {
|
||||
Set<Throwable> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
StringBuilder builder = new StringBuilder();
|
||||
collect(failure, builder, visited);
|
||||
return new FailureSnapshot(builder.toString().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private static void collect(Throwable throwable, StringBuilder builder, Set<Throwable> visited) {
|
||||
if (throwable == null || visited.contains(throwable)) {
|
||||
return;
|
||||
}
|
||||
visited.add(throwable);
|
||||
builder.append(throwable.getClass().getName()).append('\n');
|
||||
if (throwable.getMessage() != null) {
|
||||
builder.append(throwable.getMessage()).append('\n');
|
||||
}
|
||||
collect(throwable.getCause(), builder, visited);
|
||||
for (Throwable suppressed : throwable.getSuppressed()) {
|
||||
collect(suppressed, builder, visited);
|
||||
}
|
||||
}
|
||||
|
||||
boolean containsAny(List<String> patterns) {
|
||||
return patterns.stream().anyMatch(text::contains);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -33,6 +33,7 @@ object StateCenter {
|
||||
}
|
||||
|
||||
if (!finalStatePath.exists()) {
|
||||
stateRecord.saveEnabled = true
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -43,18 +44,18 @@ object StateCenter {
|
||||
return null
|
||||
}
|
||||
|
||||
stateRecord.loaded = true
|
||||
stateRecord.saveEnabled = true
|
||||
|
||||
return JSONObject.parseObject(finalStatePath.readText())
|
||||
}
|
||||
|
||||
fun load(path: Path) {
|
||||
val finalStatePath = ConfigCenter.paths.stateDir.normalize().resolve(path).normalize()
|
||||
if (!stateRegistry.containsKey(path)) {
|
||||
if (!stateRegistry.containsKey(finalStatePath)) {
|
||||
return
|
||||
}
|
||||
val record = stateRegistry[finalStatePath] ?: return
|
||||
record.loaded = true
|
||||
record.saveEnabled = true
|
||||
if (!finalStatePath.exists()) {
|
||||
return
|
||||
}
|
||||
@@ -68,7 +69,7 @@ object StateCenter {
|
||||
|
||||
fun save() {
|
||||
stateRegistry.forEach { (path, record) ->
|
||||
if (!record.loaded) {
|
||||
if (!record.saveEnabled) {
|
||||
return@forEach
|
||||
}
|
||||
path.parent?.let(Files::createDirectories)
|
||||
@@ -156,5 +157,5 @@ sealed interface StateValue {
|
||||
|
||||
data class StateRecord(
|
||||
val serializable: StateSerializable,
|
||||
var loaded: Boolean = false
|
||||
)
|
||||
var saveEnabled: Boolean = false
|
||||
)
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package work.slhaf.partner.framework.agent.model.provider.openai;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JsonShapeInstructionBuilderTest {
|
||||
|
||||
@Test
|
||||
void shouldBuildShapeInstructionForComplexPojo() {
|
||||
String instruction = JsonShapeInstructionBuilder.build(ComplexResponse.class);
|
||||
|
||||
assertTrue(instruction.startsWith("Return only a valid JSON object."));
|
||||
assertTrue(instruction.contains("The JSON object must directly match this exact output shape for ComplexResponse:"));
|
||||
assertTrue(instruction.contains("Do not wrap it in \"ComplexResponse\" or any other class name."));
|
||||
assertTrue(instruction.contains("Do not rename fields or invent alternative field names."));
|
||||
|
||||
assertTrue(instruction.contains("\"id\": \"\""));
|
||||
assertTrue(instruction.contains("\"success\": false"));
|
||||
assertTrue(instruction.contains("\"retryCount\": 0"));
|
||||
assertTrue(instruction.contains("\"score\": 0"));
|
||||
assertTrue(instruction.contains("\"status\": \"READY\""));
|
||||
assertTrue(instruction.contains("\"tags\": ["));
|
||||
assertTrue(instruction.contains("\"items\": ["));
|
||||
assertTrue(instruction.contains("\"metadata\": {}"));
|
||||
assertTrue(instruction.contains("\"matrix\": ["));
|
||||
assertTrue(instruction.contains("\"nested\": {"));
|
||||
assertTrue(instruction.contains("\"name\": \"\""));
|
||||
assertTrue(instruction.contains("\"enabled\": false"));
|
||||
assertTrue(instruction.contains("\"notes\": ["));
|
||||
|
||||
assertFalse(instruction.contains("staticValue"));
|
||||
assertFalse(instruction.contains("transientValue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreventRecursiveExpansion() {
|
||||
String instruction = JsonShapeInstructionBuilder.build(RecursiveResponse.class);
|
||||
|
||||
assertTrue(instruction.contains("\"name\": \"\""));
|
||||
assertTrue(instruction.contains("\"next\": {}"));
|
||||
}
|
||||
|
||||
private enum Status {
|
||||
READY,
|
||||
DONE
|
||||
}
|
||||
|
||||
private static class ComplexResponse {
|
||||
private static String staticValue;
|
||||
private transient String transientValue;
|
||||
|
||||
private String id;
|
||||
private boolean success;
|
||||
private int retryCount;
|
||||
private Double score;
|
||||
private Status status;
|
||||
private List<String> tags;
|
||||
private List<NestedItem> items;
|
||||
private Map<String, String> metadata;
|
||||
private int[] matrix;
|
||||
private NestedItem nested;
|
||||
}
|
||||
|
||||
private static class NestedItem {
|
||||
private String name;
|
||||
private Boolean enabled;
|
||||
private List<String> notes;
|
||||
}
|
||||
|
||||
private static class RecursiveResponse {
|
||||
private String name;
|
||||
private RecursiveResponse next;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -65,6 +65,10 @@ class ConfigCenterTest {
|
||||
writeJson(workingDir.resolve(INVALID_PATH), "invalid-init", 1);
|
||||
writeJson(workingDir.resolve(IDEMPOTENT_PATH), "idempotent-init", 1);
|
||||
writeJson(configDir.resolve(INITIAL_PATH), "initial-config-dir", 1);
|
||||
writeJson(configDir.resolve(NESTED_PATH), "nested-config-dir", 1);
|
||||
writeJson(configDir.resolve(DELETE_PATH), "delete-config-dir", 1);
|
||||
writeJson(configDir.resolve(INVALID_PATH), "invalid-config-dir", 1);
|
||||
writeJson(configDir.resolve(IDEMPOTENT_PATH), "idempotent-config-dir", 1);
|
||||
|
||||
ConfigCenter.INSTANCE.register(() -> {
|
||||
Map<Path, ConfigRegistration<? extends Config>> declared = new LinkedHashMap<>();
|
||||
@@ -159,7 +163,7 @@ class ConfigCenterTest {
|
||||
@Test
|
||||
@Order(1)
|
||||
void testStartOnlyInitializesOneRegisteredConfigAndDoesNotTriggerReload() {
|
||||
Assertions.assertEquals(1, totalInitCount());
|
||||
Assertions.assertEquals(5, totalInitCount());
|
||||
Assertions.assertEquals(0, totalReloadCount());
|
||||
}
|
||||
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package work.slhaf.partner.framework.agent.state;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import work.slhaf.partner.framework.agent.config.ConfigCenter;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class StateCenterTest {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void clearRegistry() throws Exception {
|
||||
Field registryField = StateCenter.class.getDeclaredField("stateRegistry");
|
||||
registryField.setAccessible(true);
|
||||
ConcurrentHashMap<Path, StateRecord> registry = (ConcurrentHashMap<Path, StateRecord>) registryField.get(StateCenter.INSTANCE);
|
||||
registry.clear();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
clearRegistry();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateStateFileOnFirstSaveWhenNoExistingFile() throws Exception {
|
||||
Path relativePath = Path.of("state-center-test", UUID.randomUUID() + ".json");
|
||||
Path finalPath = ConfigCenter.INSTANCE.getPaths().getStateDir().resolve(relativePath).normalize();
|
||||
Files.deleteIfExists(finalPath);
|
||||
|
||||
StubStateSerializable serializable = new StubStateSerializable(relativePath, false, "fresh");
|
||||
serializable.register();
|
||||
StateCenter.INSTANCE.save();
|
||||
|
||||
assertTrue(Files.exists(finalPath));
|
||||
JSONObject saved = JSONObject.parseObject(Files.readString(finalPath, StandardCharsets.UTF_8));
|
||||
assertEquals("fresh", saved.getString("value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotOverwriteExistingFileUntilManualLoadWhenAutoLoadDisabled() throws Exception {
|
||||
Path relativePath = Path.of("state-center-test", UUID.randomUUID() + ".json");
|
||||
Path finalPath = ConfigCenter.INSTANCE.getPaths().getStateDir().resolve(relativePath).normalize();
|
||||
Files.createDirectories(finalPath.getParent());
|
||||
Files.writeString(finalPath, "{\"value\":\"original\"}", StandardCharsets.UTF_8);
|
||||
|
||||
StubStateSerializable serializable = new StubStateSerializable(relativePath, false, "new-value");
|
||||
serializable.register();
|
||||
StateCenter.INSTANCE.save();
|
||||
|
||||
JSONObject untouched = JSONObject.parseObject(Files.readString(finalPath, StandardCharsets.UTF_8));
|
||||
assertEquals("original", untouched.getString("value"));
|
||||
|
||||
serializable.load();
|
||||
serializable.value = "after-load";
|
||||
StateCenter.INSTANCE.save();
|
||||
|
||||
JSONObject saved = JSONObject.parseObject(Files.readString(finalPath, StandardCharsets.UTF_8));
|
||||
assertEquals("after-load", saved.getString("value"));
|
||||
}
|
||||
|
||||
private static final class StubStateSerializable implements StateSerializable {
|
||||
private final Path statePath;
|
||||
private final boolean autoLoadOnRegister;
|
||||
private String value;
|
||||
|
||||
private StubStateSerializable(Path statePath, boolean autoLoadOnRegister, String value) {
|
||||
this.statePath = statePath;
|
||||
this.autoLoadOnRegister = autoLoadOnRegister;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return statePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(JSONObject state) {
|
||||
value = state.getString("value");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
state.append("value", StateValue.Companion.str(value));
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean autoLoadOnRegister() {
|
||||
return autoLoadOnRegister;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>work.slhaf.partner</groupId>
|
||||
<artifactId>partner</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>partner-interaction-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user