mirror of
https://github.com/slhaf/Partner.git
synced 2026-10-25 10:53:50 +00:00
Compare commits
172
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96817d84fe | ||
|
|
dd64599154 | ||
|
|
4b638b756e | ||
|
|
23a1b7093e | ||
|
|
9de46f3589 | ||
|
|
fd8a0642b3 | ||
|
|
cffb369aef | ||
|
|
a929b3e0e6 | ||
|
|
fe6895d10b | ||
|
|
8323f8ed13 | ||
|
|
0e1201253d | ||
|
|
f3213675ff | ||
|
|
26ef5d875d | ||
|
|
047d1b56fe | ||
|
|
11aae1a353 | ||
|
|
e5dcb49028 | ||
|
|
70a94d9c30 | ||
|
|
ef096e76b3 | ||
|
|
ed743521ec | ||
|
|
cb8ddfe4e2 | ||
|
|
756c0a12ad | ||
|
|
8a5b844a4a | ||
|
|
8d29ea4c9e | ||
|
|
4770eaf42f | ||
|
|
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
|
||||
+5
-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,7 @@ build/
|
||||
/Partner-Core/data/
|
||||
/.ai/mcp/mcp.json
|
||||
/.codex
|
||||
|
||||
# Maven / build outputs
|
||||
dependency-reduced-pom.xml
|
||||
/.backup/
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public abstract class VectorClient {
|
||||
|
||||
public static boolean status = false;
|
||||
public static VectorClient INSTANCE;
|
||||
public static String VECTOR_MODEL_ID;
|
||||
|
||||
public static void startClient(VectorConfig config) {
|
||||
try {
|
||||
@@ -23,6 +24,7 @@ public abstract class VectorClient {
|
||||
return;
|
||||
}
|
||||
status = true;
|
||||
VECTOR_MODEL_ID = config.modelId;
|
||||
} catch (VectorClientStartupException e) {
|
||||
throw e;
|
||||
} catch (VectorClientExecutionException e) {
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public class VectorClientRegistry implements Configurable, ConfigRegistration<Ve
|
||||
@Nullable
|
||||
@Override
|
||||
public VectorConfig defaultConfig() {
|
||||
return new VectorConfig(false, null);
|
||||
return new VectorConfig(false, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,10 +5,16 @@ import work.slhaf.partner.framework.agent.config.Config;
|
||||
public sealed class VectorConfig extends Config permits VectorConfig.Ollama, VectorConfig.Onnx {
|
||||
final boolean enabled;
|
||||
final Type type;
|
||||
final String modelId;
|
||||
|
||||
public VectorConfig(boolean enabled, Type type) {
|
||||
public VectorConfig(boolean enabled, Type type, String modelId) {
|
||||
this.enabled = enabled;
|
||||
this.type = type;
|
||||
this.modelId = modelId;
|
||||
}
|
||||
|
||||
protected static String fallbackModelId(String modelId, String fallback) {
|
||||
return modelId == null || modelId.isBlank() ? fallback : modelId;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
@@ -21,8 +27,8 @@ public sealed class VectorConfig extends Config permits VectorConfig.Ollama, Vec
|
||||
final String tokenizerPath;
|
||||
final String embeddingModelPath;
|
||||
|
||||
public Onnx(boolean enabled, Type type, String tokenizerPath, String embeddingModelPath) {
|
||||
super(enabled, type);
|
||||
public Onnx(boolean enabled, Type type, String tokenizerPath, String embeddingModelPath, String modelId) {
|
||||
super(enabled, type, fallbackModelId(modelId, embeddingModelPath));
|
||||
this.tokenizerPath = tokenizerPath;
|
||||
this.embeddingModelPath = embeddingModelPath;
|
||||
}
|
||||
@@ -33,12 +39,10 @@ public sealed class VectorConfig extends Config permits VectorConfig.Ollama, Vec
|
||||
final String ollamaEmbeddingUrl;
|
||||
final String ollamaEmbeddingModel;
|
||||
|
||||
public Ollama(boolean enabled, Type type, String ollamaEmbeddingUrl, String ollamaEmbeddingModel) {
|
||||
super(enabled, type);
|
||||
public Ollama(boolean enabled, Type type, String ollamaEmbeddingUrl, String ollamaEmbeddingModel, String modelId) {
|
||||
super(enabled, type, fallbackModelId(modelId, ollamaEmbeddingModel));
|
||||
this.ollamaEmbeddingUrl = ollamaEmbeddingUrl;
|
||||
this.ollamaEmbeddingModel = ollamaEmbeddingModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+67
-51
@@ -50,66 +50,70 @@ final class ActionPoolStateCodec {
|
||||
}
|
||||
|
||||
private static StateValue.Obj encodeExecutableAction(ExecutableAction action) {
|
||||
Map<String, StateValue> actionMap = new LinkedHashMap<>();
|
||||
actionMap.put("kind", StateValue.str(action instanceof SchedulableExecutableAction ? "schedulable" : "immediate"));
|
||||
actionMap.put("uuid", StateValue.str(action.getUuid()));
|
||||
actionMap.put("source", StateValue.str(action.getSource()));
|
||||
actionMap.put("reason", StateValue.str(action.getReason()));
|
||||
actionMap.put("description", StateValue.str(action.getDescription()));
|
||||
actionMap.put("status", StateValue.str(action.getStatus().name()));
|
||||
actionMap.put("tendency", StateValue.str(action.getTendency()));
|
||||
actionMap.put("executing_stage", StateValue.num(action.getExecutingStage()));
|
||||
Map<String, Object> actionMap = new LinkedHashMap<>();
|
||||
actionMap.put("kind", action instanceof SchedulableExecutableAction ? "schedulable" : "immediate");
|
||||
actionMap.put("uuid", action.getUuid());
|
||||
actionMap.put("source", action.getSource());
|
||||
actionMap.put("reason", action.getReason());
|
||||
actionMap.put("description", action.getDescription());
|
||||
actionMap.put("status", action.getStatus().name());
|
||||
actionMap.put("tendency", action.getTendency());
|
||||
actionMap.put("executing_stage", action.getExecutingStage());
|
||||
|
||||
String result = resolveExecutableResult(action);
|
||||
if (result != null) {
|
||||
actionMap.put("result", StateValue.str(result));
|
||||
actionMap.put("result", result);
|
||||
}
|
||||
if (action instanceof SchedulableExecutableAction schedulableAction) {
|
||||
actionMap.put("schedule_type", StateValue.str(schedulableAction.getScheduleType().name()));
|
||||
actionMap.put("schedule_content", StateValue.str(schedulableAction.getScheduleContent()));
|
||||
actionMap.put("enabled", StateValue.bool(schedulableAction.getEnabled()));
|
||||
actionMap.put("schedule_histories", StateValue.arr(encodeScheduleHistories(schedulableAction)));
|
||||
actionMap.put("schedule_type", schedulableAction.getScheduleType().name());
|
||||
actionMap.put("schedule_content", schedulableAction.getScheduleContent());
|
||||
actionMap.put("enabled", schedulableAction.getEnabled());
|
||||
actionMap.put("schedule_histories", encodeScheduleHistories(schedulableAction));
|
||||
}
|
||||
|
||||
List<StateValue> chainStates = action.getActionChain().entrySet().stream()
|
||||
List<StateValue.Obj> chainStates = action.getActionChain().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.<StateValue>map(entry -> {
|
||||
Map<String, StateValue> stageMap = new LinkedHashMap<>();
|
||||
stageMap.put("stage", StateValue.num(entry.getKey()));
|
||||
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
|
||||
.map(metaAction -> (StateValue) encodeMetaAction(metaAction))
|
||||
.toList()));
|
||||
.map(entry -> {
|
||||
Map<String, Object> stageMap = new LinkedHashMap<>();
|
||||
stageMap.put("stage", entry.getKey());
|
||||
String stageDescription = action.getStageDescriptions().get(entry.getKey());
|
||||
if (stageDescription != null && !stageDescription.isBlank()) {
|
||||
stageMap.put("description", stageDescription);
|
||||
}
|
||||
stageMap.put("actions", entry.getValue().stream()
|
||||
.map(ActionPoolStateCodec::encodeMetaAction)
|
||||
.toList());
|
||||
return StateValue.obj(stageMap);
|
||||
}).toList();
|
||||
actionMap.put("action_chain", StateValue.arr(chainStates));
|
||||
actionMap.put("action_chain", chainStates);
|
||||
|
||||
actionMap.put("history", StateValue.arr(encodeHistoryStages(action.getHistory())));
|
||||
actionMap.put("history", encodeHistoryStages(action.getHistory()));
|
||||
|
||||
return StateValue.obj(actionMap);
|
||||
}
|
||||
|
||||
private static StateValue.Obj encodeMetaAction(MetaAction metaAction) {
|
||||
Map<String, StateValue> metaMap = new LinkedHashMap<>();
|
||||
metaMap.put("name", StateValue.str(metaAction.getName()));
|
||||
metaMap.put("io", StateValue.bool(metaAction.getIo()));
|
||||
Map<String, Object> metaMap = new LinkedHashMap<>();
|
||||
metaMap.put("name", metaAction.getName());
|
||||
metaMap.put("io", metaAction.getIo());
|
||||
if (metaAction.getLauncher() != null) {
|
||||
metaMap.put("launcher", StateValue.str(metaAction.getLauncher()));
|
||||
metaMap.put("launcher", metaAction.getLauncher());
|
||||
}
|
||||
metaMap.put("type", StateValue.str(metaAction.getType().name()));
|
||||
metaMap.put("location", StateValue.str(metaAction.getLocation()));
|
||||
metaMap.put("params_json", StateValue.str(JSONObject.toJSONString(metaAction.getParams())));
|
||||
metaMap.put("result_status", StateValue.str(metaAction.getResult().getStatus().name()));
|
||||
metaMap.put("type", metaAction.getType().name());
|
||||
metaMap.put("location", metaAction.getLocation());
|
||||
metaMap.put("params_json", JSONObject.toJSONString(metaAction.getParams()));
|
||||
metaMap.put("result_status", metaAction.getResult().getStatus().name());
|
||||
if (metaAction.getResult().getData() != null) {
|
||||
metaMap.put("result_data", StateValue.str(metaAction.getResult().getData()));
|
||||
metaMap.put("result_data", metaAction.getResult().getData());
|
||||
}
|
||||
return StateValue.obj(metaMap);
|
||||
}
|
||||
|
||||
private static StateValue.Obj encodeHistoryAction(HistoryAction historyAction) {
|
||||
Map<String, StateValue> historyMap = new LinkedHashMap<>();
|
||||
historyMap.put("action_key", StateValue.str(historyAction.actionKey()));
|
||||
historyMap.put("description", StateValue.str(historyAction.description()));
|
||||
historyMap.put("result", StateValue.str(historyAction.result()));
|
||||
Map<String, Object> historyMap = new LinkedHashMap<>();
|
||||
historyMap.put("action_key", historyAction.actionKey());
|
||||
historyMap.put("description", historyAction.description());
|
||||
historyMap.put("result", historyAction.result());
|
||||
return StateValue.obj(historyMap);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -272,26 +288,26 @@ final class ActionPoolStateCodec {
|
||||
return restored;
|
||||
}
|
||||
|
||||
private static List<StateValue> encodeHistoryStages(Map<Integer, ? extends List<HistoryAction>> historyMap) {
|
||||
private static List<StateValue.Obj> encodeHistoryStages(Map<Integer, ? extends List<HistoryAction>> historyMap) {
|
||||
return historyMap.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.<StateValue>map(entry -> {
|
||||
Map<String, StateValue> stageMap = new LinkedHashMap<>();
|
||||
stageMap.put("stage", StateValue.num(entry.getKey()));
|
||||
stageMap.put("actions", StateValue.arr(entry.getValue().stream()
|
||||
.map(historyAction -> (StateValue) encodeHistoryAction(historyAction))
|
||||
.toList()));
|
||||
.map(entry -> {
|
||||
Map<String, Object> stageMap = new LinkedHashMap<>();
|
||||
stageMap.put("stage", entry.getKey());
|
||||
stageMap.put("actions", entry.getValue().stream()
|
||||
.map(ActionPoolStateCodec::encodeHistoryAction)
|
||||
.toList());
|
||||
return StateValue.obj(stageMap);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private static List<StateValue> encodeScheduleHistories(SchedulableExecutableAction schedulableAction) {
|
||||
private static List<StateValue.Obj> encodeScheduleHistories(SchedulableExecutableAction schedulableAction) {
|
||||
return schedulableAction.getScheduleHistories().stream()
|
||||
.<StateValue>map(scheduleHistory -> {
|
||||
Map<String, StateValue> historyMap = new LinkedHashMap<>();
|
||||
historyMap.put("end_time", StateValue.str(scheduleHistory.getEndTime().toString()));
|
||||
historyMap.put("result", StateValue.str(scheduleHistory.getResult()));
|
||||
historyMap.put("history", StateValue.arr(encodeHistoryStages(scheduleHistory.getHistory())));
|
||||
.map(scheduleHistory -> {
|
||||
Map<String, Object> historyMap = new LinkedHashMap<>();
|
||||
historyMap.put("end_time", scheduleHistory.getEndTime().toString());
|
||||
historyMap.put("result", scheduleHistory.getResult());
|
||||
historyMap.put("history", encodeHistoryStages(scheduleHistory.getHistory()));
|
||||
return StateValue.obj(historyMap);
|
||||
})
|
||||
.toList();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package work.slhaf.partner.core.cognition;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.Capability;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
|
||||
+10
-11
@@ -1,4 +1,4 @@
|
||||
package work.slhaf.partner.core.cognition;
|
||||
package work.slhaf.partner.core.cognition.context;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
@@ -27,7 +27,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
@CapabilityCore(value = "cognition")
|
||||
public class CognitionCore implements StateSerializable {
|
||||
public class ContextCore implements StateSerializable {
|
||||
|
||||
private static final String RECENT_CHAT_MESSAGE_NOTES = """
|
||||
消息格式:
|
||||
@@ -58,7 +58,7 @@ public class CognitionCore implements StateSerializable {
|
||||
|
||||
private final ContextWorkspace contextWorkspace = new ContextWorkspace();
|
||||
|
||||
public CognitionCore() {
|
||||
public ContextCore() {
|
||||
register();
|
||||
}
|
||||
|
||||
@@ -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) -> {
|
||||
@@ -200,13 +200,12 @@ public class CognitionCore implements StateSerializable {
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
|
||||
List<StateValue.Obj> convertedMessageList = chatMessages.stream().map(message -> {
|
||||
Map<String, StateValue> convertedMap = Map.of(
|
||||
"role", StateValue.str(message.roleValue()),
|
||||
"content", StateValue.str(message.getContent())
|
||||
);
|
||||
return StateValue.obj(convertedMap);
|
||||
}).toList();
|
||||
List<StateValue.Obj> convertedMessageList = chatMessages.stream()
|
||||
.map(message -> StateValue.obj(Map.of(
|
||||
"role", message.roleValue(),
|
||||
"content", message.getContent()
|
||||
)))
|
||||
.toList();
|
||||
state.append("chat_messages", StateValue.arr(convertedMessageList));
|
||||
|
||||
return state;
|
||||
+30
-30
@@ -1,13 +1,11 @@
|
||||
package work.slhaf.partner.core.cognition
|
||||
package work.slhaf.partner.core.cognition.context
|
||||
|
||||
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
-1
@@ -1,4 +1,4 @@
|
||||
package work.slhaf.partner.core.cognition
|
||||
package work.slhaf.partner.core.cognition.context
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package work.slhaf.partner.core.cognition.impression
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class ActiveEntity @JvmOverloads constructor(
|
||||
val runtimeId: String = newActiveEntityRuntimeId(),
|
||||
val createdAt: Instant = Instant.now(),
|
||||
boundEntityUuid: String? = null,
|
||||
private val _evidences: MutableList<EntityEvidence> = mutableListOf(),
|
||||
) : BlockContent("active_entity_$runtimeId", "impression") {
|
||||
val evidences: List<EntityEvidence>
|
||||
get() = synchronized(_evidences) { _evidences.toList() }
|
||||
|
||||
@Volatile
|
||||
var lastMentionedAt: Instant = createdAt
|
||||
private set
|
||||
|
||||
private val _subject = AtomicReference("UNKNOWN")
|
||||
val subject: String get() = _subject.get()
|
||||
|
||||
private val _boundEntityUuid = AtomicReference<String?>(boundEntityUuid)
|
||||
val boundEntityUuid: String? get() = _boundEntityUuid.get()
|
||||
|
||||
private val _projectedFeatures: MutableMap<String, Double> = mutableMapOf()
|
||||
val projectedFeatures: Map<String, Double>
|
||||
get() = synchronized(_projectedFeatures) { _projectedFeatures.toMap() }
|
||||
|
||||
private val _projectedImpressions: MutableMap<String, Double> = mutableMapOf()
|
||||
val projectedImpressions: Map<String, Double>
|
||||
get() = synchronized(_projectedImpressions) { _projectedImpressions.toMap() }
|
||||
|
||||
@JvmOverloads
|
||||
fun addEvidence(
|
||||
content: String,
|
||||
associationConfidence: Double = 1.0,
|
||||
source: EntityEvidence.Source = EntityEvidence.Source.USER_INPUT,
|
||||
timestamp: Long = System.currentTimeMillis(),
|
||||
) = addEvidence(EntityEvidence(content, associationConfidence, source, timestamp))
|
||||
|
||||
fun addEvidence(evidence: EntityEvidence) = synchronized(_evidences) {
|
||||
_evidences.add(evidence)
|
||||
touch(Instant.ofEpochMilli(evidence.timestamp))
|
||||
}
|
||||
|
||||
fun updateSubject(subject: String) = _subject.set(subject)
|
||||
|
||||
fun bindEntity(uuid: String?) = _boundEntityUuid.set(uuid)
|
||||
|
||||
fun touch(time: Instant = Instant.now()) {
|
||||
lastMentionedAt = time
|
||||
}
|
||||
|
||||
fun addProjectedFeatures(vararg features: Pair<String, Double>) = synchronized(_projectedFeatures) {
|
||||
features.forEach { _projectedFeatures[it.first] = it.second }
|
||||
}
|
||||
|
||||
fun addProjectedImpressions(vararg impressions: Pair<String, Double>) = synchronized(_projectedImpressions) {
|
||||
impressions.forEach { _projectedImpressions[it.first] = it.second }
|
||||
}
|
||||
|
||||
override fun fillXml(document: Document, root: Element) {
|
||||
root.setAttribute("runtime_id", runtimeId)
|
||||
boundEntityUuid?.let { root.setAttribute("bound_entity_uuid", it) }
|
||||
root.setAttribute("created_at", modelTime(createdAt))
|
||||
root.setAttribute("last_mentioned_at", modelTime(lastMentionedAt))
|
||||
|
||||
appendTextElement(document, root, "subject", subject)
|
||||
|
||||
appendListElement(
|
||||
document,
|
||||
root,
|
||||
"evidences",
|
||||
"evidence",
|
||||
synchronized(_evidences) { _evidences.toList() }
|
||||
) { evidence ->
|
||||
setAttribute("association_confidence", evidence.associationConfidence.toString())
|
||||
setAttribute("source", evidence.source.name)
|
||||
setAttribute("timestamp", evidence.timestamp.toString())
|
||||
setAttribute("truncated", evidence.isContentTruncated().toString())
|
||||
setAttribute("original_length", evidence.content.length.toString())
|
||||
textContent = evidence.contentForContext()
|
||||
}
|
||||
|
||||
appendListElement(
|
||||
document,
|
||||
root,
|
||||
"projected_features",
|
||||
"feature",
|
||||
synchronized(_projectedFeatures) { _projectedFeatures.entries.toList() }
|
||||
) { entry ->
|
||||
setAttribute("confidence", entry.value.toString())
|
||||
textContent = entry.key
|
||||
}
|
||||
|
||||
appendListElement(
|
||||
document,
|
||||
root,
|
||||
"projected_impressions",
|
||||
"impression",
|
||||
synchronized(_projectedImpressions) { _projectedImpressions.entries.toList() }
|
||||
) { entry ->
|
||||
setAttribute("confidence", entry.value.toString())
|
||||
textContent = entry.key
|
||||
}
|
||||
}
|
||||
|
||||
private fun modelTime(time: Instant): String =
|
||||
time.atZone(ZoneId.systemDefault()).toString()
|
||||
}
|
||||
|
||||
private fun newActiveEntityRuntimeId(): String =
|
||||
UUID.randomUUID().toString().replace("-", "").take(12)
|
||||
@@ -0,0 +1,272 @@
|
||||
package work.slhaf.partner.core.cognition.impression
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray
|
||||
import com.alibaba.fastjson2.JSONObject
|
||||
import work.slhaf.partner.framework.agent.state.State
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable
|
||||
import work.slhaf.partner.framework.agent.state.StateValue
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class Entity @JvmOverloads constructor(
|
||||
val uuid: String = UUID.randomUUID().toString(),
|
||||
val subject: String,
|
||||
private val relations: MutableMap<String, MutableMap<String, Double>> = mutableMapOf(),
|
||||
private val impressions: MutableMap<String, IndexableData> = mutableMapOf(),
|
||||
private val features: MutableMap<String, IndexableData> = mutableMapOf()
|
||||
) : StateSerializable {
|
||||
|
||||
private val impressionLock = ReentrantLock()
|
||||
private val relationLock = ReentrantLock()
|
||||
private val featureLock = ReentrantLock()
|
||||
|
||||
@JvmOverloads
|
||||
fun updateRelation(
|
||||
target: String,
|
||||
relation: String,
|
||||
strength: Double = 1.0
|
||||
) = relationLock.withLock {
|
||||
relations.computeIfAbsent(target) { mutableMapOf() }[relation] = strength
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun updateImpression(
|
||||
impression: String,
|
||||
newImpression: String? = null,
|
||||
confidence: Double = 1.0
|
||||
): IndexableData = impressionLock.withLock {
|
||||
if (newImpression == null) {
|
||||
impressions.computeIfAbsent(impression) { IndexableData(confidence) }
|
||||
.also {
|
||||
it.confidence = confidence
|
||||
if (it.confidence >= 0.9) {
|
||||
featureLock.withLock { features[impression] = it }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
impressions.remove(impression)
|
||||
IndexableData(confidence).also {
|
||||
impressions[newImpression] = it
|
||||
if (it.confidence >= 0.9) {
|
||||
featureLock.withLock { features[newImpression] = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFeature(feature: String, newFeature: String? = null, confidence: Double = 1.0) = featureLock.withLock {
|
||||
if (newFeature == null) {
|
||||
features.computeIfAbsent(feature) { IndexableData(confidence) }
|
||||
.also { it.confidence = confidence }
|
||||
} else {
|
||||
features.remove(feature)
|
||||
IndexableData(confidence).also {
|
||||
features[newFeature] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFeature(feature: String) = featureLock.withLock {
|
||||
features.remove(feature)
|
||||
}
|
||||
|
||||
fun removeImpression(impression: String) = impressionLock.withLock {
|
||||
impressions.remove(impression)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun removeRelation(
|
||||
target: String,
|
||||
relation: String? = null
|
||||
) = relationLock.withLock {
|
||||
if (relation == null) {
|
||||
relations.remove(target)
|
||||
} else {
|
||||
relations[target]?.remove(relation)
|
||||
if (relations[target].isNullOrEmpty()) {
|
||||
relations.remove(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showRelations(): Set<RelationView> = relationLock.withLock {
|
||||
relations.map {
|
||||
RelationView(
|
||||
it.key,
|
||||
it.value.toMap()
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun showImpressions(embeddingModel: String): Set<ImpressionView> = impressionLock.withLock {
|
||||
impressions.map {
|
||||
ImpressionView(
|
||||
it.key,
|
||||
it.value.confidence,
|
||||
it.value.getVector(embeddingModel)
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun showFeatures(): Set<FeatureView> = featureLock.withLock {
|
||||
features.map {
|
||||
FeatureView(
|
||||
it.key,
|
||||
it.value.confidence
|
||||
)
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun snapshotImpressions(): Map<String, IndexableData> = impressionLock.withLock {
|
||||
impressions.toMap()
|
||||
}
|
||||
|
||||
fun snapshotFeatures(): Map<String, IndexableData> = featureLock.withLock {
|
||||
features.toMap()
|
||||
}
|
||||
|
||||
override fun statePath(): Path = Path.of("core", "impression", "entity-$uuid.json")
|
||||
|
||||
override fun load(state: JSONObject) {
|
||||
state.getJSONObject("relations")?.let { loadedRelations ->
|
||||
relationLock.withLock {
|
||||
relations.clear()
|
||||
loadedRelations.forEach { (target, relationValue) ->
|
||||
val relationObject = relationValue as? JSONObject ?: return@forEach
|
||||
val relationMap = mutableMapOf<String, Double>()
|
||||
relationObject.forEach { (relation, strengthValue) ->
|
||||
doubleValue(strengthValue)?.let { relationMap[relation] = it }
|
||||
}
|
||||
if (relationMap.isNotEmpty()) {
|
||||
relations[target] = relationMap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.getJSONObject("impressions")?.let { loadedImpressions ->
|
||||
impressionLock.withLock {
|
||||
impressions.clear()
|
||||
impressions.putAll(loadIndexableDataMap(loadedImpressions))
|
||||
}
|
||||
}
|
||||
|
||||
state.getJSONObject("features")?.let { loadedFeatures ->
|
||||
featureLock.withLock {
|
||||
features.clear()
|
||||
features.putAll(loadIndexableDataMap(loadedFeatures))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun convert(): State {
|
||||
val state = State()
|
||||
state.append("uuid", StateValue.str(uuid))
|
||||
state.append("subject", StateValue.str(subject))
|
||||
|
||||
val relationState = relationLock.withLock {
|
||||
relations.mapValues { (_, relationMap) -> relationMap.toMap() }
|
||||
}
|
||||
state.append("relations", StateValue.obj(relationState))
|
||||
|
||||
val impressionState = impressionLock.withLock {
|
||||
indexableDataState(impressions)
|
||||
}
|
||||
state.append("impressions", StateValue.obj(impressionState))
|
||||
|
||||
val featureState = featureLock.withLock {
|
||||
indexableDataState(features)
|
||||
}
|
||||
state.append("features", StateValue.obj(featureState))
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
override fun autoLoadOnRegister(): Boolean = false
|
||||
|
||||
private fun loadIndexableDataMap(state: JSONObject): Map<String, IndexableData> {
|
||||
val loaded = mutableMapOf<String, IndexableData>()
|
||||
state.forEach { (key, value) ->
|
||||
val data = when (value) {
|
||||
is JSONObject -> loadIndexableData(value)
|
||||
else -> IndexableData(doubleValue(value) ?: return@forEach)
|
||||
}
|
||||
loaded[key] = data
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
private fun loadIndexableData(state: JSONObject): IndexableData {
|
||||
val data = IndexableData(state.getDouble("confidence") ?: 1.0)
|
||||
state.getJSONObject("vectors")?.forEach { (embeddingModel, vectorValue) ->
|
||||
val vectorArray = vectorValue as? JSONArray ?: return@forEach
|
||||
val vector = FloatArray(vectorArray.size)
|
||||
for (index in vectorArray.indices) {
|
||||
vector[index] = floatValue(vectorArray[index]) ?: return@forEach
|
||||
}
|
||||
data.updateVector(embeddingModel, vector)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private fun indexableDataState(source: Map<String, IndexableData>): Map<String, Map<String, Any>> =
|
||||
source.mapValues { (_, data) ->
|
||||
mapOf(
|
||||
"confidence" to data.confidence,
|
||||
"vectors" to data.snapshotVectors().mapValues { (_, vector) -> vector.toList() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun doubleValue(value: Any?): Double? = when (value) {
|
||||
is Number -> value.toDouble()
|
||||
is String -> value.toDoubleOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun floatValue(value: Any?): Float? = when (value) {
|
||||
is Number -> value.toFloat()
|
||||
is String -> value.toFloatOrNull()
|
||||
else -> null
|
||||
}
|
||||
|
||||
data class IndexableData(
|
||||
var confidence: Double
|
||||
) {
|
||||
private val vectors: ConcurrentHashMap<String, FloatArray> = ConcurrentHashMap()
|
||||
|
||||
fun updateVector(
|
||||
embeddingModel: String,
|
||||
vector: FloatArray
|
||||
) {
|
||||
vectors[embeddingModel] = vector.copyOf()
|
||||
}
|
||||
|
||||
fun getVector(embeddingModel: String): FloatArray? {
|
||||
return vectors[embeddingModel]?.copyOf()
|
||||
}
|
||||
|
||||
fun snapshotVectors(): Map<String, FloatArray> {
|
||||
return vectors.mapValues { (_, vector) -> vector.copyOf() }
|
||||
}
|
||||
}
|
||||
|
||||
data class RelationView(
|
||||
val target: String,
|
||||
val relations: Map<String, Double>
|
||||
)
|
||||
|
||||
data class FeatureView(
|
||||
val feature: String,
|
||||
val confidence: Double
|
||||
)
|
||||
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class ImpressionView(
|
||||
val impression: String,
|
||||
val confidence: Double,
|
||||
val vector: FloatArray?
|
||||
)
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package work.slhaf.partner.core.cognition.impression
|
||||
|
||||
/**
|
||||
* Runtime evidence associated with an active entity.
|
||||
*
|
||||
* The confidence describes how strongly this evidence is associated with the
|
||||
* current active entity, not whether the evidence content itself is true.
|
||||
*/
|
||||
data class EntityEvidence @JvmOverloads constructor(
|
||||
val content: String,
|
||||
val associationConfidence: Double = 1.0,
|
||||
val source: Source = Source.USER_INPUT,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
) {
|
||||
enum class Source {
|
||||
USER_INPUT,
|
||||
ASSISTANT_REPLY
|
||||
}
|
||||
|
||||
fun isContentTruncated(maxLength: Int = CONTEXT_CONTENT_MAX_LENGTH): Boolean =
|
||||
content.length > maxLength
|
||||
|
||||
fun contentForContext(maxLength: Int = CONTEXT_CONTENT_MAX_LENGTH): String {
|
||||
if (content.length <= maxLength) {
|
||||
return content
|
||||
}
|
||||
|
||||
val available = maxLength - OMITTED_MARKER.length
|
||||
if (available <= 0) {
|
||||
return content.take(maxLength)
|
||||
}
|
||||
|
||||
val headBudget = available / 2
|
||||
val tailBudget = available - headBudget
|
||||
val headEnd = adjustHeadEnd(content, headBudget)
|
||||
val tailStart = adjustTailStart(content, content.length - tailBudget)
|
||||
|
||||
if (tailStart <= headEnd) {
|
||||
return content.take(maxLength).trimEnd()
|
||||
}
|
||||
|
||||
return content.substring(0, headEnd).trimEnd() +
|
||||
OMITTED_MARKER +
|
||||
content.substring(tailStart).trimStart()
|
||||
}
|
||||
|
||||
private fun adjustHeadEnd(source: String, preferredEnd: Int): Int {
|
||||
val safePreferredEnd = preferredEnd.coerceIn(0, source.length)
|
||||
findForwardBoundary(source, safePreferredEnd, STRONG_BOUNDARY_SEARCH_WINDOW, ::isStrongBoundary)?.let {
|
||||
return it + 1
|
||||
}
|
||||
findForwardBoundary(source, safePreferredEnd, SOFT_BOUNDARY_SEARCH_WINDOW, ::isSoftBoundary)?.let {
|
||||
return it + 1
|
||||
}
|
||||
return safePreferredEnd
|
||||
}
|
||||
|
||||
private fun adjustTailStart(source: String, preferredStart: Int): Int {
|
||||
val safePreferredStart = preferredStart.coerceIn(0, source.length)
|
||||
findBackwardBoundary(source, safePreferredStart, STRONG_BOUNDARY_SEARCH_WINDOW, ::isStrongBoundary)?.let {
|
||||
return it
|
||||
}
|
||||
findBackwardBoundary(source, safePreferredStart, SOFT_BOUNDARY_SEARCH_WINDOW, ::isSoftBoundary)?.let {
|
||||
return it
|
||||
}
|
||||
return safePreferredStart
|
||||
}
|
||||
|
||||
private fun findForwardBoundary(
|
||||
source: String,
|
||||
start: Int,
|
||||
window: Int,
|
||||
predicate: (Char) -> Boolean,
|
||||
): Int? {
|
||||
val end = (start + window).coerceAtMost(source.length)
|
||||
for (index in start until end) {
|
||||
if (predicate(source[index])) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findBackwardBoundary(
|
||||
source: String,
|
||||
start: Int,
|
||||
window: Int,
|
||||
predicate: (Char) -> Boolean,
|
||||
): Int? {
|
||||
val end = (start - window).coerceAtLeast(0)
|
||||
for (index in start downTo end + 1) {
|
||||
if (predicate(source[index - 1])) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isStrongBoundary(char: Char): Boolean = char == '\n'
|
||||
|
||||
private fun isSoftBoundary(char: Char): Boolean = when (char) {
|
||||
'。', '!', '?', ';', ';', '.' -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CONTEXT_CONTENT_MAX_LENGTH = 480
|
||||
private const val OMITTED_MARKER = "\n...[omitted]...\n"
|
||||
private const val STRONG_BOUNDARY_SEARCH_WINDOW = 120
|
||||
private const val SOFT_BOUNDARY_SEARCH_WINDOW = 80
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package work.slhaf.partner.core.cognition.impression;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@CapabilityCore(value = "cognition")
|
||||
public class ImpressionCore implements StateSerializable {
|
||||
|
||||
/**
|
||||
* Keyed by entity uuid. Subject can be revised or merged later, so it should not be used as the stable key.
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Entity> knownEntitiesByUuid = new ConcurrentHashMap<>();
|
||||
private final ImpressionVectorIndex vectorIndex = new ImpressionVectorIndex();
|
||||
|
||||
@CapabilityMethod
|
||||
public void updateRelation() {
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void updateImpression() {
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void showImpressions() {
|
||||
}
|
||||
|
||||
@CapabilityMethod
|
||||
public void projectEntity(Set<ActiveEntity> activeEntities) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("core", "impression.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(@NotNull JSONObject state) {
|
||||
JSONArray entityArray = state.getJSONArray("entities");
|
||||
if (entityArray == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
knownEntitiesByUuid.clear();
|
||||
for (int i = 0; i < entityArray.size(); i++) {
|
||||
JSONObject entityObject = entityArray.getJSONObject(i);
|
||||
if (entityObject == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String uuid = entityObject.getString("uuid");
|
||||
String subject = entityObject.getString("subject");
|
||||
if (uuid == null || uuid.isBlank() || subject == null || subject.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Entity entity = new Entity(uuid, subject);
|
||||
entity.load();
|
||||
vectorIndex.sync(entity);
|
||||
knownEntitiesByUuid.put(uuid, entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @NotNull State convert() {
|
||||
State state = new State();
|
||||
|
||||
List<StateValue.Obj> entities = knownEntitiesByUuid.values().stream()
|
||||
.map(entity -> StateValue.obj(Map.of(
|
||||
"uuid", entity.getUuid(),
|
||||
"subject", entity.getSubject()
|
||||
)))
|
||||
.toList();
|
||||
|
||||
state.append("entities", StateValue.arr(entities));
|
||||
return state;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package work.slhaf.partner.core.cognition.impression;
|
||||
|
||||
import work.slhaf.partner.common.vector.VectorClient;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ImpressionVectorIndex {
|
||||
|
||||
private final Executor executor = Executors.newFixedThreadPool(2, r -> {
|
||||
Thread thread = new Thread(r, "impression-vector-index");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
public void sync(Entity entity) {
|
||||
if (!VectorClient.status){
|
||||
return;
|
||||
}
|
||||
entity.snapshotFeatures().forEach(this::upsert);
|
||||
entity.snapshotImpressions().forEach(this::upsert);
|
||||
}
|
||||
|
||||
public void upsert(String text, Entity.IndexableData indexableData){
|
||||
if (!VectorClient.status){
|
||||
return;
|
||||
}
|
||||
String modelId = VectorClient.VECTOR_MODEL_ID;
|
||||
if (indexableData.getVector(modelId) != null) {
|
||||
return;
|
||||
}
|
||||
executor.execute(() -> {
|
||||
float[] vector = VectorClient.INSTANCE.compute(text);
|
||||
indexableData.updateVector(modelId,vector);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class EntityAssociationMatch(
|
||||
val target: ImpressionSearchTarget,
|
||||
val score: Double,
|
||||
val hits: List<ImpressionSearchHit> = emptyList(),
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class ImpressionSearchDocument(
|
||||
val id: String,
|
||||
val target: ImpressionSearchTarget,
|
||||
val field: ImpressionSearchField,
|
||||
val text: String,
|
||||
val weight: Double = 1.0,
|
||||
val metadata: Map<String, String> = emptyMap(),
|
||||
)
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
import work.slhaf.partner.core.cognition.impression.ActiveEntity
|
||||
import work.slhaf.partner.core.cognition.impression.Entity
|
||||
|
||||
object ImpressionSearchDocuments {
|
||||
|
||||
fun fromActiveEntity(activeEntity: ActiveEntity): List<ImpressionSearchDocument> {
|
||||
val target = ImpressionSearchTarget(
|
||||
ImpressionSearchTarget.Type.ACTIVE_ENTITY,
|
||||
activeEntity.runtimeId
|
||||
)
|
||||
val metadata = activeEntity.boundEntityUuid
|
||||
?.let { mapOf("boundEntityUuid" to it) }
|
||||
.orEmpty()
|
||||
|
||||
return buildList {
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:subject",
|
||||
target = target,
|
||||
field = ImpressionSearchField.SUBJECT,
|
||||
text = activeEntity.subject,
|
||||
weight = SUBJECT_WEIGHT,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
|
||||
activeEntity.evidences.forEachIndexed { index, evidence ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:evidence:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.EVIDENCE,
|
||||
text = evidence.contentForContext(),
|
||||
weight = EVIDENCE_WEIGHT * evidence.associationConfidence,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
activeEntity.projectedFeatures.entries.forEachIndexed { index, entry ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:feature:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.FEATURE,
|
||||
text = entry.key,
|
||||
weight = FEATURE_WEIGHT * entry.value,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
activeEntity.projectedImpressions.entries.forEachIndexed { index, entry ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "active:${activeEntity.runtimeId}:impression:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.IMPRESSION,
|
||||
text = entry.key,
|
||||
weight = IMPRESSION_WEIGHT * entry.value,
|
||||
metadata = metadata,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fromEntity(entity: Entity): List<ImpressionSearchDocument> {
|
||||
val target = ImpressionSearchTarget(
|
||||
ImpressionSearchTarget.Type.ENTITY,
|
||||
entity.uuid
|
||||
)
|
||||
|
||||
return buildList {
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:subject",
|
||||
target = target,
|
||||
field = ImpressionSearchField.SUBJECT,
|
||||
text = entity.subject,
|
||||
weight = SUBJECT_WEIGHT,
|
||||
)
|
||||
)
|
||||
|
||||
entity.snapshotFeatures().keys.forEachIndexed { index, feature ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:feature:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.FEATURE,
|
||||
text = feature,
|
||||
weight = FEATURE_WEIGHT,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
entity.snapshotImpressions().keys.forEachIndexed { index, impression ->
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:impression:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.IMPRESSION,
|
||||
text = impression,
|
||||
weight = IMPRESSION_WEIGHT,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
entity.showRelations().forEachIndexed { index, relation ->
|
||||
val relationText = buildString {
|
||||
append(relation.target)
|
||||
relation.relations.keys.forEach { name ->
|
||||
append(' ')
|
||||
append(name)
|
||||
}
|
||||
}
|
||||
|
||||
add(
|
||||
ImpressionSearchDocument(
|
||||
id = "entity:${entity.uuid}:relation:$index",
|
||||
target = target,
|
||||
field = ImpressionSearchField.RELATION,
|
||||
text = relationText,
|
||||
weight = RELATION_WEIGHT,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val SUBJECT_WEIGHT = 1.0
|
||||
private const val FEATURE_WEIGHT = 0.85
|
||||
private const val IMPRESSION_WEIGHT = 0.75
|
||||
private const val RELATION_WEIGHT = 0.65
|
||||
private const val EVIDENCE_WEIGHT = 0.8
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
enum class ImpressionSearchField {
|
||||
SUBJECT,
|
||||
FEATURE,
|
||||
IMPRESSION,
|
||||
RELATION,
|
||||
EVIDENCE
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class ImpressionSearchHit(
|
||||
val document: ImpressionSearchDocument,
|
||||
val score: Double,
|
||||
val matchedTerms: Set<String> = emptySet(),
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
data class ImpressionSearchTarget(
|
||||
val type: Type,
|
||||
val id: String,
|
||||
) {
|
||||
enum class Type {
|
||||
ACTIVE_ENTITY,
|
||||
ENTITY
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package work.slhaf.partner.core.cognition.impression.search
|
||||
|
||||
interface ImpressionTextSearch {
|
||||
|
||||
fun rebuild(documents: Collection<ImpressionSearchDocument>)
|
||||
|
||||
fun upsert(document: ImpressionSearchDocument)
|
||||
|
||||
fun removeByTarget(target: ImpressionSearchTarget)
|
||||
|
||||
fun search(
|
||||
query: String,
|
||||
limit: Int = 20,
|
||||
): List<ImpressionSearchHit>
|
||||
}
|
||||
@@ -175,8 +175,7 @@ public class MemoryCore implements StateSerializable {
|
||||
State state = new State();
|
||||
state.append("memory_session_id", StateValue.str(memorySessionId));
|
||||
|
||||
List<StateValue.Str> unitOverview = memoryUnits.keySet().stream()
|
||||
.map(StateValue::str)
|
||||
List<String> unitOverview = memoryUnits.keySet().stream()
|
||||
.toList();
|
||||
state.append("memory_unit_uuid_set", StateValue.arr(unitOverview));
|
||||
return state;
|
||||
|
||||
@@ -95,25 +95,23 @@ public class MemoryUnit implements StateSerializable {
|
||||
state.append("id", StateValue.str(id));
|
||||
state.append("update_timestamp", StateValue.num(timestamp));
|
||||
|
||||
List<StateValue.Obj> convertedMessageList = conversationMessages.stream().map(message -> {
|
||||
Map<String, StateValue> convertedMap = Map.of(
|
||||
"role", StateValue.str(message.roleValue()),
|
||||
"content", StateValue.str(message.getContent())
|
||||
);
|
||||
return StateValue.obj(convertedMap);
|
||||
}).toList();
|
||||
List<StateValue.Obj> convertedMessageList = conversationMessages.stream()
|
||||
.map(message -> StateValue.obj(Map.of(
|
||||
"role", message.roleValue(),
|
||||
"content", message.getContent()
|
||||
)))
|
||||
.toList();
|
||||
state.append("conversation_messages", StateValue.arr(convertedMessageList));
|
||||
|
||||
List<StateValue.Obj> convertedSliceList = slices.stream().map(slice -> {
|
||||
Map<String, StateValue> convertedMap = Map.of(
|
||||
"id", StateValue.str(slice.getId()),
|
||||
"start_index", StateValue.num(slice.getStartIndex()),
|
||||
"end_index", StateValue.num(slice.getEndIndex()),
|
||||
"summary", StateValue.str(slice.getSummary()),
|
||||
"created_timestamp", StateValue.num(slice.getTimestamp())
|
||||
);
|
||||
return StateValue.obj(convertedMap);
|
||||
}).toList();
|
||||
List<StateValue.Obj> convertedSliceList = slices.stream()
|
||||
.map(slice -> StateValue.obj(Map.of(
|
||||
"id", slice.getId(),
|
||||
"start_index", slice.getStartIndex(),
|
||||
"end_index", slice.getEndIndex(),
|
||||
"summary", slice.getSummary(),
|
||||
"created_timestamp", slice.getTimestamp()
|
||||
)))
|
||||
.toList();
|
||||
state.append("memory_slices", StateValue.arr(convertedSliceList));
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+17
-9
@@ -6,14 +6,16 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
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(),
|
||||
|
||||
+36
-24
@@ -12,11 +12,17 @@ import work.slhaf.partner.core.action.entity.MetaAction;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.action.entity.intervention.InterventionType;
|
||||
import work.slhaf.partner.core.action.entity.intervention.MetaIntervention;
|
||||
import work.slhaf.partner.core.cognition.*;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.CommunicationBlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||
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 +41,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 +69,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 +84,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 +114,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 +160,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 +181,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 +203,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 +250,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 +295,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 +337,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();
|
||||
};
|
||||
|
||||
+19
-10
@@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
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.model.ActivateModel;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+29
-4
@@ -4,8 +4,9 @@ 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.core.cognition.context.ContextBlock;
|
||||
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.model.ActivateModel;
|
||||
@@ -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())
|
||||
|
||||
+25
-5
@@ -6,9 +6,10 @@ import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
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.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.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"));
|
||||
}
|
||||
|
||||
+6
-2
@@ -6,7 +6,7 @@ import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
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.model.ActivateModel;
|
||||
@@ -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;
|
||||
|
||||
+83
-131
@@ -8,10 +8,9 @@ import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
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.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
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;
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+167
-43
@@ -6,10 +6,10 @@ 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;
|
||||
import work.slhaf.partner.core.cognition.ResolvedContext;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ResolvedContext;
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+43
-9
@@ -2,7 +2,7 @@ package work.slhaf.partner.module.action.planner.extractor;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentRuntimeException;
|
||||
import work.slhaf.partner.framework.agent.exception.ModuleExecutionException;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
@@ -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.context.CommunicationBlockContent
|
||||
import work.slhaf.partner.core.cognition.context.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-7
@@ -4,7 +4,12 @@ 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.cognition.*;
|
||||
import org.w3c.dom.Node;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.CommunicationBlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ResolvedContext;
|
||||
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;
|
||||
@@ -29,7 +34,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 +41,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 +60,8 @@ public class CommunicationProducer extends AbstractAgentModule.Running<PartnerRu
|
||||
|
||||
输出契约:
|
||||
- 默认情况下,直接输出要发送给用户的最终回复正文,不要添加额外标签、解释或前后缀。
|
||||
- 对话轨迹中的身份行、状态行、调试行和传输边界行都属于系统内部序列化标记,只用于理解上下文,不属于可复用正文。
|
||||
- 不得在输出中复现、仿写或携带任何历史序列化标记、调试前缀或传输边界标记;即使用户正在讨论这些前缀,也只能用自然语言确认或说明。
|
||||
- 若当前情境下不应直接向用户发出回复,但仍需要留下本轮交流结果供系统后续保留在交流轨迹中,则输出以 NO_REPLY 开头。
|
||||
- 使用 NO_REPLY 时,格式为:
|
||||
|
||||
@@ -87,9 +97,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 +180,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 +251,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";
|
||||
|
||||
+6
-4
@@ -9,9 +9,9 @@ import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.action.entity.Schedulable;
|
||||
import work.slhaf.partner.core.action.entity.StateAction;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
@@ -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) {
|
||||
|
||||
+13
-15
@@ -69,8 +69,8 @@ final class MemoryRuntimeStateCodec {
|
||||
List<StateValue.Obj> dateIndexStates = dateIndex.entries().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> StateValue.obj(Map.of(
|
||||
"date", StateValue.str(entry.getKey().toString()),
|
||||
"refs", StateValue.arr(encodeSliceRefs(entry.getValue()))
|
||||
"date", entry.getKey().toString(),
|
||||
"refs", encodeSliceRefs(entry.getValue())
|
||||
)))
|
||||
.toList();
|
||||
state.append("date_index", StateValue.arr(dateIndexStates));
|
||||
@@ -82,8 +82,8 @@ final class MemoryRuntimeStateCodec {
|
||||
TopicMemoryIndex.TopicTreeNode topicNode,
|
||||
List<StateValue.Obj> topicStates) {
|
||||
topicStates.add(StateValue.obj(Map.of(
|
||||
"topic_path", StateValue.str(path),
|
||||
"bindings", StateValue.arr(encodeTopicBindings(topicNode.bindings()))
|
||||
"topic_path", path,
|
||||
"bindings", encodeTopicBindings(topicNode.bindings())
|
||||
)));
|
||||
for (Map.Entry<String, TopicMemoryIndex.TopicTreeNode> childEntry : topicNode.children().entrySet()) {
|
||||
collectTopicStates(path + "->" + childEntry.getKey(), childEntry.getValue(), topicStates);
|
||||
@@ -93,18 +93,16 @@ final class MemoryRuntimeStateCodec {
|
||||
private List<StateValue> encodeTopicBindings(List<TopicMemoryIndex.TopicBinding> bindings) {
|
||||
return bindings.stream()
|
||||
.map(binding -> (StateValue) StateValue.obj(Map.of(
|
||||
"unit_id", StateValue.str(binding.sliceRef().getUnitId()),
|
||||
"slice_id", StateValue.str(binding.sliceRef().getSliceId()),
|
||||
"timestamp", StateValue.num(binding.timestamp()),
|
||||
"unit_id", binding.sliceRef().getUnitId(),
|
||||
"slice_id", binding.sliceRef().getSliceId(),
|
||||
"timestamp", binding.timestamp(),
|
||||
"activation_profile", StateValue.obj(Map.of(
|
||||
"activation_weight", StateValue.num(binding.activationProfile().getActivationWeight()),
|
||||
"diffusion_weight", StateValue.num(binding.activationProfile().getDiffusionWeight()),
|
||||
"activation_weight", binding.activationProfile().getActivationWeight(),
|
||||
"diffusion_weight", binding.activationProfile().getDiffusionWeight(),
|
||||
"context_independence_weight",
|
||||
StateValue.num(binding.activationProfile().getContextIndependenceWeight())
|
||||
binding.activationProfile().getContextIndependenceWeight()
|
||||
)),
|
||||
"related_topic_paths", StateValue.arr(binding.relatedTopicPaths().stream()
|
||||
.map(StateValue::str)
|
||||
.toList())
|
||||
"related_topic_paths", binding.relatedTopicPaths()
|
||||
)))
|
||||
.toList();
|
||||
}
|
||||
@@ -156,8 +154,8 @@ final class MemoryRuntimeStateCodec {
|
||||
private List<StateValue> encodeSliceRefs(List<SliceRef> refs) {
|
||||
return refs.stream()
|
||||
.map(ref -> (StateValue) StateValue.obj(Map.of(
|
||||
"unit_id", StateValue.str(ref.getUnitId()),
|
||||
"slice_id", StateValue.str(ref.getSliceId())
|
||||
"unit_id", ref.getUnitId(),
|
||||
"slice_id", ref.getSliceId()
|
||||
)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
+118
-7
@@ -5,9 +5,11 @@ 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.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.action.ActionCapability;
|
||||
import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
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.InjectModule;
|
||||
@@ -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 ->
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ 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.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
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;
|
||||
|
||||
+52
-23
@@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
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.InjectModule;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package work.slhaf.partner.module.perceive;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
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.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.CommunicationBlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.perceive.PerceiveCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@ import kotlin.Unit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@ package work.slhaf.partner.runtime.exception;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.exception.AgentException;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReport;
|
||||
import work.slhaf.partner.framework.agent.exception.ExceptionReporter;
|
||||
|
||||
+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;
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package work.slhaf.partner.core.cognition;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock;
|
||||
import work.slhaf.partner.core.cognition.context.ContextCore;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@@ -20,15 +22,15 @@ class CognitionCoreTest {
|
||||
|
||||
@Test
|
||||
void shouldRenderRecentChatMessagesWithWrapperAndNotes() {
|
||||
CognitionCore cognitionCore = new CognitionCore();
|
||||
cognitionCore.getChatMessages().addAll(List.of(
|
||||
ContextCore contextCore = new ContextCore();
|
||||
contextCore.getChatMessages().addAll(List.of(
|
||||
new Message(Message.Character.USER, "[[USER]: user-1]: hello"),
|
||||
new Message(Message.Character.ASSISTANT, "[NOT_REPLIED]: wait"),
|
||||
new Message(Message.Character.ASSISTANT, "latest message")
|
||||
));
|
||||
|
||||
cognitionCore.refreshRecentChatMessagesContext();
|
||||
String content = cognitionCore.contextWorkspace()
|
||||
contextCore.refreshRecentChatMessagesContext();
|
||||
String content = contextCore.contextWorkspace()
|
||||
.resolve(List.of(ContextBlock.FocusedDomain.COMMUNICATION))
|
||||
.encodeToMessage()
|
||||
.getContent();
|
||||
|
||||
@@ -2,6 +2,9 @@ package work.slhaf.partner.core.cognition
|
||||
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import work.slhaf.partner.core.cognition.context.BlockContent
|
||||
import work.slhaf.partner.core.cognition.context.ContextBlock
|
||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace
|
||||
|
||||
class ContextWorkspaceTest {
|
||||
|
||||
|
||||
+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");
|
||||
});
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import work.slhaf.partner.core.action.ActionCore;
|
||||
import work.slhaf.partner.core.action.entity.*;
|
||||
import work.slhaf.partner.core.action.runner.RunnerClient;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextWorkspace;
|
||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.action.executor.entity.ExtractorResult;
|
||||
import work.slhaf.partner.module.action.executor.entity.HistoryAction;
|
||||
|
||||
+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));
|
||||
}
|
||||
}
|
||||
+34
-2
@@ -3,7 +3,7 @@ package work.slhaf.partner.module.communication;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextWorkspace;
|
||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.runtime.PartnerRunningFlowContext;
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.context.ContextWorkspace;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
import work.slhaf.partner.core.memory.pojo.MemorySlice;
|
||||
import work.slhaf.partner.core.memory.pojo.MemoryUnit;
|
||||
@@ -65,8 +66,8 @@ class MemoryRuntimeTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public work.slhaf.partner.core.cognition.ContextWorkspace contextWorkspace() {
|
||||
return new work.slhaf.partner.core.cognition.ContextWorkspace();
|
||||
public ContextWorkspace contextWorkspace() {
|
||||
return new ContextWorkspace();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user