Compare commits
83
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84b4ca120f | ||
|
|
a461e9799a | ||
|
|
9624f0e18d | ||
|
|
2a3b080e29 | ||
|
|
00fc83c081 | ||
|
|
0da90fdd7a | ||
|
|
924aa398b6 | ||
|
|
4a34c8e672 | ||
|
|
ebf95506cd | ||
|
|
66c17e821f | ||
|
|
86ad8e7b5e | ||
|
|
e8984b055c | ||
|
|
0d70da4f31 | ||
|
|
fd2ebce8e1 | ||
|
|
1c16869126 | ||
|
|
a0e64224a7 | ||
|
|
5a465b145c | ||
|
|
687bdb21e0 | ||
|
|
ab79cf33bc | ||
|
|
25a4787022 | ||
|
|
db71919207 | ||
|
|
9b9e99da3e | ||
|
|
fd3d2787c3 | ||
|
|
f69295db6a | ||
|
|
5ef14ad5ec | ||
|
|
978a33377c | ||
|
|
6a87833c7f | ||
|
|
121cad1ad5 | ||
|
|
c7fcd3c6f9 | ||
|
|
ca96769736 | ||
|
|
6360466a28 | ||
|
|
71f3bfae65 | ||
|
|
b14d124049 | ||
|
|
00f1dad5df | ||
|
|
cb94314bcc | ||
|
|
75a01a9444 | ||
|
|
e00762bb0b | ||
|
|
b0313ae818 | ||
|
|
cb8a3fe12a | ||
|
|
00d40e3bbe | ||
|
|
9ed6c2638d | ||
|
|
b2cebe1ba7 | ||
|
|
d8fe3f5a3c | ||
|
|
87ceea3d30 | ||
|
|
8baf16a9d3 | ||
|
|
b77c7b5f56 | ||
|
|
9315fa6d32 | ||
|
|
e3f3b35047 | ||
|
|
37d7aea5a7 | ||
|
|
b1e3136350 | ||
|
|
927835cb0e | ||
|
|
60f9ca2b1d | ||
|
|
59a1eeed85 | ||
|
|
90fcd6a9df | ||
|
|
e3a5e571bf | ||
|
|
02a623712e | ||
|
|
78714e237f | ||
|
|
3c54f07ebb | ||
|
|
de3fd45271 | ||
|
|
b3e865e96f | ||
|
|
f39df91aaf | ||
|
|
03d06792db | ||
|
|
bd5ec944c7 | ||
|
|
ac382af545 | ||
|
|
f062f087c1 | ||
|
|
93226d69fa | ||
|
|
43e06fbd4e | ||
|
|
7e4a86e158 | ||
|
|
a3da04f406 | ||
|
|
c5c07cdc73 | ||
|
|
60ec02bcfb | ||
|
|
b2f0c129a2 | ||
|
|
38c2af9f39 | ||
|
|
5530e9154b | ||
|
|
8765d2d6b6 | ||
|
|
03c8ae6168 | ||
|
|
25e0431e92 | ||
|
|
d74644d197 | ||
|
|
7d4a3e2230 | ||
|
|
69c717a851 | ||
|
|
b863a92ade | ||
|
|
fc29a7db53 | ||
|
|
002fe98e17 |
@@ -1,11 +0,0 @@
|
|||||||
.git
|
|
||||||
.gitignore
|
|
||||||
*.md
|
|
||||||
.env.local
|
|
||||||
.env
|
|
||||||
tests/
|
|
||||||
*.test.go
|
|
||||||
coverage/
|
|
||||||
.DS_Store
|
|
||||||
k8s/
|
|
||||||
migrations/
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Workflows Backend Configuration
|
||||||
|
|
||||||
|
# Database (memory-db CNPG in K8s)
|
||||||
|
# Option A: Direct DATABASE_URL
|
||||||
|
DATABASE_URL=postgresql://app:[email protected]:5432/memory?sslmode=disable
|
||||||
|
|
||||||
|
# Option B: Individual env vars (used if DATABASE_URL is empty)
|
||||||
|
DATABASE_HOST=memory-db-rw.poimen.svc.cluster.local
|
||||||
|
DATABASE_PORT=5432
|
||||||
|
DATABASE_NAME=memory
|
||||||
|
DATABASE_USER=app
|
||||||
|
DATABASE_PASSWORD=PASSWORD
|
||||||
|
|
||||||
|
# Temporal
|
||||||
|
TEMPORAL_HOST_PORT=localhost:7233
|
||||||
|
TEMPORAL_NAMESPACE=default
|
||||||
|
|
||||||
|
# API Server
|
||||||
|
API_PORT=8080
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
VERBOSE=false
|
||||||
+12
-36
@@ -1,4 +1,4 @@
|
|||||||
name: CI
|
name: ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,18 +6,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-build-push:
|
test:
|
||||||
runs-on: golang
|
runs-on: golang
|
||||||
container:
|
container:
|
||||||
image: golang:1.26
|
image: golang:1.25
|
||||||
env:
|
env:
|
||||||
GOPRIVATE: forgejo.riotpiao.com
|
GOPRIVATE: forgejo.riotpiao.com
|
||||||
REGISTRY: forgejo.riotpiao.com
|
GOFLAGS: -mod=readonly
|
||||||
IMAGE: forgejo.riotpiao.com/rock/poimen-workflows
|
GITHUB_TOKEN: ${{ secrets.REGISTRY_PAT }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Node.js and Docker for actions runtime
|
|
||||||
run: apt-get update && apt-get install -y nodejs docker.io
|
|
||||||
|
|
||||||
- name: Configure git authentication
|
- name: Configure git authentication
|
||||||
run: |
|
run: |
|
||||||
git config --global url."https://oauth2:${{ secrets.REGISTRY_PAT }}@forgejo.riotpiao.com".insteadOf "https://forgejo.riotpiao.com"
|
git config --global url."https://oauth2:${{ secrets.REGISTRY_PAT }}@forgejo.riotpiao.com".insteadOf "https://forgejo.riotpiao.com"
|
||||||
@@ -28,38 +25,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git init
|
git init
|
||||||
git remote add origin https://forgejo.riotpiao.com/rock/poimen-workflows.git
|
git remote add origin https://forgejo.riotpiao.com/rock/poimen-workflows.git
|
||||||
git fetch origin ${{ github.head_ref || github.ref_name }} --depth=1
|
git fetch origin ${{ github.ref_name }} --depth=1
|
||||||
git checkout FETCH_HEAD
|
git checkout FETCH_HEAD
|
||||||
|
|
||||||
- name: Download dependencies
|
- name: Download dependencies
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -o /tmp/poimen-bin/ ./cmd/...
|
||||||
|
|
||||||
- name: Vet
|
- name: Vet
|
||||||
run: go vet ./...
|
run: go vet ./...
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: go test ./...
|
|
||||||
|
|
||||||
- name: Build binary
|
|
||||||
run: CGO_ENABLED=0 GOOS=linux go build -o /tmp/poimen-worker ./cmd/worker
|
|
||||||
|
|
||||||
- name: Get short SHA
|
|
||||||
id: sha
|
|
||||||
run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Registry login
|
|
||||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.FORGEJO_REGISTRY_TOKEN }}" | docker login "${REGISTRY}" \
|
|
||||||
--username "${{ secrets.FORGEJO_REGISTRY_USER }}" --password-stdin
|
|
||||||
|
|
||||||
- name: Build and push image
|
|
||||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
||||||
run: |
|
|
||||||
docker build \
|
|
||||||
-t "${IMAGE}:${{ steps.sha.outputs.short_sha }}" \
|
|
||||||
-t "${IMAGE}:latest" \
|
|
||||||
.
|
|
||||||
docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}"
|
|
||||||
docker push "${IMAGE}:latest"
|
|
||||||
echo "✓ Pushed ${IMAGE}:${{ steps.sha.outputs.short_sha }}"
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
creation_rules:
|
|
||||||
- path_regex: k8s/.*\.enc\.ya?ml
|
|
||||||
age: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
|
||||||
-293
@@ -1,293 +0,0 @@
|
|||||||
# Poimen Application Deployment
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Poimen is a unified application consisting of three services:
|
|
||||||
- **poimen-memory**: Memory/Graph RAG service
|
|
||||||
- **poimen-workflows**: Temporal orchestration + API
|
|
||||||
- **poimen-frontend**: Next.js frontend
|
|
||||||
|
|
||||||
All services are deployed together as a single application in the `poimen` namespace.
|
|
||||||
|
|
||||||
## Local Development
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Docker
|
|
||||||
- Docker Compose
|
|
||||||
- Node.js 18+
|
|
||||||
- Go 1.21+
|
|
||||||
- Python 3.11+
|
|
||||||
|
|
||||||
### Start Local Stack
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This starts:
|
|
||||||
- PostgreSQL (memory + workflows DBs)
|
|
||||||
- Redis (cache)
|
|
||||||
- Temporal (workflow orchestration)
|
|
||||||
- poimen-memory (8000)
|
|
||||||
- poimen-workflows (8080)
|
|
||||||
- poimen-workflows-worker
|
|
||||||
- poimen-frontend (3000)
|
|
||||||
|
|
||||||
### Access Services
|
|
||||||
|
|
||||||
- Frontend: http://localhost:3000
|
|
||||||
- Workflows API: http://localhost:8080
|
|
||||||
- Memory API: http://localhost:8000
|
|
||||||
- Temporal UI: http://localhost:8233
|
|
||||||
|
|
||||||
### Stop Stack
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building & Pushing Images
|
|
||||||
|
|
||||||
### Build All Services
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build-push.sh latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Or specific services:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t forgejo.riotpiao.com/rock/poimen-memory:v1.0.0 ./memory
|
|
||||||
docker push forgejo.riotpiao.com/rock/poimen-memory:v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Image Tagging Strategy
|
|
||||||
|
|
||||||
- `latest`: Development/staging
|
|
||||||
- `v1.0.0`, `v1.0.1`, etc.: Production releases
|
|
||||||
- `main-{commit-hash}`: CI/CD automated builds
|
|
||||||
|
|
||||||
## Kubernetes Deployment
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Kubernetes cluster (1.24+)
|
|
||||||
- kubectl configured
|
|
||||||
- Kustomize installed
|
|
||||||
- Registry credentials configured
|
|
||||||
|
|
||||||
### Deploy to Cluster
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd k8s
|
|
||||||
./deploy.sh -a
|
|
||||||
```
|
|
||||||
|
|
||||||
Or with specific tags:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy.sh -m v1.0.0 -w v1.0.0 -f v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verify Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl get pods -n poimen
|
|
||||||
kubectl get svc -n poimen
|
|
||||||
kubectl logs -n poimen -l app=poimen-workflows
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Configure in `k8s/poimen-application.yaml` under `spec.template.spec.env`:
|
|
||||||
|
|
||||||
**Common:**
|
|
||||||
- `TEMPORAL_HOST`: Temporal server (default: temporal:7233)
|
|
||||||
- `DATABASE_URL`: PostgreSQL connection
|
|
||||||
- `JWT_SECRET`: JWT signing key
|
|
||||||
- `LOG_LEVEL`: debug|info|warn|error
|
|
||||||
|
|
||||||
**Memory Service:**
|
|
||||||
- `REDIS_URL`: Redis connection
|
|
||||||
- `ELASTICSEARCH_URL`: Optional full-text search
|
|
||||||
|
|
||||||
**Workflows Service:**
|
|
||||||
- `MEMORY_SERVICE_URL`: Internal memory service URL
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- `NEXT_PUBLIC_WORKFLOWS_API`: External workflows API
|
|
||||||
- `NEXT_PUBLIC_MEMORY_API`: External memory API
|
|
||||||
- `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`: Auth provider
|
|
||||||
|
|
||||||
### Secrets
|
|
||||||
|
|
||||||
Create secrets before deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl create secret generic poimen-db-credentials \
|
|
||||||
--from-literal=memory-url="postgresql://..." \
|
|
||||||
--from-literal=workflows-url="postgresql://..." \
|
|
||||||
-n poimen
|
|
||||||
|
|
||||||
kubectl create secret generic poimen-secrets \
|
|
||||||
--from-literal=jwt-secret="..." \
|
|
||||||
--from-literal=oauth-client-id="..." \
|
|
||||||
--from-literal=oauth-client-secret="..." \
|
|
||||||
-n poimen
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ LoadBalancer Service │
|
|
||||||
│ poimen-frontend:80→3000 │
|
|
||||||
└─────────────────┬───────────────────────────┘
|
|
||||||
│
|
|
||||||
┌───────┴────────┐
|
|
||||||
▼ ▼
|
|
||||||
┌──────────────┐ ┌──────────────┐
|
|
||||||
│ Frontend │ │ Workflows │
|
|
||||||
│ (3000) │ │ API (8080) │
|
|
||||||
│ 2 replicas │ │ 2 replicas │
|
|
||||||
└──────────────┘ └──────┬───────┘
|
|
||||||
│ │
|
|
||||||
│ ┌─────┴─────┐
|
|
||||||
│ ▼ ▼
|
|
||||||
│ ┌─────────────────────┐
|
|
||||||
│ │ Temporal Cluster │
|
|
||||||
│ │ (External) │
|
|
||||||
│ └─────────────────────┘
|
|
||||||
│
|
|
||||||
└──────────────────┬──────────────┐
|
|
||||||
▼ ▼
|
|
||||||
┌──────────────┐ ┌──────────────┐
|
|
||||||
│ Memory │ │ PostgreSQL │
|
|
||||||
│ (8000) │ │ (5432) │
|
|
||||||
│ 1 replica │ │ │
|
|
||||||
└──────────────┘ └──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scaling
|
|
||||||
|
|
||||||
### Horizontal Scaling
|
|
||||||
|
|
||||||
Adjust replicas in `k8s/poimen-application.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
spec:
|
|
||||||
replicas: 3 # Increase this
|
|
||||||
```
|
|
||||||
|
|
||||||
Or patch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl patch deployment poimen-workflows -p '{"spec":{"replicas":3}}' -n poimen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Requests/Limits
|
|
||||||
|
|
||||||
Add to container spec:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 256Mi
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring & Logging
|
|
||||||
|
|
||||||
### Check Status
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl get pods -n poimen -w
|
|
||||||
kubectl describe pod <pod-name> -n poimen
|
|
||||||
kubectl logs -n poimen -f -l app=poimen-workflows --all-containers=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
|
|
||||||
All services expose `/health` endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://poimen-workflows:8080/health
|
|
||||||
curl http://poimen-memory:8000/health
|
|
||||||
curl http://poimen-frontend:3000/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Updates & Rollbacks
|
|
||||||
|
|
||||||
### Rolling Update
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./deploy.sh -w v1.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
Kubernetes automatically rolls out with health checks.
|
|
||||||
|
|
||||||
### View Rollout Status
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl rollout status deploy/poimen-workflows -n poimen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rollback
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl rollout undo deploy/poimen-workflows -n poimen
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Services Can't Connect
|
|
||||||
|
|
||||||
Check service DNS:
|
|
||||||
```bash
|
|
||||||
kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup poimen-workflows
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Migrations Failing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
kubectl exec -it <workflows-pod> -n poimen -- \
|
|
||||||
./workflows migrate up
|
|
||||||
```
|
|
||||||
|
|
||||||
### Temporal Worker Not Picking Up Activities
|
|
||||||
|
|
||||||
Check worker logs:
|
|
||||||
```bash
|
|
||||||
kubectl logs -n poimen -l app=poimen-workflows --all-containers=true | grep -i activity
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify activities registered in `cmd/worker/main.go`
|
|
||||||
|
|
||||||
## CI/CD Integration
|
|
||||||
|
|
||||||
### GitHub Actions Example
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Build & Push Poimen
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Build & Push
|
|
||||||
run: ./build-push.sh main-${{ github.sha }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automatic Deployment
|
|
||||||
|
|
||||||
Configure ArgoCD to watch `k8s/` directory for updates.
|
|
||||||
+380
-15
@@ -1,20 +1,385 @@
|
|||||||
FROM golang:1.26-alpine AS builder
|
# Multi-stage build for Poimen Temporal Worker
|
||||||
|
# Stage 1: Builder - Compile Go binary and set up tools
|
||||||
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /build
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Re-use CI-built binary if present, otherwise build
|
# Install system dependencies (ast-grep, git, build essentials)
|
||||||
ARG BINARY_PATH=
|
RUN apk add --no-cache \
|
||||||
RUN if [ -n "$BINARY_PATH" ] && [ -f "$BINARY_PATH" ]; then \
|
git \
|
||||||
cp "$BINARY_PATH" worker; \
|
curl \
|
||||||
else \
|
wget \
|
||||||
CGO_ENABLED=0 GOOS=linux go build -o worker ./cmd/worker; \
|
ca-certificates \
|
||||||
fi
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
bash \
|
||||||
|
&& echo "[builder] System dependencies installed"
|
||||||
|
|
||||||
|
# Install ast-grep CLI tool
|
||||||
|
RUN curl -fsSL https://github.com/ast-grep/ast-grep/releases/download/0.24.0/sg-x86_64-unknown-linux-musl.tar.gz \
|
||||||
|
| tar xzf - -C /usr/local/bin \
|
||||||
|
&& chmod +x /usr/local/bin/sg \
|
||||||
|
&& sg --version \
|
||||||
|
&& echo "[builder] ast-grep installed"
|
||||||
|
|
||||||
|
# Install Node.js for pi CLI and browser-use
|
||||||
|
RUN apk add --no-cache nodejs npm \
|
||||||
|
&& echo "[builder] Node.js installed"
|
||||||
|
|
||||||
|
# Install pi CLI globally
|
||||||
|
RUN npm install -g @earendil-works/pi-coding-agent --unsafe-perm \
|
||||||
|
&& pi --version \
|
||||||
|
&& echo "[builder] pi CLI installed"
|
||||||
|
|
||||||
|
# Install browser-use CLI for browser automation
|
||||||
|
RUN npm install -g browser-use --unsafe-perm \
|
||||||
|
&& browser-use --version \
|
||||||
|
&& echo "[builder] browser-use CLI installed"
|
||||||
|
|
||||||
|
# Set up pi home directory and skills
|
||||||
|
RUN mkdir -p ~/.pi/agent/skills ~/.pi/agent/agents \
|
||||||
|
&& echo "[builder] pi directories created"
|
||||||
|
|
||||||
|
# Stage 2: Download pi skills (caveman & andrej karpathy)
|
||||||
|
# Clone caveman skill from pi-agent repo
|
||||||
|
RUN cd /tmp && git clone https://github.com/earendil-works/pi-agent.git pi-repo \
|
||||||
|
&& mkdir -p ~/.pi/agent/skills/caveman \
|
||||||
|
&& cp -r pi-repo/examples/skills/caveman/* ~/.pi/agent/skills/caveman/ 2>/dev/null || true \
|
||||||
|
&& echo "[builder] caveman skill installed"
|
||||||
|
|
||||||
|
# Create andrej karpathy skill manually (reference/training patterns)
|
||||||
|
RUN mkdir -p ~/.pi/agent/skills/andrej-karpathy && cat > ~/.pi/agent/skills/andrej-karpathy/SKILL.md << 'EOF'
|
||||||
|
# Andrej Karpathy LLM & AI Principles Skill
|
||||||
|
|
||||||
|
Build neural networks and LLM systems with proven patterns from Andrej Karpathy.
|
||||||
|
Topics: attention mechanisms, transformer training, inference optimization, edge cases.
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
### 1. Simplicity First
|
||||||
|
- Start with minimal implementation
|
||||||
|
- Add complexity only when justified
|
||||||
|
- Test each component independently
|
||||||
|
- Use debugging tools effectively
|
||||||
|
|
||||||
|
### 2. Neural Network Architecture
|
||||||
|
- Understand backward pass deeply
|
||||||
|
- Implement from scratch when possible
|
||||||
|
- Use visualization for debugging
|
||||||
|
- Profile before optimizing
|
||||||
|
|
||||||
|
### 3. LLM Training Patterns
|
||||||
|
- Quality data > quantity
|
||||||
|
- Curriculum learning for complex tasks
|
||||||
|
- Loss landscape visualization
|
||||||
|
- Checkpoint strategy matters
|
||||||
|
|
||||||
|
### 4. Inference Optimization
|
||||||
|
- Quantization without quality loss
|
||||||
|
- KV cache management
|
||||||
|
- Batch processing strategies
|
||||||
|
- Latency profiling
|
||||||
|
|
||||||
|
### 5. Failure Analysis
|
||||||
|
- Log intermediate activations
|
||||||
|
- Check gradient flow
|
||||||
|
- Validate data pipeline
|
||||||
|
- Test edge cases explicitly
|
||||||
|
|
||||||
|
## Usage in Poimen
|
||||||
|
|
||||||
|
Apply when:
|
||||||
|
- Designing workflow stages (like training curricula)
|
||||||
|
- Optimizing inference (planner/judge/implementer prompts)
|
||||||
|
- Debugging convergence issues (retry patterns)
|
||||||
|
- Scaling to production (quantization patterns)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- github.com/karpathy/minGPT - Minimal GPT implementation
|
||||||
|
- youtube: "Neural Networks: Zero to Hero" series
|
||||||
|
- Papers: Attention Is All You Need, GPT series whitepapers
|
||||||
|
EOF
|
||||||
|
&& echo "[builder] andrej-karpathy skill created"
|
||||||
|
|
||||||
|
# Create browser-use skill for web testing & automation
|
||||||
|
RUN mkdir -p ~/.pi/agent/skills/browser-use && cat > ~/.pi/agent/skills/browser-use/SKILL.md << 'EOF'
|
||||||
|
# browser-use: Browser Automation Skill
|
||||||
|
|
||||||
|
Automate web browser interactions for testing, verification, and UI validation.
|
||||||
|
Topics: headless browser control, visual testing, form automation, screenshot capture.
|
||||||
|
|
||||||
|
## Key Capabilities
|
||||||
|
|
||||||
|
### 1. Browser Control
|
||||||
|
- Launch headless Chrome/Firefox
|
||||||
|
- Navigate to URLs
|
||||||
|
- Wait for elements/navigation
|
||||||
|
- Handle popups/dialogs
|
||||||
|
|
||||||
|
### 2. Interaction Patterns
|
||||||
|
- Click buttons/links
|
||||||
|
- Fill forms (text, dropdown, checkbox)
|
||||||
|
- Drag & drop
|
||||||
|
- Keyboard input
|
||||||
|
|
||||||
|
### 3. Verification & Capture
|
||||||
|
- Screenshot capture
|
||||||
|
- Element inspection
|
||||||
|
- Accessibility checks
|
||||||
|
- Network monitoring
|
||||||
|
|
||||||
|
### 4. Wait Strategies
|
||||||
|
- Wait for element visible
|
||||||
|
- Wait for navigation
|
||||||
|
- Wait for condition (custom JS)
|
||||||
|
- Timeout handling
|
||||||
|
|
||||||
|
### 5. Error Recovery
|
||||||
|
- Retry failed actions
|
||||||
|
- Handle stale elements
|
||||||
|
- Browser crash recovery
|
||||||
|
- Memory leak prevention
|
||||||
|
|
||||||
|
## Usage in Poimen Phases
|
||||||
|
|
||||||
|
### Phase T2 (Implementation)
|
||||||
|
- Test generated UI code in real browser
|
||||||
|
- Verify visual layout matches spec
|
||||||
|
- Validate form inputs work correctly
|
||||||
|
|
||||||
|
### Phase T3 (Verification)
|
||||||
|
- Visual regression testing
|
||||||
|
- Accessibility validation (ARIA, keyboard nav)
|
||||||
|
- Cross-browser verification
|
||||||
|
|
||||||
|
### Phase T6 (Integration)
|
||||||
|
- End-to-end workflow testing
|
||||||
|
- External service integration testing
|
||||||
|
- User journey verification
|
||||||
|
|
||||||
|
### Phase T9 (Release)
|
||||||
|
- Pre-release smoke tests
|
||||||
|
- Deployment verification
|
||||||
|
- Production canary testing
|
||||||
|
|
||||||
|
## Example Workflows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch browser and take screenshot
|
||||||
|
browser-use screenshot "https://example.com" --file output.png
|
||||||
|
|
||||||
|
# Fill form and submit
|
||||||
|
browser-use interact "https://example.com" \
|
||||||
|
--click "#submit-btn" \
|
||||||
|
--type "#email" "[email protected]" \
|
||||||
|
--type "#password" "secretpass" \
|
||||||
|
--click ".submit"
|
||||||
|
|
||||||
|
# Wait for dynamic content and extract data
|
||||||
|
browser-use extract "https://example.com" \
|
||||||
|
--wait ".dynamic-content" \
|
||||||
|
--selector ".data-row" \
|
||||||
|
--output json
|
||||||
|
|
||||||
|
# Accessibility audit
|
||||||
|
browser-use audit "https://example.com" \
|
||||||
|
--check wcag2a \
|
||||||
|
--report a11y-report.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Poimen
|
||||||
|
|
||||||
|
Pre-generated code can be tested:
|
||||||
|
```bash
|
||||||
|
# Generate code (T2)
|
||||||
|
implementer_output = "function handleClick() { ... }"
|
||||||
|
|
||||||
|
# Verify in browser (T3)
|
||||||
|
browser-use interact "http://localhost:3000" \
|
||||||
|
--click ".test-button" \
|
||||||
|
--screenshot result.png
|
||||||
|
|
||||||
|
# Compare with expected
|
||||||
|
verify_visual_match(result.png, expected.png)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
- Startup: ~2-5s per browser
|
||||||
|
- Action latency: 100-500ms per interaction
|
||||||
|
- Screenshot: 500ms-2s (depends on page size)
|
||||||
|
- Keep browser alive for batch operations (pool management)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Transient: Network timeout → retry with backoff
|
||||||
|
- Permanent: Element not found → fail and log
|
||||||
|
- Flaky: Wait strategies → increase timeout gradually
|
||||||
|
- Memory: Reuse browser instances → kill after 10 uses
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- docs.browseruse.com - Official documentation
|
||||||
|
- github.com/browser-use/browser-use - Source code
|
||||||
|
- Chrome DevTools Protocol - Advanced browser control
|
||||||
|
EOF
|
||||||
|
&& echo "[builder] browser-use skill created"
|
||||||
|
|
||||||
|
# Copy Go source code
|
||||||
|
COPY . /build/
|
||||||
|
|
||||||
|
# Download Go dependencies
|
||||||
|
RUN go mod download \
|
||||||
|
&& echo "[builder] Go dependencies downloaded"
|
||||||
|
|
||||||
|
# Build worker binary
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -o /build/worker ./cmd/worker \
|
||||||
|
&& echo "[builder] Worker binary built"
|
||||||
|
|
||||||
|
# Verify binary
|
||||||
|
RUN file /build/worker && ls -lh /build/worker
|
||||||
|
|
||||||
|
# Stage 3: Runtime - Minimal base image with runtime dependencies
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
RUN apk --no-cache add ca-certificates
|
|
||||||
|
LABEL maintainer="Poimen Team"
|
||||||
|
LABEL description="Poimen Temporal Worker with memory service, ast-grep, and browser automation"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/worker .
|
|
||||||
ENTRYPOINT ["./worker"]
|
# Install runtime dependencies (including Chromium for browser-use)
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
git \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
jq \
|
||||||
|
chromium \
|
||||||
|
chromium-chromedriver \
|
||||||
|
&& echo "[runtime] Runtime dependencies installed"
|
||||||
|
|
||||||
|
# Install Node.js for pi CLI and browser-use
|
||||||
|
RUN apk add --no-cache nodejs npm \
|
||||||
|
&& echo "[runtime] Node.js installed"
|
||||||
|
|
||||||
|
# Install pi CLI in runtime image
|
||||||
|
RUN npm install -g @earendil-works/pi-coding-agent --unsafe-perm \
|
||||||
|
&& pi --version \
|
||||||
|
&& echo "[runtime] pi CLI installed"
|
||||||
|
|
||||||
|
# Install browser-use CLI in runtime image
|
||||||
|
RUN npm install -g browser-use --unsafe-perm \
|
||||||
|
&& browser-use --version \
|
||||||
|
&& echo "[runtime] browser-use CLI installed"
|
||||||
|
|
||||||
|
# Copy ast-grep binary from builder
|
||||||
|
COPY --from=builder /usr/local/bin/sg /usr/local/bin/sg
|
||||||
|
RUN chmod +x /usr/local/bin/sg && sg --version \
|
||||||
|
&& echo "[runtime] ast-grep copied"
|
||||||
|
|
||||||
|
# Copy pi skills from builder
|
||||||
|
COPY --from=builder /root/.pi /root/.pi
|
||||||
|
RUN ls -la /root/.pi/agent/skills/ \
|
||||||
|
&& echo "[runtime] pi skills configured"
|
||||||
|
|
||||||
|
# Copy worker binary from builder
|
||||||
|
COPY --from=builder /build/worker /app/worker
|
||||||
|
RUN chmod +x /app/worker && file /app/worker \
|
||||||
|
&& echo "[runtime] Worker binary copied"
|
||||||
|
|
||||||
|
# Create app directory structure
|
||||||
|
RUN mkdir -p /app/work /app/logs /app/screenshots \
|
||||||
|
&& chmod 755 /app/work /app/logs /app/screenshots \
|
||||||
|
&& echo "[runtime] App directories created"
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
|
# Worker task queue listener
|
||||||
|
ENV TEMPORAL_NAMESPACE=poimen-harness \
|
||||||
|
TEMPORAL_HOSTPORT=temporal-frontend.temporal:7233 \
|
||||||
|
MEMORY_SERVICE_URL=http://memory-service.poimen:5000 \
|
||||||
|
MEMORY_SERVICE_TOKEN= \
|
||||||
|
ANTHROPIC_API_KEY= \
|
||||||
|
PI_SKILLS_PATH=/root/.pi/agent/skills \
|
||||||
|
AST_GREP_BIN=/usr/local/bin/sg \
|
||||||
|
BROWSER_USE_BIN=/usr/local/bin/browser-use \
|
||||||
|
CHROMIUM_BIN=/usr/bin/chromium-browser \
|
||||||
|
SCREENSHOTS_DIR=/app/screenshots
|
||||||
|
|
||||||
|
# Entrypoint script with startup diagnostics
|
||||||
|
COPY --chmod=755 << 'EOF' /app/entrypoint.sh
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[$(date)] ========== POIMEN WORKER STARTUP =========="
|
||||||
|
echo "[$(date)] Container: $HOSTNAME"
|
||||||
|
echo "[$(date)] Image: $(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2)"
|
||||||
|
|
||||||
|
# Verify CLI tools
|
||||||
|
echo "[$(date)] ✓ Checking CLI tools..."
|
||||||
|
echo " - Go version: $(go version 2>/dev/null || echo 'N/A')"
|
||||||
|
echo " - ast-grep: $(sg --version 2>&1 | head -1)"
|
||||||
|
echo " - pi: $(pi --version 2>&1 | head -1)"
|
||||||
|
echo " - browser-use: $(browser-use --version 2>&1 | head -1)"
|
||||||
|
echo " - chromium: $(chromium-browser --version 2>&1 || echo 'Not found')"
|
||||||
|
echo " - git: $(git --version)"
|
||||||
|
echo " - node: $(node --version)"
|
||||||
|
echo " - npm: $(npm --version)"
|
||||||
|
|
||||||
|
# Verify pi skills
|
||||||
|
echo "[$(date)] ✓ Checking pi skills..."
|
||||||
|
if [ -d "$PI_SKILLS_PATH" ]; then
|
||||||
|
echo " - Skills path: $PI_SKILLS_PATH"
|
||||||
|
ls -1 "$PI_SKILLS_PATH" | sed 's/^/ ✓ /'
|
||||||
|
else
|
||||||
|
echo " - WARNING: Skills path not found: $PI_SKILLS_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify browser tools
|
||||||
|
echo "[$(date)] ✓ Checking browser automation tools..."
|
||||||
|
echo " - Chromium binary: $CHROMIUM_BIN"
|
||||||
|
echo " - Screenshots directory: $SCREENSHOTS_DIR"
|
||||||
|
if [ -d "$SCREENSHOTS_DIR" ]; then
|
||||||
|
echo " - Screenshots dir ready ($(du -sh $SCREENSHOTS_DIR 2>/dev/null | cut -f1 || echo '0B'))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check environment variables
|
||||||
|
echo "[$(date)] ✓ Configuration loaded:"
|
||||||
|
echo " - TEMPORAL_NAMESPACE: $TEMPORAL_NAMESPACE"
|
||||||
|
echo " - TEMPORAL_HOSTPORT: $TEMPORAL_HOSTPORT"
|
||||||
|
echo " - MEMORY_SERVICE_URL: ${MEMORY_SERVICE_URL:-(not set)}"
|
||||||
|
echo " - PI_SKILLS_PATH: $PI_SKILLS_PATH"
|
||||||
|
echo " - CHROMIUM_BIN: $CHROMIUM_BIN"
|
||||||
|
|
||||||
|
# Verify memory service connectivity (optional, non-blocking)
|
||||||
|
if [ ! -z "$MEMORY_SERVICE_URL" ]; then
|
||||||
|
echo "[$(date)] ✓ Testing memory service connectivity..."
|
||||||
|
if curl -sf "$MEMORY_SERVICE_URL/health" > /dev/null 2>&1; then
|
||||||
|
echo " - Memory service: HEALTHY"
|
||||||
|
else
|
||||||
|
echo " - Memory service: UNREACHABLE (will retry in worker)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test browser automation (optional, non-blocking)
|
||||||
|
echo "[$(date)] ✓ Testing browser automation..."
|
||||||
|
if command -v chromium-browser &> /dev/null && command -v browser-use &> /dev/null; then
|
||||||
|
echo " - Chromium available: YES"
|
||||||
|
echo " - browser-use available: YES"
|
||||||
|
echo " - Browser automation: READY"
|
||||||
|
else
|
||||||
|
echo " - Browser automation: WARNING - missing dependencies"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(date)] ========== STARTING WORKER =========="
|
||||||
|
exec /app/worker
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
# Run worker with diagnostics
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8081/health || exit 1
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,21 +1,14 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/db"
|
"github.com/rockliang/poimen/workflows/pkg/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CanvasCompatibilityOutput validation results
|
|
||||||
type CanvasCompatibilityOutput struct {
|
|
||||||
IsValid bool `json:"is_valid"`
|
|
||||||
Incompatibilities []IncompatibilityWarning `json:"incompatibilities"`
|
|
||||||
DisconnectedNodes []string `json:"disconnected_nodes"`
|
|
||||||
Warnings []string `json:"warnings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncompatibilityWarning explains why two activities can't be connected
|
// IncompatibilityWarning explains why two activities can't be connected
|
||||||
type IncompatibilityWarning struct {
|
type IncompatibilityWarning struct {
|
||||||
Source string `json:"source"` // Source node ID
|
Source string `json:"source"` // Source node ID
|
||||||
@@ -48,7 +41,7 @@ type OutputField struct {
|
|||||||
// getActivitySchema returns schema from knowledge base
|
// getActivitySchema returns schema from knowledge base
|
||||||
func getActivitySchema(activityType string) (*ActivitySchema, error) {
|
func getActivitySchema(activityType string) (*ActivitySchema, error) {
|
||||||
kb := knowledgeBaseData()
|
kb := knowledgeBaseData()
|
||||||
if kb == "" {
|
if kb == nil {
|
||||||
return nil, fmt.Errorf("knowledge base not loaded")
|
return nil, fmt.Errorf("knowledge base not loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +189,7 @@ func CheckConnectionCompatibility(sourceNode, targetNode db.WorkflowNode) []Inco
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CheckCanvasConnectivity analyzes all suggested edges for compatibility
|
// CheckCanvasConnectivity analyzes all suggested edges for compatibility
|
||||||
func CheckCanvasConnectivity(nodes []db.WorkflowNode, suggestedEdges []EdgeWithWording) []IncompatibilityWarning {
|
func CheckCanvasConnectivity(nodes []db.WorkflowNode, suggestedEdges []db.WorkflowEdge) []IncompatibilityWarning {
|
||||||
warnings := []IncompatibilityWarning{}
|
warnings := []IncompatibilityWarning{}
|
||||||
nodeMap := make(map[string]db.WorkflowNode)
|
nodeMap := make(map[string]db.WorkflowNode)
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
@@ -221,7 +214,7 @@ func CheckCanvasConnectivity(nodes []db.WorkflowNode, suggestedEdges []EdgeWithW
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IdentifyDisconnectedNodes finds nodes that can't connect to anything
|
// IdentifyDisconnectedNodes finds nodes that can't connect to anything
|
||||||
func IdentifyDisconnectedNodes(nodes []db.WorkflowNode, suggestedEdges []EdgeWithWording) []string {
|
func IdentifyDisconnectedNodes(nodes []db.WorkflowNode, suggestedEdges []db.WorkflowEdge) []string {
|
||||||
edgeMap := make(map[string]bool)
|
edgeMap := make(map[string]bool)
|
||||||
for _, edge := range suggestedEdges {
|
for _, edge := range suggestedEdges {
|
||||||
edgeMap[edge.Source] = true
|
edgeMap[edge.Source] = true
|
||||||
@@ -300,60 +293,3 @@ func knowledgeBaseData() string {
|
|||||||
// For now, return empty - real implementation loads from file
|
// For now, return empty - real implementation loads from file
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanvasCompatibilityActivity validates workflow canvas for type mismatches and isolation
|
|
||||||
func CanvasCompatibilityActivity(ctx interface{}, input CanvasCompatibilityInput) (CanvasCompatibilityOutput, error) {
|
|
||||||
output := CanvasCompatibilityOutput{
|
|
||||||
IsValid: true,
|
|
||||||
Incompatibilities: []IncompatibilityWarning{},
|
|
||||||
DisconnectedNodes: []string{},
|
|
||||||
Warnings: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all edges for compatibility
|
|
||||||
for _, edge := range input.Edges {
|
|
||||||
var sourceNode, targetNode *db.WorkflowNode
|
|
||||||
for i := range input.Nodes {
|
|
||||||
if input.Nodes[i].ID == edge.Source {
|
|
||||||
sourceNode = &input.Nodes[i]
|
|
||||||
}
|
|
||||||
if input.Nodes[i].ID == edge.Target {
|
|
||||||
targetNode = &input.Nodes[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceNode != nil && targetNode != nil {
|
|
||||||
if warning, err := ValidateConnection(sourceNode, targetNode); err != nil {
|
|
||||||
output.IsValid = false
|
|
||||||
output.Incompatibilities = append(output.Incompatibilities, warning)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find disconnected nodes
|
|
||||||
connected := make(map[string]bool)
|
|
||||||
for _, edge := range input.Edges {
|
|
||||||
connected[edge.Source] = true
|
|
||||||
connected[edge.Target] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, node := range input.Nodes {
|
|
||||||
if node.Type == "activity" && !connected[node.ID] {
|
|
||||||
output.DisconnectedNodes = append(output.DisconnectedNodes, node.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateConnection checks if two nodes can be connected based on their types.
|
|
||||||
func ValidateConnection(source, target *db.WorkflowNode) (IncompatibilityWarning, error) {
|
|
||||||
if source.Type != "activity" || target.Type != "activity" {
|
|
||||||
return IncompatibilityWarning{
|
|
||||||
Source: source.ID,
|
|
||||||
Target: target.ID,
|
|
||||||
Reason: fmt.Sprintf("Cannot connect %s to %s: both must be activity type", source.Type, target.Type),
|
|
||||||
}, fmt.Errorf("type mismatch")
|
|
||||||
}
|
|
||||||
return IncompatibilityWarning{}, nil
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,44 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/activity/llm"
|
"github.com/rockliang/poimen/workflows/action/llm"
|
||||||
"github.com/rockliang/poimen/workflows/pkg/db"
|
"github.com/rockliang/poimen/workflows/pkg/db"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CanvasReasonerInput infers connections between nodes using LLM reasoning
|
||||||
|
type CanvasReasonerInput struct {
|
||||||
|
Nodes []db.WorkflowNode `json:"nodes"` // Canvas nodes
|
||||||
|
Edges []db.WorkflowEdge `json:"edges"` // Existing edges
|
||||||
|
// If true, only suggest new edges; if false, redesign entire canvas
|
||||||
|
PreserveExisting bool `json:"preserve_existing,omitempty"`
|
||||||
|
AuthToken string `json:"auth_token,omitempty"` // JWT for LLM calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// RelationWording describes semantic meaning of an edge
|
||||||
|
type RelationWording struct {
|
||||||
|
Verb string `json:"verb"` // outputs, inputs, depends-on, etc
|
||||||
|
SourceOutput string `json:"source_output"` // What source produces
|
||||||
|
TargetInput string `json:"target_input"` // What target requires
|
||||||
|
ConnectionType string `json:"connection_type"` // direct-map, requires-transformer, conditional
|
||||||
|
Confidence float64 `json:"confidence"` // 0.0-1.0
|
||||||
|
SemanticMatch string `json:"semantic_match"` // Human-readable explanation
|
||||||
|
TransformerNeeded string `json:"transformer_needed,omitempty"` // If transformation required
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeWithWording pairs an edge with its semantic description
|
||||||
|
type EdgeWithWording struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
RelationType string `json:"relation_type"` // data-flow, dependency, conditional, parallel
|
||||||
|
RelationLabel string `json:"relation_label"` // e.g., "CloneRepo outputs path → AnalyzeCode requires path"
|
||||||
|
RelationWording RelationWording `json:"relation_wording"`
|
||||||
|
}
|
||||||
|
|
||||||
// CanvasReasonerOutput returns suggested edges and reasoning
|
// CanvasReasonerOutput returns suggested edges and reasoning
|
||||||
type CanvasReasonerOutput struct {
|
type CanvasReasonerOutput struct {
|
||||||
SuggestedEdges []EdgeWithWording `json:"suggested_edges"` // Edges with wording
|
SuggestedEdges []EdgeWithWording `json:"suggested_edges"` // Edges with wording
|
||||||
@@ -25,14 +55,14 @@ func CanvasReasonerActivity(ctx context.Context, in CanvasReasonerInput) (Canvas
|
|||||||
logger := newActivityLogger(ctx)
|
logger := newActivityLogger(ctx)
|
||||||
|
|
||||||
output := CanvasReasonerOutput{
|
output := CanvasReasonerOutput{
|
||||||
SuggestedEdges: []EdgeWithWording{},
|
SuggestedEdges: []db.WorkflowEdge{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(in.Nodes) == 0 {
|
if len(in.Nodes) == 0 {
|
||||||
return output, fmt.Errorf("no nodes provided")
|
return output, fmt.Errorf("no nodes provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Analyzing canvas with %d nodes, %d edges", len(in.Nodes), len(in.Edges))
|
logger.logf("info", "Analyzing canvas with %d nodes, %d edges", len(in.Nodes), len(in.Edges))
|
||||||
|
|
||||||
// Build activity descriptions for LLM context
|
// Build activity descriptions for LLM context
|
||||||
nodeDesc := buildNodeDescriptions(in.Nodes)
|
nodeDesc := buildNodeDescriptions(in.Nodes)
|
||||||
@@ -88,7 +118,7 @@ KEY RULES:
|
|||||||
|
|
||||||
Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReasoningTask(in.PreserveExisting))
|
Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReasoningTask(in.PreserveExisting))
|
||||||
|
|
||||||
logger.Info("Calling LLM reasoning (preserve_existing=%v)", in.PreserveExisting)
|
logger.logf("info", "Calling LLM reasoning (preserve_existing=%v)", in.PreserveExisting)
|
||||||
|
|
||||||
// Call LLM
|
// Call LLM
|
||||||
client, err := llm.NewClient()
|
client, err := llm.NewClient()
|
||||||
@@ -97,7 +127,7 @@ Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReason
|
|||||||
}
|
}
|
||||||
|
|
||||||
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
||||||
Model: ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: "reasoning", // Use reasoning model for complex analysis
|
ModelID: "reasoning", // Use reasoning model for complex analysis
|
||||||
},
|
},
|
||||||
SystemPrompt: systemPrompt,
|
SystemPrompt: systemPrompt,
|
||||||
@@ -116,13 +146,13 @@ Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReason
|
|||||||
|
|
||||||
// Parse LLM response
|
// Parse LLM response
|
||||||
var reasonerResp struct {
|
var reasonerResp struct {
|
||||||
Edges []EdgeWithWording `json:"edges"`
|
Edges []db.WorkflowEdge `json:"edges"`
|
||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
Confidence float64 `json:"confidence"`
|
Confidence float64 `json:"confidence"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(response), &reasonerResp); err != nil {
|
if err := json.Unmarshal([]byte(response), &reasonerResp); err != nil {
|
||||||
logger.Warn("Failed to parse LLM response as JSON: %v", err)
|
logger.logf("warn", "Failed to parse LLM response as JSON: %v", err)
|
||||||
// Try to extract from response text
|
// Try to extract from response text
|
||||||
output.Reasoning = response
|
output.Reasoning = response
|
||||||
output.Confidence = 0.5
|
output.Confidence = 0.5
|
||||||
@@ -135,19 +165,19 @@ Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReason
|
|||||||
nodeMap[n.ID] = true
|
nodeMap[n.ID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
validEdges := []EdgeWithWording{}
|
validEdges := []db.WorkflowEdge{}
|
||||||
for _, edge := range reasonerResp.Edges {
|
for _, edge := range reasonerResp.Edges {
|
||||||
if !nodeMap[edge.Source] {
|
if !nodeMap[edge.Source] {
|
||||||
logger.Warn("Suggested edge references unknown source: %s", edge.Source)
|
logger.logf("warn", "Suggested edge references unknown source: %s", edge.Source)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !nodeMap[edge.Target] {
|
if !nodeMap[edge.Target] {
|
||||||
logger.Warn("Suggested edge references unknown target: %s", edge.Target)
|
logger.logf("warn", "Suggested edge references unknown target: %s", edge.Target)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Don't suggest self-loops
|
// Don't suggest self-loops
|
||||||
if edge.Source == edge.Target {
|
if edge.Source == edge.Target {
|
||||||
logger.Warn("Skipping self-loop: %s", edge.Source)
|
logger.logf("warn", "Skipping self-loop: %s", edge.Source)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
validEdges = append(validEdges, edge)
|
validEdges = append(validEdges, edge)
|
||||||
@@ -161,7 +191,7 @@ Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReason
|
|||||||
incompatibilities := CheckCanvasConnectivity(in.Nodes, validEdges)
|
incompatibilities := CheckCanvasConnectivity(in.Nodes, validEdges)
|
||||||
if len(incompatibilities) > 0 {
|
if len(incompatibilities) > 0 {
|
||||||
output.IncompatibleEdges = incompatibilities
|
output.IncompatibleEdges = incompatibilities
|
||||||
logger.Warn("Found %d incompatible edge connections", len(incompatibilities))
|
logger.logf("warn", "Found %d incompatible edge connections", len(incompatibilities))
|
||||||
|
|
||||||
// Generate user-friendly alerts
|
// Generate user-friendly alerts
|
||||||
for i, incompat := range incompatibilities {
|
for i, incompat := range incompatibilities {
|
||||||
@@ -179,7 +209,7 @@ Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReason
|
|||||||
disconnected := IdentifyDisconnectedNodes(in.Nodes, validEdges)
|
disconnected := IdentifyDisconnectedNodes(in.Nodes, validEdges)
|
||||||
if len(disconnected) > 0 {
|
if len(disconnected) > 0 {
|
||||||
output.DisconnectedNodes = disconnected
|
output.DisconnectedNodes = disconnected
|
||||||
logger.Warn("Found %d disconnected nodes", len(disconnected))
|
logger.logf("warn", "Found %d disconnected nodes", len(disconnected))
|
||||||
|
|
||||||
for _, nodeID := range disconnected {
|
for _, nodeID := range disconnected {
|
||||||
var label string
|
var label string
|
||||||
@@ -197,7 +227,7 @@ Return ONLY valid JSON, no markdown code blocks.`, nodeDesc, edgeDesc, getReason
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("LLM suggested %d edges with confidence %.2f | %d incompatibilities | %d disconnected",
|
logger.logf("info", "LLM suggested %d edges with confidence %.2f | %d incompatibilities | %d disconnected",
|
||||||
len(validEdges), output.Confidence, len(incompatibilities), len(disconnected))
|
len(validEdges), output.Confidence, len(incompatibilities), len(disconnected))
|
||||||
|
|
||||||
return output, nil
|
return output, nil
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/action/llm"
|
||||||
|
"github.com/rockliang/poimen/workflows/prompts"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
|
"go.temporal.io/sdk/activity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImplementerInput is input to ImplementerActivity.
|
||||||
|
type ImplementerInput struct {
|
||||||
|
Config statemachine.OrchestratorConfig
|
||||||
|
TaskID string
|
||||||
|
WorktreePath string
|
||||||
|
Lessons string // "known errors — do not repeat" section
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementerOutput is the output of ImplementerActivity.
|
||||||
|
type ImplementerOutput struct {
|
||||||
|
Success bool
|
||||||
|
Changes string // summary of changes made
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImplementerActivity calls the Implementer LLM to implement the task.
|
||||||
|
func ImplementerActivity(ctx context.Context, in ImplementerInput) (ImplementerOutput, error) {
|
||||||
|
// Record heartbeat
|
||||||
|
activity.RecordHeartbeat(ctx, "starting implementer for "+in.TaskID)
|
||||||
|
|
||||||
|
// Get LLM client
|
||||||
|
client, err := llm.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return ImplementerOutput{}, fmt.Errorf("failed to create LLM client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implementer spec
|
||||||
|
implementerSpec, exists := in.Config.RolePrompts["implementer"]
|
||||||
|
if !exists {
|
||||||
|
return ImplementerOutput{}, fmt.Errorf("implementer role prompt not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build variables for template
|
||||||
|
templateVars := map[string]any{
|
||||||
|
"SystemPrompt": in.Config.SystemPrompt,
|
||||||
|
"Task": in.TaskID,
|
||||||
|
"WorktreePath": in.WorktreePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject lessons if provided
|
||||||
|
if in.Lessons != "" {
|
||||||
|
templateVars["Lessons"] = in.Lessons
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render template
|
||||||
|
var templateContent string
|
||||||
|
if implementerSpec.RawTemplate != "" {
|
||||||
|
templateContent = implementerSpec.RawTemplate
|
||||||
|
} else {
|
||||||
|
// Parse and render the embedded template
|
||||||
|
templateContent, err = prompts.Render(implementerSpec.TemplateRef, templateVars)
|
||||||
|
if err != nil {
|
||||||
|
return ImplementerOutput{}, fmt.Errorf("failed to render implementer template: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call LLM
|
||||||
|
messages := []llm.MessageParam{
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: templateContent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
||||||
|
Model: implementerSpec.Model,
|
||||||
|
SystemPrompt: in.Config.SystemPrompt,
|
||||||
|
Messages: messages,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return ImplementerOutput{}, fmt.Errorf("implementer LLM call failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record progress
|
||||||
|
activity.RecordHeartbeat(ctx, "implementer completed for "+in.TaskID)
|
||||||
|
|
||||||
|
// Return success (in full implementation would parse response and execute tool calls)
|
||||||
|
return ImplementerOutput{
|
||||||
|
Success: true,
|
||||||
|
Changes: response,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/action/llm"
|
||||||
|
"github.com/rockliang/poimen/workflows/prompts"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JudgeInput is input to JudgeActivity.
|
||||||
|
type JudgeInput struct {
|
||||||
|
Config statemachine.OrchestratorConfig
|
||||||
|
Diff string // git diff output
|
||||||
|
IntegrationTestLogs string // test output
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeOutput is the output of JudgeActivity.
|
||||||
|
type JudgeOutput struct {
|
||||||
|
Verdict string // "pass" or "fail"
|
||||||
|
Critique string // explanation if fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeActivity calls the Judge LLM to review correctness.
|
||||||
|
func JudgeActivity(ctx context.Context, in JudgeInput) (JudgeOutput, error) {
|
||||||
|
// Get LLM client
|
||||||
|
client, err := llm.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return JudgeOutput{}, fmt.Errorf("failed to create LLM client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get judge spec
|
||||||
|
judgeSpec, exists := in.Config.RolePrompts["judge"]
|
||||||
|
if !exists {
|
||||||
|
return JudgeOutput{}, fmt.Errorf("judge role prompt not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render template
|
||||||
|
var templateContent string
|
||||||
|
if judgeSpec.RawTemplate != "" {
|
||||||
|
templateContent = judgeSpec.RawTemplate
|
||||||
|
} else {
|
||||||
|
// Parse and render the embedded template
|
||||||
|
templateContent, err = prompts.Render(judgeSpec.TemplateRef, map[string]any{
|
||||||
|
"SystemPrompt": in.Config.SystemPrompt,
|
||||||
|
"Diff": in.Diff,
|
||||||
|
"TestResult": in.IntegrationTestLogs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return JudgeOutput{}, fmt.Errorf("failed to render judge template: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call LLM
|
||||||
|
messages := []llm.MessageParam{
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: templateContent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
||||||
|
Model: judgeSpec.Model,
|
||||||
|
SystemPrompt: in.Config.SystemPrompt,
|
||||||
|
Messages: messages,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return JudgeOutput{}, fmt.Errorf("judge LLM call failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return a default pass verdict
|
||||||
|
// In full implementation, would parse LLM response
|
||||||
|
return JudgeOutput{
|
||||||
|
Verdict: "pass",
|
||||||
|
Critique: response,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -54,7 +54,7 @@ func NewClient() (*OpenAIClient, error) {
|
|||||||
|
|
||||||
// MessageInput is the input to CreateMessage.
|
// MessageInput is the input to CreateMessage.
|
||||||
type MessageInput struct {
|
type MessageInput struct {
|
||||||
Model types.ModelSpec
|
Model statemachine.ModelSpec
|
||||||
SystemPrompt string
|
SystemPrompt string
|
||||||
Messages []MessageParam
|
Messages []MessageParam
|
||||||
AuthToken string // Optional JWT token for authenticated endpoints
|
AuthToken string // Optional JWT token for authenticated endpoints
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
@@ -70,7 +70,7 @@ func TestCreateMessageValidation(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
in := MessageInput{
|
in := MessageInput{
|
||||||
Model: types.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: tt.modelID,
|
ModelID: tt.modelID,
|
||||||
},
|
},
|
||||||
SystemPrompt: "test",
|
SystemPrompt: "test",
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/action/llm"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LLMInferenceInput is input for LLMInferenceActivity
|
||||||
|
type LLMInferenceInput struct {
|
||||||
|
Model string `json:"model"` // Model ID (reasoning, ornith:35b, etc)
|
||||||
|
SystemPrompt string `json:"system_prompt"` // System instruction
|
||||||
|
UserPrompt string `json:"user_prompt"` // User message
|
||||||
|
Temperature float64 `json:"temperature,omitempty"` // LLM temperature (0-1)
|
||||||
|
MaxTokens int `json:"max_tokens,omitempty"` // Max output tokens
|
||||||
|
AuthToken string `json:"auth_token,omitempty"` // JWT token for authenticated endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMInferenceOutput is output from LLMInferenceActivity
|
||||||
|
type LLMInferenceOutput struct {
|
||||||
|
Response string `json:"response"` // LLM response text
|
||||||
|
Model string `json:"model"` // Model used
|
||||||
|
StopReason string `json:"stop_reason"` // How inference stopped (stop_sequence, length, etc)
|
||||||
|
TokensUsed int `json:"tokens_used"` // Total tokens consumed
|
||||||
|
ErrorMessage string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMInferenceActivity calls LLM API with given prompt and returns response
|
||||||
|
func LLMInferenceActivity(ctx context.Context, in LLMInferenceInput) (LLMInferenceOutput, error) {
|
||||||
|
logger := newActivityLogger(ctx)
|
||||||
|
|
||||||
|
output := LLMInferenceOutput{
|
||||||
|
Model: in.Model,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if in.Model == "" {
|
||||||
|
return output, fmt.Errorf("model not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.UserPrompt == "" {
|
||||||
|
return output, fmt.Errorf("user_prompt not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.logf("info", "Starting LLM inference with model: %s", in.Model)
|
||||||
|
|
||||||
|
// Create LLM client
|
||||||
|
client, err := llm.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
output.ErrorMessage = err.Error()
|
||||||
|
return output, fmt.Errorf("failed to create LLM client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call LLM
|
||||||
|
logger.logf("info", "Calling LLM API (model=%s, prompt_len=%d, auth=%v)", in.Model, len(in.UserPrompt), in.AuthToken != "")
|
||||||
|
|
||||||
|
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
||||||
|
Model: statemachine.ModelSpec{
|
||||||
|
ModelID: in.Model,
|
||||||
|
},
|
||||||
|
SystemPrompt: in.SystemPrompt,
|
||||||
|
Messages: []llm.MessageParam{
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: in.UserPrompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthToken: in.AuthToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
output.ErrorMessage = err.Error()
|
||||||
|
logger.logf("error", "LLM API call failed: %v", err)
|
||||||
|
return output, fmt.Errorf("LLM inference failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Response = response
|
||||||
|
output.StopReason = "stop_sequence"
|
||||||
|
|
||||||
|
logger.logf("info", "LLM inference completed (response_len=%d)", len(response))
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMBatchInferenceInput is input for batch inference
|
||||||
|
type LLMBatchInferenceInput struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
SystemPrompt string `json:"system_prompt"`
|
||||||
|
Prompts []string `json:"prompts"` // List of user prompts
|
||||||
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
|
AuthToken string `json:"auth_token,omitempty"` // JWT token for authenticated endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMBatchInferenceOutput is output from batch inference
|
||||||
|
type LLMBatchInferenceOutput struct {
|
||||||
|
Responses []string `json:"responses"` // LLM responses (parallel to input Prompts)
|
||||||
|
Model string `json:"model"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLMBatchInferenceActivity calls LLM multiple times in sequence
|
||||||
|
func LLMBatchInferenceActivity(ctx context.Context, in LLMBatchInferenceInput) (LLMBatchInferenceOutput, error) {
|
||||||
|
logger := newActivityLogger(ctx)
|
||||||
|
|
||||||
|
output := LLMBatchInferenceOutput{
|
||||||
|
Model: in.Model,
|
||||||
|
Responses: []string{},
|
||||||
|
Errors: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Model == "" {
|
||||||
|
return output, fmt.Errorf("model not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(in.Prompts) == 0 {
|
||||||
|
return output, fmt.Errorf("no prompts provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.logf("info", "Starting batch LLM inference (model=%s, count=%d)", in.Model, len(in.Prompts))
|
||||||
|
|
||||||
|
// Create LLM client
|
||||||
|
client, err := llm.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return output, fmt.Errorf("failed to create LLM client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each prompt
|
||||||
|
for i, prompt := range in.Prompts {
|
||||||
|
logger.logf("info", "Processing prompt %d/%d", i+1, len(in.Prompts))
|
||||||
|
|
||||||
|
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
||||||
|
Model: statemachine.ModelSpec{
|
||||||
|
ModelID: in.Model,
|
||||||
|
},
|
||||||
|
SystemPrompt: in.SystemPrompt,
|
||||||
|
Messages: []llm.MessageParam{
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
output.Errors = append(output.Errors, fmt.Sprintf("prompt %d: %v", i, err))
|
||||||
|
output.Responses = append(output.Responses, "")
|
||||||
|
logger.logf("warn", "Failed to process prompt %d: %v", i, err)
|
||||||
|
} else {
|
||||||
|
output.Responses = append(output.Responses, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.logf("info", "Batch inference completed (responses=%d, errors=%d)",
|
||||||
|
len(output.Responses), len(output.Errors))
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -92,9 +92,9 @@ func RetrieveMemoryActivity(ctx context.Context, in RetrieveMemoryInput) (Retrie
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get memory service URL and token
|
// Get memory service URL and token
|
||||||
baseURL := os.Getenv("MEMORY_SERVICE_URL")
|
baseURL := os.Getenv("POIMEN_MEMORY_URL")
|
||||||
if baseURL == "" {
|
if baseURL == "" {
|
||||||
baseURL = "http://localhost:8080"
|
baseURL = "http://poimen-memory.poimen.svc.cluster.local:8080"
|
||||||
}
|
}
|
||||||
|
|
||||||
token := os.Getenv("POIMEN_MEMORY_TOKEN")
|
token := os.Getenv("POIMEN_MEMORY_TOKEN")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -39,7 +39,7 @@ func TestRetrieveMemoryActivity_Query(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// Set env for test
|
// Set env for test
|
||||||
t.Setenv("MEMORY_SERVICE_URL", server.URL)
|
t.Setenv("POIMEN_MEMORY_URL", server.URL)
|
||||||
|
|
||||||
output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{
|
output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{
|
||||||
Query: "security scanning",
|
Query: "security scanning",
|
||||||
@@ -93,7 +93,7 @@ func TestRetrieveMemoryActivity_Context(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
t.Setenv("MEMORY_SERVICE_URL", server.URL)
|
t.Setenv("POIMEN_MEMORY_URL", server.URL)
|
||||||
|
|
||||||
output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{
|
output, err := RetrieveMemoryActivity(context.Background(), RetrieveMemoryInput{
|
||||||
Query: "security scan repo",
|
Query: "security scan repo",
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/action/llm"
|
||||||
|
"github.com/rockliang/poimen/workflows/prompts"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlanningInput is input to PlanningActivity.
|
||||||
|
type PlanningInput struct {
|
||||||
|
Config statemachine.OrchestratorConfig
|
||||||
|
BoardState string // JSON or markdown of task board
|
||||||
|
RepoPath string // Path to target repository
|
||||||
|
Milestone string // e.g., "T0"
|
||||||
|
TaskResults []statemachine.TaskUnitOutput // Results from completed tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskDispatch represents a dispatched task.
|
||||||
|
type TaskDispatch struct {
|
||||||
|
TaskID string
|
||||||
|
PromptSpec statemachine.PromptSpec
|
||||||
|
BaseTimeout *int64 // optional override in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanningOutput is the output of PlanningActivity.
|
||||||
|
type PlanningOutput struct {
|
||||||
|
TasksToDispatch []string // Task IDs to dispatch in this cycle
|
||||||
|
CompletedBranches []string // Branches to squash merge (when milestone complete)
|
||||||
|
SubmilestoneComplete bool // Whether the milestone is complete
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanningActivity calls the Planner LLM to decide which tasks to dispatch.
|
||||||
|
func PlanningActivity(ctx context.Context, in PlanningInput) (PlanningOutput, error) {
|
||||||
|
// Get LLM client
|
||||||
|
client, err := llm.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return PlanningOutput{}, fmt.Errorf("failed to create LLM client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get planner spec
|
||||||
|
plannerSpec, exists := in.Config.RolePrompts["planner"]
|
||||||
|
if !exists {
|
||||||
|
return PlanningOutput{}, fmt.Errorf("planner role prompt not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render template
|
||||||
|
var templateContent string
|
||||||
|
if plannerSpec.RawTemplate != "" {
|
||||||
|
templateContent = plannerSpec.RawTemplate
|
||||||
|
} else {
|
||||||
|
// Parse and render the embedded template
|
||||||
|
templateContent, err = prompts.Render(plannerSpec.TemplateRef, map[string]any{
|
||||||
|
"SystemPrompt": in.Config.SystemPrompt,
|
||||||
|
"BoardState": in.BoardState,
|
||||||
|
"Milestone": in.Milestone,
|
||||||
|
"Config": in.Config,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return PlanningOutput{}, fmt.Errorf("failed to render planner template: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call LLM
|
||||||
|
messages := []llm.MessageParam{
|
||||||
|
{
|
||||||
|
Role: "user",
|
||||||
|
Content: templateContent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
||||||
|
Model: plannerSpec.Model,
|
||||||
|
SystemPrompt: in.Config.SystemPrompt,
|
||||||
|
Messages: messages,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return PlanningOutput{}, fmt.Errorf("planner LLM call failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, return empty dispatch (will be parsed from LLM response in full implementation)
|
||||||
|
// This is a stub that allows the test to verify the activity is called
|
||||||
|
_ = response
|
||||||
|
return PlanningOutput{
|
||||||
|
TasksToDispatch: []string{},
|
||||||
|
CompletedBranches: []string{},
|
||||||
|
SubmilestoneComplete: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package activity
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -10,11 +10,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.temporal.io/sdk/activity"
|
"go.temporal.io/sdk/activity"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrepareSkillsInput is input to PrepareSkillsActivity.
|
// PrepareSkillsInput is input to PrepareSkillsActivity.
|
||||||
type PrepareSkillsInput struct {
|
type PrepareSkillsInput struct {
|
||||||
Skills []SkillRef
|
Skills []statemachine.SkillRef
|
||||||
StreamTimeout time.Duration
|
StreamTimeout time.Duration
|
||||||
Provider string // pi provider name (e.g. "homelab-reasoning"); required, pi has no usable default provider
|
Provider string // pi provider name (e.g. "homelab-reasoning"); required, pi has no usable default provider
|
||||||
}
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FetchCanvasRelationsActivity fetches canvas + relations from DB
|
|
||||||
func FetchCanvasRelationsActivity(ctx context.Context, input FetchCanvasRelationsInput) (CanvasWithRelationsData, error) {
|
|
||||||
logger := newActivityLogger(ctx)
|
|
||||||
output := CanvasWithRelationsData{
|
|
||||||
WorkflowID: input.WorkflowID,
|
|
||||||
Version: input.Version,
|
|
||||||
Nodes: []db.WorkflowNode{},
|
|
||||||
Edges: []db.WorkflowEdge{},
|
|
||||||
Relations: []EdgeWithWording{},
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Fetching canvas relations: %s v%d", input.WorkflowID, input.Version)
|
|
||||||
|
|
||||||
// Get database client from context or activity manager
|
|
||||||
dbClient, ok := ctx.Value("db_client").(*db.DB)
|
|
||||||
if !ok {
|
|
||||||
return output, fmt.Errorf("database client not in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch workflow
|
|
||||||
workflow, err := dbClient.FetchWorkflow(ctx, input.WorkflowID, "")
|
|
||||||
if err != nil {
|
|
||||||
return output, fmt.Errorf("failed to get workflow: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse canvas nodes and edges
|
|
||||||
var nodes []db.WorkflowNode
|
|
||||||
if err := json.Unmarshal([]byte(workflow.Nodes), &nodes); err != nil {
|
|
||||||
return output, fmt.Errorf("failed to parse nodes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var edges []db.WorkflowEdge
|
|
||||||
if err := json.Unmarshal([]byte(workflow.Edges), &edges); err != nil {
|
|
||||||
return output, fmt.Errorf("failed to parse edges: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.Nodes = nodes
|
|
||||||
output.Edges = edges
|
|
||||||
output.UpdatedAt = workflow.UpdatedAt.String()
|
|
||||||
|
|
||||||
// Fetch workflow relations
|
|
||||||
relations, err := dbClient.GetWorkflowRelations(ctx, input.WorkflowID, input.Version)
|
|
||||||
if err != nil {
|
|
||||||
// Relations may not exist for old canvases - this is OK
|
|
||||||
logger.Warn("Failed to fetch relations: %v", err)
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map to EdgeWithWording
|
|
||||||
for _, rel := range relations {
|
|
||||||
edge := EdgeWithWording{
|
|
||||||
ID: rel.ID,
|
|
||||||
Source: rel.SourceNodeID,
|
|
||||||
Target: rel.TargetNodeID,
|
|
||||||
RelationType: rel.RelationType,
|
|
||||||
RelationLabel: rel.Label,
|
|
||||||
CreatedAt: rel.CreatedAt.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse relation wording JSON
|
|
||||||
if err := json.Unmarshal(rel.RelationWording, &edge.RelationWording); err != nil {
|
|
||||||
logger.Warn("Failed to parse relation wording: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.Relations = append(output.Relations, edge)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Fetched %d nodes, %d edges, %d relations", len(output.Nodes), len(output.Edges), len(output.Relations))
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
"go.temporal.io/sdk/activity"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ImplementerInput struct {
|
|
||||||
Config types.OrchestratorConfig
|
|
||||||
TaskID string
|
|
||||||
WorktreePath string
|
|
||||||
Lessons string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImplementerOutput struct {
|
|
||||||
Success bool
|
|
||||||
Changes string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ImplementerActivity(ctx context.Context, in ImplementerInput) (ImplementerOutput, error) {
|
|
||||||
activity.RecordHeartbeat(ctx, "starting implementer for "+in.TaskID)
|
|
||||||
|
|
||||||
vars := map[string]any{
|
|
||||||
"SystemPrompt": in.Config.SystemPrompt,
|
|
||||||
"Task": in.TaskID,
|
|
||||||
"WorktreePath": in.WorktreePath,
|
|
||||||
}
|
|
||||||
if in.Lessons != "" {
|
|
||||||
vars["Lessons"] = in.Lessons
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := CallRoleLLM(ctx, in.Config, "implementer", vars)
|
|
||||||
if err != nil {
|
|
||||||
return ImplementerOutput{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.RecordHeartbeat(ctx, "implementer completed for "+in.TaskID)
|
|
||||||
return ImplementerOutput{Success: true, Changes: response}, nil
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IndexGraphRAGActivity indexes workflow canvas to GraphRAG (stub for now)
|
|
||||||
func IndexGraphRAGActivity(ctx context.Context, input IndexGraphRAGInput) (IndexGraphRAGOutput, error) {
|
|
||||||
output := IndexGraphRAGOutput{
|
|
||||||
WorkflowID: input.WorkflowID,
|
|
||||||
Version: input.Version,
|
|
||||||
Status: "indexed",
|
|
||||||
IndexedEntities: len(input.Nodes),
|
|
||||||
IndexedEdges: len(input.Relations),
|
|
||||||
IndexedAt: time.Now().UTC().Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stub implementation - actual GraphRAG indexing would happen here
|
|
||||||
// For now, just return success
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryGraphRAGRelationsInput for direct relation discovery
|
|
||||||
type QueryGraphRAGRelationsInput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
Query string `json:"query"`
|
|
||||||
TopK int `json:"top_k"`
|
|
||||||
Filters map[string]interface{} `json:"filters,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryGraphRAGRelationsOutput returns discovered relations
|
|
||||||
type QueryGraphRAGRelationsOutput struct {
|
|
||||||
Query string `json:"query"`
|
|
||||||
Results []EdgeWithWording `json:"results"`
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
ExecutionMs int64 `json:"execution_time_ms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryGraphRAGRelationsActivity queries GraphRAG for relation patterns (stub)
|
|
||||||
func QueryGraphRAGRelationsActivity(ctx context.Context, input QueryGraphRAGRelationsInput) (QueryGraphRAGRelationsOutput, error) {
|
|
||||||
output := QueryGraphRAGRelationsOutput{
|
|
||||||
Query: input.Query,
|
|
||||||
Results: []EdgeWithWording{},
|
|
||||||
TotalCount: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stub implementation - actual GraphRAG querying would happen here
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JudgeInput struct {
|
|
||||||
Config types.OrchestratorConfig
|
|
||||||
Diff string
|
|
||||||
IntegrationTestLogs string
|
|
||||||
}
|
|
||||||
|
|
||||||
type JudgeOutput struct {
|
|
||||||
Verdict string
|
|
||||||
Critique string
|
|
||||||
}
|
|
||||||
|
|
||||||
func JudgeActivity(ctx context.Context, in JudgeInput) (JudgeOutput, error) {
|
|
||||||
response, err := CallRoleLLM(ctx, in.Config, "judge", map[string]any{
|
|
||||||
"SystemPrompt": in.Config.SystemPrompt,
|
|
||||||
"Diff": in.Diff,
|
|
||||||
"TestResult": in.IntegrationTestLogs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return JudgeOutput{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: parse LLM response for verdict
|
|
||||||
return JudgeOutput{Verdict: "pass", Critique: response}, nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/activity/llm"
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
"github.com/rockliang/poimen/workflows/prompts"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CallRoleLLM is the shared pattern for calling an LLM with a role-based prompt.
|
|
||||||
// Used by planner, implementer, and judge activities (DRY extraction).
|
|
||||||
func CallRoleLLM(ctx context.Context, config types.OrchestratorConfig, role string, vars map[string]any) (string, error) {
|
|
||||||
client, err := llm.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create LLM client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spec, exists := config.RolePrompts[role]
|
|
||||||
if !exists {
|
|
||||||
return "", fmt.Errorf("%s role prompt not configured", role)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render template
|
|
||||||
var content string
|
|
||||||
if spec.RawTemplate != "" {
|
|
||||||
content = spec.RawTemplate
|
|
||||||
} else {
|
|
||||||
content, err = prompts.Render(spec.TemplateRef, vars)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to render %s template: %w", role, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call LLM
|
|
||||||
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
|
||||||
Model: spec.Model,
|
|
||||||
SystemPrompt: config.SystemPrompt,
|
|
||||||
Messages: []llm.MessageParam{{Role: "user", Content: content}},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s LLM call failed: %w", role, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/activity/llm"
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LLMInferenceInput struct {
|
|
||||||
Model string `json:"model"`
|
|
||||||
SystemPrompt string `json:"system_prompt"`
|
|
||||||
UserPrompt string `json:"user_prompt"`
|
|
||||||
Temperature float64 `json:"temperature,omitempty"`
|
|
||||||
MaxTokens int `json:"max_tokens,omitempty"`
|
|
||||||
AuthToken string `json:"auth_token,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LLMInferenceOutput struct {
|
|
||||||
Response string `json:"response"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
StopReason string `json:"stop_reason"`
|
|
||||||
TokensUsed int `json:"tokens_used"`
|
|
||||||
ErrorMessage string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func LLMInferenceActivity(ctx context.Context, in LLMInferenceInput) (LLMInferenceOutput, error) {
|
|
||||||
logger := newActivityLogger(ctx)
|
|
||||||
output := LLMInferenceOutput{Model: in.Model}
|
|
||||||
|
|
||||||
if in.Model == "" {
|
|
||||||
return output, fmt.Errorf("model not specified")
|
|
||||||
}
|
|
||||||
if in.UserPrompt == "" {
|
|
||||||
return output, fmt.Errorf("user_prompt not specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Starting LLM inference", "model", in.Model)
|
|
||||||
|
|
||||||
client, err := llm.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
output.ErrorMessage = err.Error()
|
|
||||||
return output, fmt.Errorf("failed to create LLM client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
|
||||||
Model: types.ModelSpec{ModelID: in.Model},
|
|
||||||
SystemPrompt: in.SystemPrompt,
|
|
||||||
Messages: []llm.MessageParam{{Role: "user", Content: in.UserPrompt}},
|
|
||||||
AuthToken: in.AuthToken,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
output.ErrorMessage = err.Error()
|
|
||||||
logger.Warn("LLM API call failed", "error", err)
|
|
||||||
return output, fmt.Errorf("LLM inference failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.Response = response
|
|
||||||
output.StopReason = "stop_sequence"
|
|
||||||
logger.Info("LLM inference completed", "response_len", len(response))
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type LLMBatchInferenceInput struct {
|
|
||||||
Model string `json:"model"`
|
|
||||||
SystemPrompt string `json:"system_prompt"`
|
|
||||||
Prompts []string `json:"prompts"`
|
|
||||||
Temperature float64 `json:"temperature,omitempty"`
|
|
||||||
AuthToken string `json:"auth_token,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LLMBatchInferenceOutput struct {
|
|
||||||
Responses []string `json:"responses"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Errors []string `json:"errors,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func LLMBatchInferenceActivity(ctx context.Context, in LLMBatchInferenceInput) (LLMBatchInferenceOutput, error) {
|
|
||||||
logger := newActivityLogger(ctx)
|
|
||||||
output := LLMBatchInferenceOutput{Model: in.Model, Responses: []string{}, Errors: []string{}}
|
|
||||||
|
|
||||||
if in.Model == "" {
|
|
||||||
return output, fmt.Errorf("model not specified")
|
|
||||||
}
|
|
||||||
if len(in.Prompts) == 0 {
|
|
||||||
return output, fmt.Errorf("no prompts provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Starting batch inference", "model", in.Model, "count", len(in.Prompts))
|
|
||||||
|
|
||||||
client, err := llm.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
return output, fmt.Errorf("failed to create LLM client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, prompt := range in.Prompts {
|
|
||||||
response, err := client.CreateMessage(ctx, llm.MessageInput{
|
|
||||||
Model: types.ModelSpec{ModelID: in.Model},
|
|
||||||
SystemPrompt: in.SystemPrompt,
|
|
||||||
Messages: []llm.MessageParam{{Role: "user", Content: prompt}},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
output.Errors = append(output.Errors, fmt.Sprintf("prompt %d: %v", i, err))
|
|
||||||
output.Responses = append(output.Responses, "")
|
|
||||||
logger.Warn("Failed prompt", "index", i, "error", err)
|
|
||||||
} else {
|
|
||||||
output.Responses = append(output.Responses, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Batch inference completed", "responses", len(output.Responses), "errors", len(output.Errors))
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PlanningInput struct {
|
|
||||||
Config types.OrchestratorConfig
|
|
||||||
BoardState string
|
|
||||||
RepoPath string
|
|
||||||
Milestone string
|
|
||||||
TaskResults []types.TaskUnitOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskDispatch struct {
|
|
||||||
TaskID string
|
|
||||||
PromptSpec types.PromptSpec
|
|
||||||
BaseTimeout *int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlanningOutput struct {
|
|
||||||
TasksToDispatch []string
|
|
||||||
CompletedBranches []string
|
|
||||||
SubmilestoneComplete bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func PlanningActivity(ctx context.Context, in PlanningInput) (PlanningOutput, error) {
|
|
||||||
response, err := CallRoleLLM(ctx, in.Config, "planner", map[string]any{
|
|
||||||
"SystemPrompt": in.Config.SystemPrompt,
|
|
||||||
"BoardState": in.BoardState,
|
|
||||||
"Milestone": in.Milestone,
|
|
||||||
"Config": in.Config,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return PlanningOutput{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: parse LLM response into task dispatch list
|
|
||||||
_ = response
|
|
||||||
return PlanningOutput{
|
|
||||||
TasksToDispatch: []string{},
|
|
||||||
CompletedBranches: []string{},
|
|
||||||
SubmilestoneComplete: false,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QueryGraphRAGActivity queries Memory System for semantic relations
|
|
||||||
func QueryGraphRAGActivity(ctx context.Context, input GraphRAGQueryInput) (GraphRAGQueryOutput, error) {
|
|
||||||
logger := newActivityLogger(ctx)
|
|
||||||
output := GraphRAGQueryOutput{
|
|
||||||
WorkflowID: input.WorkflowID,
|
|
||||||
Query: input.Query,
|
|
||||||
Edges: []EdgeWithWording{},
|
|
||||||
Paths: []QueryPathData{},
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Querying GraphRAG: %s", input.Query)
|
|
||||||
|
|
||||||
// Get Memory Service URL from env
|
|
||||||
memoryURL := os.Getenv("MEMORY_SERVICE_URL")
|
|
||||||
if memoryURL == "" {
|
|
||||||
memoryURL = "http://localhost:8000"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build payload for Memory System
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"workflow_id": input.WorkflowID,
|
|
||||||
"query": input.Query,
|
|
||||||
"search_type": input.SearchType,
|
|
||||||
"relation_type": input.RelationType,
|
|
||||||
"confidence_floor": input.ConfidenceFloor,
|
|
||||||
"top_k": input.TopK,
|
|
||||||
"ranking_profile": input.RankingProfile,
|
|
||||||
"canvas_nodes": input.Canvas.Nodes,
|
|
||||||
"canvas_edges": input.Canvas.Edges,
|
|
||||||
"relations": input.Canvas.Relations,
|
|
||||||
}
|
|
||||||
|
|
||||||
reqBody, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return output, fmt.Errorf("failed to marshal payload: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Memory System unified query endpoint
|
|
||||||
req, err := http.NewRequestWithContext(
|
|
||||||
ctx,
|
|
||||||
"POST",
|
|
||||||
memoryURL+"/workflows/query",
|
|
||||||
bytes.NewReader(reqBody),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return output, fmt.Errorf("failed to create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
if token := ctx.Value("jwt_token"); token != nil {
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return output, fmt.Errorf("failed to call Memory Service: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return output, fmt.Errorf("Memory Service returned %d: %s", resp.StatusCode, string(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse response
|
|
||||||
var graphResp struct {
|
|
||||||
Edges []EdgeWithWording `json:"edges"`
|
|
||||||
Paths []QueryPathData `json:"paths"`
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&graphResp); err != nil {
|
|
||||||
return output, fmt.Errorf("failed to decode response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.Edges = graphResp.Edges
|
|
||||||
output.Paths = graphResp.Paths
|
|
||||||
output.TotalCount = graphResp.TotalCount
|
|
||||||
output.HasMore = graphResp.HasMore
|
|
||||||
output.ExecutionMs = time.Since(startTime).Milliseconds()
|
|
||||||
|
|
||||||
logger.Info("GraphRAG returned %d edges, %d paths in %dms",
|
|
||||||
len(output.Edges), len(output.Paths), output.ExecutionMs)
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package activity
|
|
||||||
|
|
||||||
import "github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
|
|
||||||
// Re-export from pkg/types for convenience within activity package.
|
|
||||||
type ModelSpec = types.ModelSpec
|
|
||||||
type PromptSpec = types.PromptSpec
|
|
||||||
type SkillRef = types.SkillRef
|
|
||||||
type OrchestratorConfig = types.OrchestratorConfig
|
|
||||||
type TaskUnitOutput = types.TaskUnitOutput
|
|
||||||
type EdgeWithWording = types.EdgeWithWording
|
|
||||||
type RelationWording = types.RelationWording
|
|
||||||
type CanvasWithRelationsData = types.CanvasWithRelationsData
|
|
||||||
type FetchCanvasRelationsInput = types.FetchCanvasRelationsInput
|
|
||||||
type CanvasReasonerInput = types.CanvasReasonerInput
|
|
||||||
type GraphRAGQueryInput = types.GraphRAGQueryInput
|
|
||||||
type GraphRAGQueryOutput = types.GraphRAGQueryOutput
|
|
||||||
type QueryPathData = types.QueryPathData
|
|
||||||
type CanvasCompatibilityInput = types.CanvasCompatibilityInput
|
|
||||||
type IndexGraphRAGInput = types.IndexGraphRAGInput
|
|
||||||
type IndexGraphRAGOutput = types.IndexGraphRAGOutput
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"go.temporal.io/sdk/client"
|
||||||
|
"go.temporal.io/sdk/worker"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/action"
|
||||||
|
"github.com/rockliang/poimen/workflows/internal/api"
|
||||||
|
"github.com/rockliang/poimen/workflows/internal/config"
|
||||||
|
"github.com/rockliang/poimen/workflows/pkg/db"
|
||||||
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
apiPort = flag.Int("port", 8080, "HTTP API port")
|
||||||
|
verbose = flag.Bool("verbose", false, "verbose logging")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
logger := log.New(os.Stdout, "[poimen-server] ", log.LstdFlags|log.Lshortfile)
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to database (memory-db via K8s CNPG)
|
||||||
|
logger.Println("connecting to database...")
|
||||||
|
database, err := db.New(os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
logger.Println("✓ Connected to database")
|
||||||
|
|
||||||
|
// Connect to Temporal
|
||||||
|
logger.Printf("connecting to Temporal at %s", cfg.Temporal.HostPort)
|
||||||
|
c, err := client.Dial(client.Options{
|
||||||
|
HostPort: cfg.Temporal.HostPort,
|
||||||
|
Namespace: cfg.Temporal.Namespace,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("failed to connect to temporal: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
logger.Println("✓ Connected to Temporal")
|
||||||
|
|
||||||
|
// Create and start Temporal worker
|
||||||
|
w := worker.New(c, "default", worker.Options{})
|
||||||
|
|
||||||
|
// Register RoutingWorkflow
|
||||||
|
w.RegisterWorkflow(statemachine.RoutingWorkflow)
|
||||||
|
|
||||||
|
// Register activities
|
||||||
|
w.RegisterActivity(action.CloneRepoActivity)
|
||||||
|
w.RegisterActivity(action.AnalyzeCodeActivity)
|
||||||
|
w.RegisterActivity(action.SecurityScanActivity)
|
||||||
|
w.RegisterActivity(action.GenerateReportActivity)
|
||||||
|
w.RegisterActivity(action.DeploymentPreCheckActivity)
|
||||||
|
w.RegisterActivity(action.NotifyStatusActivity)
|
||||||
|
w.RegisterActivity(action.ApproveWorkflowActivity)
|
||||||
|
w.RegisterActivity(action.ArchiveResultsActivity)
|
||||||
|
w.RegisterActivity(action.RetrieveMemoryActivity)
|
||||||
|
w.RegisterActivity(action.AssumeRoleActivity)
|
||||||
|
w.RegisterActivity(action.LLMInferenceActivity)
|
||||||
|
w.RegisterActivity(action.LLMBatchInferenceActivity)
|
||||||
|
w.RegisterActivity(action.CanvasReasonerActivity)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errChan := make(chan error, 2)
|
||||||
|
|
||||||
|
// Start Temporal worker
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
logger.Println("starting Temporal worker...")
|
||||||
|
if err := w.Run(worker.InterruptCh()); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start HTTP API server
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
server := api.NewServer(database, c, logger)
|
||||||
|
logger.Printf("starting API server on port %d", *apiPort)
|
||||||
|
if err := server.Start(*apiPort); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for interrupt signal
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sig := <-sigChan
|
||||||
|
logger.Printf("received signal: %v", sig)
|
||||||
|
w.Stop()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Monitor for errors
|
||||||
|
go func() {
|
||||||
|
err := <-errChan
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("error: %v", err)
|
||||||
|
w.Stop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
logger.Println("✓ Server stopped gracefully")
|
||||||
|
}
|
||||||
+15
-15
@@ -11,12 +11,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.temporal.io/sdk/client"
|
"go.temporal.io/sdk/client"
|
||||||
"github.com/rockliang/poimen/workflows/activity/llm"
|
"github.com/rockliang/poimen/workflows/action/llm"
|
||||||
"github.com/rockliang/poimen/workflows/internal/config"
|
"github.com/rockliang/poimen/workflows/internal/config"
|
||||||
"github.com/rockliang/poimen/workflows/internal/health"
|
"github.com/rockliang/poimen/workflows/internal/health"
|
||||||
"github.com/rockliang/poimen/workflows/internal/logging"
|
"github.com/rockliang/poimen/workflows/internal/logging"
|
||||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||||
"github.com/rockliang/poimen/workflows/workflow"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -89,20 +89,20 @@ func main() {
|
|||||||
|
|
||||||
|
|
||||||
// Build OrchestratorInput
|
// Build OrchestratorInput
|
||||||
input := workflow.OrchestratorInput{
|
input := statemachine.OrchestratorInput{
|
||||||
TargetRepoPath: *repoPath,
|
TargetRepoPath: *repoPath,
|
||||||
RemoteURL: *remoteURL,
|
RemoteURL: *remoteURL,
|
||||||
Milestone: *milestone,
|
Milestone: *milestone,
|
||||||
DryRun: *dryRun,
|
DryRun: *dryRun,
|
||||||
MaxCyclesBeforeCAN: 100,
|
MaxCyclesBeforeCAN: 100,
|
||||||
PiProvider: *piProvider,
|
PiProvider: *piProvider,
|
||||||
Config: workflow.OrchestratorConfig{
|
Config: statemachine.OrchestratorConfig{
|
||||||
SystemPrompt: "You are an expert software developer orchestrating multi-agent work.",
|
SystemPrompt: "You are an expert software developer orchestrating multi-agent work.",
|
||||||
Skills: []workflow.SkillRef{},
|
Skills: []statemachine.SkillRef{},
|
||||||
RolePrompts: map[string]workflow.PromptSpec{
|
RolePrompts: map[string]statemachine.PromptSpec{
|
||||||
"planner": {
|
"planner": {
|
||||||
TemplateRef: "planner/default.tmpl",
|
TemplateRef: "planner/default.tmpl",
|
||||||
Model: workflow.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: *plannerModel,
|
ModelID: *plannerModel,
|
||||||
Thinking: "adaptive",
|
Thinking: "adaptive",
|
||||||
Effort: "high",
|
Effort: "high",
|
||||||
@@ -110,7 +110,7 @@ func main() {
|
|||||||
},
|
},
|
||||||
"judge": {
|
"judge": {
|
||||||
TemplateRef: "judge/default.tmpl",
|
TemplateRef: "judge/default.tmpl",
|
||||||
Model: workflow.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: *judgeModel,
|
ModelID: *judgeModel,
|
||||||
Thinking: "adaptive",
|
Thinking: "adaptive",
|
||||||
Effort: "high",
|
Effort: "high",
|
||||||
@@ -118,12 +118,12 @@ func main() {
|
|||||||
},
|
},
|
||||||
"implementer": {
|
"implementer": {
|
||||||
TemplateRef: "implementer/default.tmpl",
|
TemplateRef: "implementer/default.tmpl",
|
||||||
Model: workflow.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: *implementerModel,
|
ModelID: *implementerModel,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Tuning: workflow.NewActivityTuning(),
|
Tuning: statemachine.NewActivityTuning(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ func main() {
|
|||||||
run, err := c.ExecuteWorkflow(context.Background(), client.StartWorkflowOptions{
|
run, err := c.ExecuteWorkflow(context.Background(), client.StartWorkflowOptions{
|
||||||
ID: workflowID,
|
ID: workflowID,
|
||||||
TaskQueue: "poimen-taskqueue",
|
TaskQueue: "poimen-taskqueue",
|
||||||
}, workflow.OrchestratorWorkflow, input)
|
}, statemachine.OrchestratorWorkflow, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Fatal("failed to start workflow", logging.Err(err))
|
logging.Fatal("failed to start workflow", logging.Err(err))
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ func main() {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var result workflow.OrchestratorOutput
|
var result statemachine.OrchestratorOutput
|
||||||
if err := run.Get(ctx, &result); err != nil {
|
if err := run.Get(ctx, &result); err != nil {
|
||||||
fmt.Printf("\nWorkflow initiated (execution in progress).\n")
|
fmt.Printf("\nWorkflow initiated (execution in progress).\n")
|
||||||
fmt.Printf("Check the Web UI for real-time status updates.\n")
|
fmt.Printf("Check the Web UI for real-time status updates.\n")
|
||||||
@@ -267,12 +267,12 @@ func runRoutingWorkflow(c client.Client, routeMsg, specFile string, isCron, dryR
|
|||||||
}
|
}
|
||||||
|
|
||||||
workflowID := "routing-" + spec.Name + "-" + time.Now().Format("20060102-150405")
|
workflowID := "routing-" + spec.Name + "-" + time.Now().Format("20060102-150405")
|
||||||
input := workflow.RoutingWorkflowInput{Spec: spec}
|
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||||
|
|
||||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||||
ID: workflowID,
|
ID: workflowID,
|
||||||
TaskQueue: "poimen-taskqueue",
|
TaskQueue: "poimen-taskqueue",
|
||||||
}, workflow.RoutingWorkflow, input)
|
}, statemachine.RoutingWorkflow, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Fatal("failed to start routing workflow", logging.Err(err))
|
logging.Fatal("failed to start routing workflow", logging.Err(err))
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ func runRoutingWorkflow(c client.Client, routeMsg, specFile string, isCron, dryR
|
|||||||
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var result workflow.RoutingWorkflowOutput
|
var result statemachine.RoutingWorkflowOutput
|
||||||
if err := run.Get(waitCtx, &result); err != nil {
|
if err := run.Get(waitCtx, &result); err != nil {
|
||||||
fmt.Printf("\nWorkflow running (check Temporal UI for status)\n")
|
fmt.Printf("\nWorkflow running (check Temporal UI for status)\n")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+32
-40
@@ -11,11 +11,11 @@ import (
|
|||||||
|
|
||||||
"go.temporal.io/sdk/client"
|
"go.temporal.io/sdk/client"
|
||||||
"go.temporal.io/sdk/worker"
|
"go.temporal.io/sdk/worker"
|
||||||
"github.com/rockliang/poimen/workflows/activity"
|
"github.com/rockliang/poimen/workflows/action"
|
||||||
"github.com/rockliang/poimen/workflows/internal/config"
|
"github.com/rockliang/poimen/workflows/internal/config"
|
||||||
"github.com/rockliang/poimen/workflows/internal/health"
|
"github.com/rockliang/poimen/workflows/internal/health"
|
||||||
"github.com/rockliang/poimen/workflows/internal/logging"
|
"github.com/rockliang/poimen/workflows/internal/logging"
|
||||||
"github.com/rockliang/poimen/workflows/workflow"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -48,56 +48,48 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register all workflows
|
// Register all workflows
|
||||||
w.RegisterWorkflow(workflow.OrchestratorWorkflow)
|
w.RegisterWorkflow(statemachine.OrchestratorWorkflow)
|
||||||
w.RegisterWorkflow(workflow.TaskUnitWorkflow)
|
w.RegisterWorkflow(statemachine.TaskUnitWorkflow)
|
||||||
w.RegisterWorkflow(workflow.TestWorkflow)
|
w.RegisterWorkflow(statemachine.TestWorkflow)
|
||||||
w.RegisterWorkflow(workflow.RoutingWorkflow)
|
w.RegisterWorkflow(statemachine.RoutingWorkflow)
|
||||||
w.RegisterWorkflow(workflow.WorkflowGraphQuery)
|
|
||||||
|
|
||||||
// Register all activities
|
// Register all activities
|
||||||
w.RegisterActivity(activity.CloneRepoActivity)
|
w.RegisterActivity(action.CloneRepoActivity)
|
||||||
w.RegisterActivity(activity.GitWorktreeAddActivity)
|
w.RegisterActivity(action.GitWorktreeAddActivity)
|
||||||
w.RegisterActivity(activity.GitCommitActivity)
|
w.RegisterActivity(action.GitCommitActivity)
|
||||||
w.RegisterActivity(activity.GitPushActivity)
|
w.RegisterActivity(action.GitPushActivity)
|
||||||
w.RegisterActivity(activity.GitSquashMergeActivity)
|
w.RegisterActivity(action.GitSquashMergeActivity)
|
||||||
w.RegisterActivity(activity.GitDiffActivity)
|
w.RegisterActivity(action.GitDiffActivity)
|
||||||
w.RegisterActivity(activity.PrepareSkillsActivity)
|
w.RegisterActivity(action.PrepareSkillsActivity)
|
||||||
w.RegisterActivity(activity.PlanningActivity)
|
w.RegisterActivity(action.PlanningActivity)
|
||||||
w.RegisterActivity(activity.ImplementerActivity)
|
w.RegisterActivity(action.ImplementerActivity)
|
||||||
w.RegisterActivity(activity.JudgeActivity)
|
w.RegisterActivity(action.JudgeActivity)
|
||||||
// Integration and lessons activities - register when fully tested
|
// Integration and lessons activities - register when fully tested
|
||||||
w.RegisterActivity(activity.RunIntegrationTestActivity)
|
w.RegisterActivity(action.RunIntegrationTestActivity)
|
||||||
// w.RegisterActivity(activity.UpdateLessonsActivity)
|
// w.RegisterActivity(action.UpdateLessonsActivity)
|
||||||
// w.RegisterActivity(activity.ReadLessonsActivity)
|
// w.RegisterActivity(action.ReadLessonsActivity)
|
||||||
|
|
||||||
// Routing workflow activities
|
// Routing workflow activities
|
||||||
w.RegisterActivity(activity.LLMRouterActivity)
|
w.RegisterActivity(action.LLMRouterActivity)
|
||||||
w.RegisterActivity(activity.ValidateWorkflowSpecActivity)
|
w.RegisterActivity(action.ValidateWorkflowSpecActivity)
|
||||||
w.RegisterActivity(activity.ValidateCronWorkflowSpecActivity)
|
w.RegisterActivity(action.ValidateCronWorkflowSpecActivity)
|
||||||
|
|
||||||
// Analysis activities
|
// Analysis activities
|
||||||
w.RegisterActivity(activity.AnalyzeCodeActivity)
|
w.RegisterActivity(action.AnalyzeCodeActivity)
|
||||||
w.RegisterActivity(activity.SecurityScanActivity)
|
w.RegisterActivity(action.SecurityScanActivity)
|
||||||
w.RegisterActivity(activity.GenerateReportActivity)
|
w.RegisterActivity(action.GenerateReportActivity)
|
||||||
|
|
||||||
// Notification and utility activities
|
// Notification and utility activities
|
||||||
w.RegisterActivity(activity.NotifyStatusActivity)
|
w.RegisterActivity(action.NotifyStatusActivity)
|
||||||
w.RegisterActivity(activity.ArchiveResultsActivity)
|
w.RegisterActivity(action.ArchiveResultsActivity)
|
||||||
w.RegisterActivity(activity.DeploymentPreCheckActivity)
|
w.RegisterActivity(action.DeploymentPreCheckActivity)
|
||||||
w.RegisterActivity(activity.ApproveWorkflowActivity)
|
w.RegisterActivity(action.ApproveWorkflowActivity)
|
||||||
|
|
||||||
// Authentication activities
|
// Authentication activities
|
||||||
w.RegisterActivity(activity.AssumeRoleActivity)
|
w.RegisterActivity(action.AssumeRoleActivity)
|
||||||
|
|
||||||
// Memory activities
|
// Memory activities
|
||||||
w.RegisterActivity(activity.RetrieveMemoryActivity)
|
w.RegisterActivity(action.RetrieveMemoryActivity)
|
||||||
|
|
||||||
// GraphRAG activities
|
|
||||||
w.RegisterActivity(activity.FetchCanvasRelationsActivity)
|
|
||||||
w.RegisterActivity(activity.QueryGraphRAGActivity)
|
|
||||||
w.RegisterActivity(activity.CanvasReasonerActivity)
|
|
||||||
w.RegisterActivity(activity.IndexGraphRAGActivity)
|
|
||||||
w.RegisterActivity(activity.CanvasCompatibilityActivity)
|
|
||||||
|
|
||||||
// Initialize health checker
|
// Initialize health checker
|
||||||
healthChecker := health.NewChecker(c)
|
healthChecker := health.NewChecker(c)
|
||||||
@@ -127,7 +119,7 @@ func main() {
|
|||||||
// Run worker in a goroutine
|
// Run worker in a goroutine
|
||||||
workerErrChan := make(chan error, 1)
|
workerErrChan := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
logging.Info("starting worker", logging.String("queue", "poimen"))
|
logging.Info("starting worker on queue", logging.String("queue", "poimen-taskqueue"))
|
||||||
if err := w.Run(worker.InterruptCh()); err != nil {
|
if err := w.Run(worker.InterruptCh()); err != nil {
|
||||||
workerErrChan <- err
|
workerErrChan <- err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,322 @@
|
|||||||
|
# Canvas Reasoner: Auto-Inferring Workflow Connections
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The **CanvasReasonerActivity** uses LLM reasoning to automatically suggest connections between workflow activities when users drop new nodes onto the canvas. It analyzes input/output compatibility and detects connection problems.
|
||||||
|
|
||||||
|
## Connection Logic
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Analyze Node Schemas**
|
||||||
|
- Get each activity's input/output fields from knowledge base
|
||||||
|
- Activities are classified as:
|
||||||
|
- **Generators**: No inputs, has outputs (e.g., API call, trigger)
|
||||||
|
- **Processors**: Has inputs and outputs (e.g., analyze code, security scan)
|
||||||
|
- **Sinks/Terminals**: Has inputs, no outputs (e.g., notification, approval)
|
||||||
|
|
||||||
|
2. **LLM Reasoning**
|
||||||
|
- Pass all nodes + their schemas to reasoning model
|
||||||
|
- Ask LLM to suggest edges based on:
|
||||||
|
- Type compatibility (string→string, object→object)
|
||||||
|
- Logical execution order
|
||||||
|
- Data flow requirements
|
||||||
|
- Common workflow patterns
|
||||||
|
|
||||||
|
3. **Validate Suggestions**
|
||||||
|
- Check all suggested edges exist in node map
|
||||||
|
- Skip self-loops
|
||||||
|
- Remove duplicates
|
||||||
|
|
||||||
|
4. **Compatibility Checking**
|
||||||
|
- For each suggested edge: `source → target`
|
||||||
|
- Verify source produces outputs
|
||||||
|
- Verify target accepts inputs
|
||||||
|
- Check output/input type compatibility
|
||||||
|
- Flag incompatible connections
|
||||||
|
|
||||||
|
5. **Identify Issues**
|
||||||
|
- Collect all incompatible edges
|
||||||
|
- Identify disconnected nodes (no edges in/out)
|
||||||
|
- Generate user alerts for problems
|
||||||
|
|
||||||
|
## Connection Impossibility Detection
|
||||||
|
|
||||||
|
### Why Connections Fail
|
||||||
|
|
||||||
|
1. **Missing Outputs**
|
||||||
|
```
|
||||||
|
NotifyStatusActivity → AnalyzeCodeActivity
|
||||||
|
⚠️ NotifyStatusActivity produces no outputs
|
||||||
|
Reason: Notification is terminal activity (sink)
|
||||||
|
Solution: Add an intermediate processor that has outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Missing Inputs**
|
||||||
|
```
|
||||||
|
CloneRepoActivity → ApproveWorkflowActivity
|
||||||
|
⚠️ ApproveWorkflowActivity accepts no inputs
|
||||||
|
Reason: Approval is a terminal activity (sink)
|
||||||
|
Solution: ApproveWorkflowActivity only works as final step
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Type Mismatch**
|
||||||
|
```
|
||||||
|
LLMInferenceActivity (output: string) → DeploymentPreCheckActivity (input: object)
|
||||||
|
⚠️ String output cannot satisfy object input requirement
|
||||||
|
Reason: Incompatible data types
|
||||||
|
Solution: Use LLM transformation node to convert string→object
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Semantic Incompatibility**
|
||||||
|
```
|
||||||
|
NotifyStatusActivity → CloneRepoActivity
|
||||||
|
⚠️ No logical connection between these activities
|
||||||
|
Reason: Notification cannot be input to clone operation
|
||||||
|
Solution: Ensure data flow makes semantic sense
|
||||||
|
```
|
||||||
|
|
||||||
|
### Incompatibility Data Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"incompatible_edges": [
|
||||||
|
{
|
||||||
|
"source": "node-1",
|
||||||
|
"target": "node-2",
|
||||||
|
"reason": "Source activity produces no outputs",
|
||||||
|
"source_needs": "any output",
|
||||||
|
"target_needs": "path, depth",
|
||||||
|
"suggestion": "Use LLM transformation to map outputs to inputs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"disconnected_nodes": ["node-5", "node-8"],
|
||||||
|
"user_alerts": [
|
||||||
|
"⚠️ node-1 → node-2: Source activity produces no outputs. Use LLM transformation to map outputs to inputs",
|
||||||
|
"🔌 Node 'NotifyStatus-1' has no connections. Consider adding edges or removing it."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Alerts
|
||||||
|
|
||||||
|
### Alert Types
|
||||||
|
|
||||||
|
1. **Incompatibility Warnings** (⚠️)
|
||||||
|
```
|
||||||
|
⚠️ source → target: reason. suggestion.
|
||||||
|
```
|
||||||
|
- Highlighted in red on canvas
|
||||||
|
- Shows in error sidebar
|
||||||
|
- Prevents workflow execution until fixed
|
||||||
|
|
||||||
|
2. **Disconnection Warnings** (🔌)
|
||||||
|
```
|
||||||
|
🔌 Node 'label' has no connections. Consider adding edges or removing it.
|
||||||
|
```
|
||||||
|
- Highlighted in yellow
|
||||||
|
- Nodes with no input/output edges
|
||||||
|
- May be valid (first step, last step) or indicate design error
|
||||||
|
|
||||||
|
3. **Type Mismatch Info** (ℹ️)
|
||||||
|
```
|
||||||
|
ℹ️ To connect source → target, use transformer to map: {source_outputs} → {target_inputs}
|
||||||
|
```
|
||||||
|
- Suggestion to use intermediate LLM node
|
||||||
|
- Provides mapping information
|
||||||
|
|
||||||
|
## Frontend Integration
|
||||||
|
|
||||||
|
### Canvas UI Feedback
|
||||||
|
|
||||||
|
When CanvasReasonerActivity returns incompatibilities:
|
||||||
|
|
||||||
|
1. **Visual Markers**
|
||||||
|
- Incompatible suggested edges: ❌ red dashed line (don't auto-add)
|
||||||
|
- Disconnected nodes: ⚠️ yellow border
|
||||||
|
|
||||||
|
2. **Sidebar Alerts**
|
||||||
|
```
|
||||||
|
🚨 Connection Issues (3)
|
||||||
|
|
||||||
|
⚠️ CloneRepo → ApproveWorkflow
|
||||||
|
Reason: ApproveWorkflow is terminal (no outputs)
|
||||||
|
Suggestion: Place ApproveWorkflow at end of workflow
|
||||||
|
|
||||||
|
⚠️ LLMInference → DeploymentPreCheck
|
||||||
|
Reason: Type mismatch (string ≠ object)
|
||||||
|
Suggestion: Add LLM transformation node
|
||||||
|
|
||||||
|
🔌 SecurityScan-1 has no incoming edges
|
||||||
|
Suggestion: Connect CloneRepo → SecurityScan
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **User Actions**
|
||||||
|
- ✅ Accept suggestions (green edges)
|
||||||
|
- ❌ Reject incompatible edges
|
||||||
|
- 🔧 Add transformer nodes
|
||||||
|
- 🗑️ Remove disconnected nodes
|
||||||
|
|
||||||
|
### API Response Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"suggested_edges": [
|
||||||
|
{"source": "clone-1", "target": "analyze-1"},
|
||||||
|
{"source": "analyze-1", "target": "security-1"},
|
||||||
|
{"source": "security-1", "target": "report-1"}
|
||||||
|
],
|
||||||
|
"reasoning": "Standard code review workflow: clone → analyze → scan → report",
|
||||||
|
"confidence": 0.92,
|
||||||
|
"incompatible_edges": [
|
||||||
|
{
|
||||||
|
"source": "report-1",
|
||||||
|
"target": "approve-1",
|
||||||
|
"reason": "ReportGenerator has no outputs (terminal activity)",
|
||||||
|
"suggestion": "ApproveWorkflow can only be a final step"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"disconnected_nodes": [],
|
||||||
|
"user_alerts": [
|
||||||
|
"⚠️ report-1 → approve-1: ReportGenerator has no outputs (terminal activity). ApproveWorkflow can only be a final step"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Knowledge Base Schema
|
||||||
|
|
||||||
|
Each activity in `activity_knowledge_base.json` defines:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "CloneRepoActivity",
|
||||||
|
"inputs": {
|
||||||
|
"repo": {"type": "string", "required": true},
|
||||||
|
"branch": {"type": "string", "required": false}
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"commit": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Classification Rules
|
||||||
|
|
||||||
|
- **Generator** (0 inputs): trigger, API call, schedule
|
||||||
|
- **Processor** (1+ inputs, 1+ outputs): analysis, transformation, scan
|
||||||
|
- **Sink** (1+ inputs, 0 outputs): notification, approval, archive
|
||||||
|
- **Bypass** (0 inputs, 0 outputs): rare - usually error
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### ✅ Valid Chains
|
||||||
|
|
||||||
|
```
|
||||||
|
CloneRepo → Analyze → SecurityScan → Report
|
||||||
|
(generator) → (processor) → (processor) → (sink)
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Trigger → LLMInference → Decision → (Branch: Notify OR Approve)
|
||||||
|
(gen) → (processor) → (processor) → (sink)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ❌ Invalid Chains
|
||||||
|
|
||||||
|
```
|
||||||
|
Notify → CloneRepo ❌
|
||||||
|
(sink) → (generator) - backward flow
|
||||||
|
|
||||||
|
CloneRepo → CloneRepo → Analyze ❌
|
||||||
|
self-loop - no benefit
|
||||||
|
|
||||||
|
Analyze → Approve → Notify ❌
|
||||||
|
Approve is terminal (sink), can't output to Notify
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
### Multiple Outputs → Single Input
|
||||||
|
```
|
||||||
|
SecurityScan → Report
|
||||||
|
SecurityScan outputs: [issues, metrics, severity]
|
||||||
|
Report inputs: [report_data]
|
||||||
|
|
||||||
|
LLM must infer: bundle all outputs into single report_data object
|
||||||
|
Confidence: 0.7 (requires transformation)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal Activities
|
||||||
|
- **ApproveWorkflowActivity**: Must be last (blocks workflow)
|
||||||
|
- **NotifyStatusActivity**: Can be mid-workflow (async notify)
|
||||||
|
- **ArchiveResultsActivity**: Should be last (persistence)
|
||||||
|
|
||||||
|
### Data Transformation
|
||||||
|
When source outputs don't match target inputs:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# User can insert transformer node:
|
||||||
|
LLMInference → [LLMTransformer] → DeploymentPreCheck
|
||||||
|
|
||||||
|
# Transformer:
|
||||||
|
# - Input: LLMInference.output (string)
|
||||||
|
# - Output: DeploymentPreCheck.requirements (object)
|
||||||
|
# - Action: Call LLM to convert format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Incompatibility Detection
|
||||||
|
|
||||||
|
### Test Case 1: Terminal Activity as Source
|
||||||
|
```go
|
||||||
|
source := db.WorkflowNode{ID: "n1", Type: "notify-status", Label: "Notify"}
|
||||||
|
target := db.WorkflowNode{ID: "n2", Type: "clone-repo", Label: "Clone"}
|
||||||
|
|
||||||
|
warnings := CheckConnectionCompatibility(source, target)
|
||||||
|
// Should warn: NotifyStatusActivity produces no outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Case 2: Type Mismatch
|
||||||
|
```go
|
||||||
|
source := db.WorkflowNode{ID: "n1", Type: "llm-inference", ...}
|
||||||
|
target := db.WorkflowNode{ID: "n2", Type: "deployment-check", ...}
|
||||||
|
|
||||||
|
warnings := CheckConnectionCompatibility(source, target)
|
||||||
|
// Should warn: string output ≠ object input
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Case 3: Disconnected Node
|
||||||
|
```go
|
||||||
|
nodes := []db.WorkflowNode{n1, n2, n3}
|
||||||
|
edges := []db.WorkflowEdge{{Source: "n1", Target: "n2"}}
|
||||||
|
|
||||||
|
disconnected := IdentifyDisconnectedNodes(nodes, edges)
|
||||||
|
// Should return ["n3"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Automatic Transformer Insertion**
|
||||||
|
- Detect incompatibilities
|
||||||
|
- Auto-suggest LLM transformer nodes
|
||||||
|
- Chain transformers if needed
|
||||||
|
|
||||||
|
2. **Confidence Scoring**
|
||||||
|
- Increase when types match perfectly
|
||||||
|
- Decrease for semantic mismatches
|
||||||
|
- Factor in activity dependencies
|
||||||
|
|
||||||
|
3. **Learning from History**
|
||||||
|
- Track successful workflows
|
||||||
|
- Remember user edits to suggestions
|
||||||
|
- Improve LLM prompts over time
|
||||||
|
|
||||||
|
4. **Multi-Path Analysis**
|
||||||
|
- Suggest multiple connection topologies
|
||||||
|
- Show cost/efficiency of each
|
||||||
|
- Rank by execution time/cost
|
||||||
|
|
||||||
|
5. **Dry-Run Validation**
|
||||||
|
- Execute suggested workflow in simulation
|
||||||
|
- Catch runtime errors early
|
||||||
|
- Show data flow through each node
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,712 @@
|
|||||||
|
# Workflows + Graph RAG Integration
|
||||||
|
|
||||||
|
Align Poimen Workflows with Poimen Memory System API for versioned relations, semantic queries, and intelligent canvas reasoning.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- Versioned workflow relations following `/memory/entities/{id}/versions` pattern
|
||||||
|
- Semantic edge queries via `/workflows/{id}/query/semantic/edges`
|
||||||
|
- Relation wording as Facts (matching Memory System edges schema)
|
||||||
|
- Point-in-time canvas reconstruction via `?as_of=timestamp`
|
||||||
|
- Ranking profiles for relation importance
|
||||||
|
- Automatic Graph RAG indexing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Workflow Canvas (React Flow) │
|
||||||
|
│ - Nodes (activities) │
|
||||||
|
│ - Edges (connections) │
|
||||||
|
└──────────────┬──────────────────┘
|
||||||
|
│ PUT /workflows/{id}
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ CanvasReasonerActivity │
|
||||||
|
│ - Suggest edges │
|
||||||
|
│ - Validate compatibility │
|
||||||
|
│ - Generate change reasoning │
|
||||||
|
└──────────────┬──────────────────┘
|
||||||
|
│ suggested_edges + reasoning
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Workflows API Server │
|
||||||
|
│ - Update canvas in DB │
|
||||||
|
│ - Create version entry │
|
||||||
|
│ - Store change metadata │
|
||||||
|
└──────────────┬──────────────────┘
|
||||||
|
│ canvas_version, relations
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Graph RAG Backend │
|
||||||
|
│ - Store versioned relations │
|
||||||
|
│ - Index relation wording │
|
||||||
|
│ - Enable semantic queries │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### Workflow Canvas Version
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- In memory.workflows_versions (new table)
|
||||||
|
CREATE TABLE workflow_versions (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
workflow_id UUID NOT NULL REFERENCES workflows(id),
|
||||||
|
customer_id UUID NOT NULL,
|
||||||
|
version INT NOT NULL,
|
||||||
|
canvas JSONB NOT NULL, -- {nodes, edges}
|
||||||
|
changed_by UUID,
|
||||||
|
change_reason TEXT,
|
||||||
|
change_type VARCHAR(50), -- 'manual', 'auto_reasoned', 'import'
|
||||||
|
reasoner_confidence FLOAT,
|
||||||
|
reasoning_metadata JSONB, -- LLM reasoning output
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
|
||||||
|
UNIQUE(workflow_id, version),
|
||||||
|
FOREIGN KEY(workflow_id, customer_id)
|
||||||
|
REFERENCES workflows(id, customer_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- In memory.workflow_relations (replaces simple edges)
|
||||||
|
CREATE TABLE workflow_relations (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
workflow_id UUID NOT NULL,
|
||||||
|
version INT NOT NULL,
|
||||||
|
source_node_id VARCHAR(255),
|
||||||
|
target_node_id VARCHAR(255),
|
||||||
|
relation_type VARCHAR(100), -- 'data-flow', 'dependency', 'conditional', 'parallel'
|
||||||
|
relation_label TEXT, -- Human-readable: "SecurityScan outputs issues → Report inputs requirements"
|
||||||
|
relation_wording JSONB, -- {verb, object, context}
|
||||||
|
metadata JSONB, -- {source_output_type, target_input_type, compatibility_score}
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY(workflow_id, version)
|
||||||
|
REFERENCES workflow_versions(id, version),
|
||||||
|
INDEX (workflow_id, version)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- In memory.relation_changes (for Graph RAG indexing)
|
||||||
|
CREATE TABLE relation_changes (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
workflow_id UUID,
|
||||||
|
version_from INT,
|
||||||
|
version_to INT,
|
||||||
|
change_type VARCHAR(50), -- 'added', 'removed', 'modified'
|
||||||
|
relation_id UUID REFERENCES workflow_relations(id),
|
||||||
|
source_node_id VARCHAR(255),
|
||||||
|
target_node_id VARCHAR(255),
|
||||||
|
old_wording JSONB,
|
||||||
|
new_wording JSONB,
|
||||||
|
change_reason TEXT,
|
||||||
|
change_timestamp TIMESTAMP,
|
||||||
|
reasoner_confidence FLOAT,
|
||||||
|
|
||||||
|
INDEX (workflow_id, version_to)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relation Wording Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "edge-1",
|
||||||
|
"source": "clone-repo-1",
|
||||||
|
"target": "analyze-code-1",
|
||||||
|
"relation_type": "data-flow",
|
||||||
|
|
||||||
|
"relation_label": "CloneRepo outputs path → AnalyzeCode requires path",
|
||||||
|
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "outputs",
|
||||||
|
"source_output": "path (string): Local filesystem path where repo was cloned",
|
||||||
|
"target_input": "path (string, required): Local filesystem path to analyze",
|
||||||
|
"connection_type": "direct-map",
|
||||||
|
"confidence": 0.98,
|
||||||
|
"notes": "Perfect type match between CloneRepo.path and AnalyzeCode.path"
|
||||||
|
},
|
||||||
|
|
||||||
|
"metadata": {
|
||||||
|
"source_activity": "CloneRepoActivity",
|
||||||
|
"target_activity": "AnalyzeCodeActivity",
|
||||||
|
"output_type": "string",
|
||||||
|
"input_type": "string",
|
||||||
|
"compatibility_score": 0.98,
|
||||||
|
"requires_transformation": false,
|
||||||
|
"semantic_match": "File path passes directly"
|
||||||
|
},
|
||||||
|
|
||||||
|
"change_history": [
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"action": "added",
|
||||||
|
"reason": "LLM reasoner suggested data-flow connection",
|
||||||
|
"confidence": 0.98,
|
||||||
|
"timestamp": "2025-09-05T10:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unified API Endpoints
|
||||||
|
|
||||||
|
All workflow queries follow the unified endpoint pattern from Memory System.
|
||||||
|
|
||||||
|
### 1. Unified Workflow Query
|
||||||
|
|
||||||
|
**Endpoint:** `POST /workflows/{id}/query`
|
||||||
|
|
||||||
|
This is the primary endpoint for all workflow canvas queries (replaces separate search endpoints).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "how does code analysis flow into security scanning",
|
||||||
|
"search_type": "edges|entities|all",
|
||||||
|
"version": 3,
|
||||||
|
"relation_type": "data-flow",
|
||||||
|
"confidence_floor": 0.7,
|
||||||
|
"top_k": 10,
|
||||||
|
"find_paths": true,
|
||||||
|
"target_node_id": "security-scan-1",
|
||||||
|
"max_path_depth": 3,
|
||||||
|
"ranking_profile": "default",
|
||||||
|
"include_reasoning": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"workflow_id": "workflow-1",
|
||||||
|
"query": "how does code analysis flow into security scanning",
|
||||||
|
"search_type": "edges",
|
||||||
|
"version": 3,
|
||||||
|
"execution_time_ms": 145,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "edge_analyze_scan",
|
||||||
|
"source_node_id": "analyze-code-1",
|
||||||
|
"source_name": "AnalyzeCodeActivity",
|
||||||
|
"target_node_id": "security-scan-1",
|
||||||
|
"target_name": "SecurityScanActivity",
|
||||||
|
"relation_type": "data-flow",
|
||||||
|
"relation_label": "AnalyzeCode outputs metrics → SecurityScan requires code structure",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "provides-input-for",
|
||||||
|
"source_output": "metrics (object): Code quality and structural metrics",
|
||||||
|
"target_input": "path (string): Directory to scan",
|
||||||
|
"connection_type": "requires-transformer",
|
||||||
|
"confidence": 0.85,
|
||||||
|
"semantic_match": "Analysis metrics can guide security scan prioritization"
|
||||||
|
},
|
||||||
|
"similarity_score": 0.92,
|
||||||
|
"confidence": 0.85,
|
||||||
|
"created_at": "2025-09-05T10:00:00Z",
|
||||||
|
"metadata": {
|
||||||
|
"source": "canvas://workflow-1:v3",
|
||||||
|
"tags": ["code-review", "security"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": [
|
||||||
|
{
|
||||||
|
"source_id": "analyze-code-1",
|
||||||
|
"target_id": "security-scan-1",
|
||||||
|
"path_count": 1,
|
||||||
|
"shortest_distance": 1,
|
||||||
|
"paths_found": [
|
||||||
|
{
|
||||||
|
"node_ids": ["analyze-code-1", "security-scan-1"],
|
||||||
|
"relation_types": ["data-flow"],
|
||||||
|
"distance": 1,
|
||||||
|
"total_confidence": 0.85
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_count": 1,
|
||||||
|
"has_more": false,
|
||||||
|
"ranking_profile": "default"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Update Workflow Canvas (with Versioning)
|
||||||
|
|
||||||
|
**Endpoint:** `PUT /workflows/{id}`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [...],
|
||||||
|
"edges": [...],
|
||||||
|
"auto_reason": true,
|
||||||
|
"change_reason": "User connected CloneRepo to AnalyzeCode",
|
||||||
|
"user_id": "uuid"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "workflow-1",
|
||||||
|
"version": 3,
|
||||||
|
"canvas": {
|
||||||
|
"nodes": [...],
|
||||||
|
"edges": [...]
|
||||||
|
},
|
||||||
|
"version_info": {
|
||||||
|
"version_number": 3,
|
||||||
|
"created_at": "2025-09-05T10:05:00Z",
|
||||||
|
"created_by": "user-uuid",
|
||||||
|
"change_reason": "User connected CloneRepo to AnalyzeCode",
|
||||||
|
"change_type": "manual"
|
||||||
|
},
|
||||||
|
"relation_updates": {
|
||||||
|
"added": [
|
||||||
|
{
|
||||||
|
"source": "clone-repo-1",
|
||||||
|
"target": "analyze-code-1",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "outputs",
|
||||||
|
"source_output": "path: Local filesystem path where repo was cloned",
|
||||||
|
"target_input": "path (required): Local filesystem path to analyze",
|
||||||
|
"confidence": 0.98
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"removed": [],
|
||||||
|
"modified": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Get Relation Version History (Memory System Pattern)
|
||||||
|
|
||||||
|
**Endpoint:** `GET /workflows/{id}/relations/{edge_id}/versions`
|
||||||
|
|
||||||
|
Follows `/memory/entities/{id}/versions` pattern from Memory System.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"edge_id": "edge_1",
|
||||||
|
"workflow_id": "workflow-1",
|
||||||
|
"source": "clone-repo-1",
|
||||||
|
"target": "analyze-code-1",
|
||||||
|
"versions": [
|
||||||
|
{
|
||||||
|
"version_num": 1,
|
||||||
|
"operation": "CREATE",
|
||||||
|
"snapshot": {
|
||||||
|
"relation_type": "data-flow",
|
||||||
|
"relation_label": "CloneRepo → AnalyzeCode",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "connects-to",
|
||||||
|
"confidence": 0.75
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"changed_at": "2025-09-04T12:00:00Z",
|
||||||
|
"changed_by": "system",
|
||||||
|
"fields_changed": ["relation_type", "relation_wording"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version_num": 2,
|
||||||
|
"operation": "UPDATE",
|
||||||
|
"snapshot": {
|
||||||
|
"relation_type": "data-flow",
|
||||||
|
"relation_label": "CloneRepo outputs path → AnalyzeCode requires path",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "outputs",
|
||||||
|
"source_output": "path (string)",
|
||||||
|
"target_input": "path (string, required)",
|
||||||
|
"confidence": 0.98
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"changed_at": "2025-09-05T10:00:00Z",
|
||||||
|
"changed_by": "reasoner-activity",
|
||||||
|
"fields_changed": ["relation_wording", "relation_label"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_versions": 2,
|
||||||
|
"current_version": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Edge Diff (Versioning Pattern)
|
||||||
|
|
||||||
|
**Endpoint:** `POST /workflows/{id}/relations/diff`
|
||||||
|
|
||||||
|
Follows `/memory/entities/diff` pattern.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"edge_id": "edge_1",
|
||||||
|
"from_version": 1,
|
||||||
|
"to_version": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"edge_id": "edge_1",
|
||||||
|
"from_version": 1,
|
||||||
|
"to_version": 2,
|
||||||
|
"source": "clone-repo-1",
|
||||||
|
"target": "analyze-code-1",
|
||||||
|
"diff": {
|
||||||
|
"added_fields": {},
|
||||||
|
"removed_fields": {},
|
||||||
|
"modified_fields": {
|
||||||
|
"relation_wording": {
|
||||||
|
"old": {
|
||||||
|
"verb": "connects-to",
|
||||||
|
"confidence": 0.75
|
||||||
|
},
|
||||||
|
"new": {
|
||||||
|
"verb": "outputs",
|
||||||
|
"source_output": "path (string)",
|
||||||
|
"target_input": "path (string, required)",
|
||||||
|
"confidence": 0.98
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"change_timeline": [
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"confidence": 0.75,
|
||||||
|
"changed_at": "2025-09-04T12:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"confidence": 0.98,
|
||||||
|
"changed_at": "2025-09-05T10:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"editors_involved": ["system", "reasoner-activity"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Point-in-Time Canvas (Versioning Pattern)
|
||||||
|
|
||||||
|
**Endpoint:** `GET /workflows/{id}?at_version={v}` or `?as_of=2025-09-05T10:00:00Z`
|
||||||
|
|
||||||
|
Follows `/memory/entities/at` pattern.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"workflow_id": "workflow-1",
|
||||||
|
"version": 2,
|
||||||
|
"as_of_timestamp": "2025-09-05T10:00:00Z",
|
||||||
|
"canvas": {
|
||||||
|
"nodes": [...],
|
||||||
|
"edges": [...]
|
||||||
|
},
|
||||||
|
"relations": [
|
||||||
|
{
|
||||||
|
"id": "edge_1",
|
||||||
|
"source": "clone-repo-1",
|
||||||
|
"target": "analyze-code-1",
|
||||||
|
"relation_type": "data-flow",
|
||||||
|
"relation_label": "CloneRepo outputs path → AnalyzeCode requires path",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "outputs",
|
||||||
|
"source_output": "path (string)",
|
||||||
|
"target_input": "path (string, required)",
|
||||||
|
"confidence": 0.98
|
||||||
|
},
|
||||||
|
"version": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"version_number": 2,
|
||||||
|
"created_at": "2025-09-05T10:00:00Z",
|
||||||
|
"changed_by": "reasoner-activity",
|
||||||
|
"change_reason": "LLM reasoner refined relation wording"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Bulk Import Canvas (with Relations)
|
||||||
|
|
||||||
|
**Endpoint:** `POST /workflows/{id}/import`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"canvas": {
|
||||||
|
"nodes": [...],
|
||||||
|
"edges": [...]
|
||||||
|
},
|
||||||
|
"relations": [
|
||||||
|
{
|
||||||
|
"source": "n1",
|
||||||
|
"target": "n2",
|
||||||
|
"relation_type": "data-flow",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "outputs",
|
||||||
|
"source_output": "result (string)",
|
||||||
|
"target_input": "input (string, required)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"change_reason": "Imported from external workflow system"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"workflow_id": "workflow-1",
|
||||||
|
"version": 4,
|
||||||
|
"canvas": {...},
|
||||||
|
"relations": {...},
|
||||||
|
"import_metadata": {
|
||||||
|
"imported_nodes": 5,
|
||||||
|
"imported_edges": 4,
|
||||||
|
"validation_status": "success",
|
||||||
|
"indexing_status": "queued_for_graph_rag"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with CanvasReasonerActivity
|
||||||
|
|
||||||
|
### Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User edits canvas
|
||||||
|
↓
|
||||||
|
PUT /workflows/{id} with auto_reason=true
|
||||||
|
↓
|
||||||
|
Backend calls CanvasReasonerActivity
|
||||||
|
↓
|
||||||
|
LLM suggests edges + reasoning
|
||||||
|
↓
|
||||||
|
Generate relation_wording from suggestions
|
||||||
|
↓
|
||||||
|
Create workflow_version entry
|
||||||
|
↓
|
||||||
|
Create workflow_relations entries
|
||||||
|
↓
|
||||||
|
Index relations in Graph RAG
|
||||||
|
↓
|
||||||
|
Response includes suggested_edges + relation_wording
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"suggested_edges": [
|
||||||
|
{
|
||||||
|
"source": "clone-1",
|
||||||
|
"target": "analyze-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reasoning": "Standard code review workflow",
|
||||||
|
"confidence": 0.92,
|
||||||
|
"relation_wordings": [
|
||||||
|
{
|
||||||
|
"source": "clone-1",
|
||||||
|
"target": "analyze-1",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "outputs",
|
||||||
|
"source_output": "path (string): Cloned repository path",
|
||||||
|
"target_input": "path (string, required): Directory to analyze",
|
||||||
|
"connection_type": "direct-map",
|
||||||
|
"confidence": 0.98,
|
||||||
|
"semantic_description": "Repository path flows from clone operation to analysis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Graph RAG Indexing
|
||||||
|
|
||||||
|
### Relations Indexed
|
||||||
|
|
||||||
|
Each workflow relation creates Graph RAG entities:
|
||||||
|
|
||||||
|
```
|
||||||
|
Node: {
|
||||||
|
id: "clone-repo-1",
|
||||||
|
type: "activity",
|
||||||
|
name: "CloneRepoActivity",
|
||||||
|
workflow_id: "workflow-1",
|
||||||
|
version: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Node: {
|
||||||
|
id: "analyze-code-1",
|
||||||
|
type: "activity",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
Edge: {
|
||||||
|
id: "rel-1",
|
||||||
|
source: "clone-repo-1",
|
||||||
|
target: "analyze-code-1",
|
||||||
|
type: "data-flow",
|
||||||
|
label: "outputs path",
|
||||||
|
wording: {...},
|
||||||
|
version: 3,
|
||||||
|
created_at: "2025-09-05T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Semantic Queries
|
||||||
|
|
||||||
|
Users can query like:
|
||||||
|
|
||||||
|
- *"Which activities receive data from CloneRepo?"*
|
||||||
|
- *"What's the data flow from Analyze to Report?"*
|
||||||
|
- *"Which relations were added in version 3?"*
|
||||||
|
- *"Show me all type-mismatched connections"*
|
||||||
|
- *"Find workflows with Security Scan that require approval"*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versioning Strategy
|
||||||
|
|
||||||
|
### Version Numbering
|
||||||
|
|
||||||
|
- Increment on every canvas change
|
||||||
|
- Track change_type: `manual`, `auto_reasoned`, `import`, `rag_query_applied`
|
||||||
|
- Store reasoner_confidence for auto changes
|
||||||
|
|
||||||
|
### Relation Wording Versions
|
||||||
|
|
||||||
|
- Each relation has independent wording history
|
||||||
|
- Confidence scores tracked per version
|
||||||
|
- Sources: user input, LLM reasoner, import, RAG query
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"workflow_id": "workflow-1",
|
||||||
|
"total_versions": 5,
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"type": "created",
|
||||||
|
"timestamp": "2025-09-04T10:00:00Z",
|
||||||
|
"user": "system",
|
||||||
|
"reason": "Workflow initialized"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"type": "auto_reasoned",
|
||||||
|
"timestamp": "2025-09-04T12:00:00Z",
|
||||||
|
"reasoner_confidence": 0.89,
|
||||||
|
"changes": {
|
||||||
|
"edges_added": 4,
|
||||||
|
"edges_modified": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"type": "manual",
|
||||||
|
"timestamp": "2025-09-05T10:05:00Z",
|
||||||
|
"user": "user-uuid",
|
||||||
|
"reason": "Connected SecurityScan to Report manually"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow Example
|
||||||
|
|
||||||
|
### Scenario: User drops SecurityScan node, system suggests connection
|
||||||
|
|
||||||
|
1. **User Action:** Drops SecurityScan node onto existing workflow
|
||||||
|
2. **Frontend Call:** `PUT /workflows/{id}` with new nodes + `auto_reason=true`
|
||||||
|
3. **Backend:**
|
||||||
|
- Saves canvas to `workflow_versions` v.3
|
||||||
|
- Calls CanvasReasonerActivity
|
||||||
|
4. **LLM Reasoning:**
|
||||||
|
- Analyzes: AnalyzeCode (outputs: quality, metrics) → SecurityScan (inputs: path, depth)
|
||||||
|
- Suggests: Add transformer node OR use metrics for decision
|
||||||
|
- Confidence: 0.85 (type mismatch, requires transformation)
|
||||||
|
5. **Relation Wording:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source": "analyze-code-1",
|
||||||
|
"target": "security-scan-1",
|
||||||
|
"relation_wording": {
|
||||||
|
"verb": "provides-context-for",
|
||||||
|
"source_output": "quality (object): Code quality metrics",
|
||||||
|
"target_input": "path (string): Directory to scan",
|
||||||
|
"connection_type": "requires-transformer",
|
||||||
|
"reasoning": "Metrics inform which files to prioritize in scanning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
6. **Graph RAG:** Relations indexed automatically
|
||||||
|
7. **Response:** Frontend shows suggestions with natural language descriptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
- [ ] Create `workflow_versions` table in memory DB
|
||||||
|
- [ ] Create `workflow_relations` table with wording schema
|
||||||
|
- [ ] Create `relation_changes` table for tracking modifications
|
||||||
|
- [ ] Update CanvasReasonerActivity to generate relation wordings
|
||||||
|
- [ ] Implement versioned CRUD endpoints (PUT, GET, DIFF)
|
||||||
|
- [ ] Add Graph RAG indexing on relation creation
|
||||||
|
- [ ] Implement semantic query endpoint
|
||||||
|
- [ ] Add changelog view
|
||||||
|
- [ ] Test point-in-time reconstruction
|
||||||
|
- [ ] Document API in homelab-frontend/API.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Relation Wording Language
|
||||||
|
|
||||||
|
### Verbs (relation_type → verb mapping)
|
||||||
|
|
||||||
|
| Type | Verbs | Example |
|
||||||
|
|------|-------|---------|
|
||||||
|
| `data-flow` | outputs, inputs, receives, provides | "CloneRepo outputs path → AnalyzeCode inputs path" |
|
||||||
|
| `dependency` | must-complete-before, depends-on, requires | "SecurityScan depends-on AnalyzeCode completion" |
|
||||||
|
| `conditional` | triggers-if, branches-on, routes-to | "ApproveWorkflow branches-on result approval" |
|
||||||
|
| `parallel` | runs-alongside, concurrent-with, independent-of | "Notify runs-alongside Report generation" |
|
||||||
|
| `transformation` | transforms, converts, maps, adapts | "LLMTransform converts metrics → deployment plan" |
|
||||||
|
|
||||||
|
### Confidence Scoring
|
||||||
|
|
||||||
|
- **0.9-1.0:** Perfect match (type-compatible, direct data flow)
|
||||||
|
- **0.7-0.9:** Good match (semantic fit, minor transformation needed)
|
||||||
|
- **0.5-0.7:** Possible match (requires user confirmation)
|
||||||
|
- **<0.5:** Poor match (suggest removal or transformer)
|
||||||
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
module github.com/rockliang/poimen/workflows
|
module forgejo.riotpiao.com/rock/poimen-workflows
|
||||||
|
|
||||||
go 1.26.0
|
go 1.25.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0
|
|
||||||
github.com/lib/pq v1.12.3
|
|
||||||
github.com/prometheus/client_golang v1.24.1
|
github.com/prometheus/client_golang v1.24.1
|
||||||
github.com/stretchr/testify v1.12.1
|
github.com/stretchr/testify v1.12.1
|
||||||
go.temporal.io/sdk v1.48.0
|
go.temporal.io/sdk v1.48.0
|
||||||
@@ -18,8 +16,10 @@ require (
|
|||||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
|
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect
|
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect
|
||||||
github.com/nexus-rpc/sdk-go v0.7.0 // indirect
|
github.com/nexus-rpc/sdk-go v0.7.0 // indirect
|
||||||
@@ -27,6 +27,8 @@ require (
|
|||||||
github.com/prometheus/common v0.70.1 // indirect
|
github.com/prometheus/common v0.70.1 // indirect
|
||||||
github.com/prometheus/procfs v0.21.1 // indirect
|
github.com/prometheus/procfs v0.21.1 // indirect
|
||||||
github.com/robfig/cron v1.2.0 // indirect
|
github.com/robfig/cron v1.2.0 // indirect
|
||||||
|
github.com/spf13/cobra v1.10.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
go.temporal.io/api v1.63.4 // indirect
|
go.temporal.io/api v1.63.4 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
|
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
|
||||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
|
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
@@ -22,6 +23,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4z
|
|||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||||
@@ -32,8 +35,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
|
||||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 h1:2fELd+9sqUtNu6Fg//pw8YFsxOvp8vZ8hfP0nHhNI80=
|
github.com/nexus-rpc/nexus-proto-annotations v0.1.0 h1:2fELd+9sqUtNu6Fg//pw8YFsxOvp8vZ8hfP0nHhNI80=
|
||||||
@@ -52,6 +53,11 @@ github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
|||||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
@@ -83,6 +89,7 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
|||||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.temporal.io/sdk/client"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server handles HTTP routing for workflow APIs
|
||||||
|
type Server struct {
|
||||||
|
api *WorkflowAPI
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates new HTTP server with database connection
|
||||||
|
func NewServer(database *db.DB, temporalClient client.Client, logger *log.Logger) *Server {
|
||||||
|
return &Server{
|
||||||
|
api: NewWorkflowAPI(database, temporalClient, logger),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP dispatches HTTP requests to appropriate handler
|
||||||
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Enable CORS
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := r.URL.Path
|
||||||
|
method := r.Method
|
||||||
|
|
||||||
|
s.logger.Printf("%s %s", method, path)
|
||||||
|
|
||||||
|
// Route requests
|
||||||
|
switch {
|
||||||
|
// Workflow endpoints
|
||||||
|
case path == "/workflows" && method == http.MethodPost:
|
||||||
|
s.api.CreateWorkflow(w, r)
|
||||||
|
case path == "/workflows" && method == http.MethodGet:
|
||||||
|
s.api.ListWorkflows(w, r)
|
||||||
|
case strings.HasPrefix(path, "/workflows/") && method == http.MethodGet:
|
||||||
|
id := strings.TrimPrefix(path, "/workflows/")
|
||||||
|
// Exclude special paths
|
||||||
|
if !strings.Contains(id, "/") {
|
||||||
|
s.api.GetWorkflow(w, r, id)
|
||||||
|
} else if strings.HasSuffix(id, "/executions") {
|
||||||
|
// GET /workflows/{id}/executions
|
||||||
|
workflowID := strings.TrimSuffix(id, "/executions")
|
||||||
|
s.api.ListExecutions(w, r, workflowID)
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(path, "/workflows/") && method == http.MethodPut:
|
||||||
|
id := extractID(path, "/workflows/")
|
||||||
|
s.api.UpdateWorkflow(w, r, id)
|
||||||
|
case strings.HasPrefix(path, "/workflows/") && method == http.MethodDelete:
|
||||||
|
id := extractID(path, "/workflows/")
|
||||||
|
s.api.DeleteWorkflow(w, r, id)
|
||||||
|
|
||||||
|
// Execute workflow
|
||||||
|
case strings.HasSuffix(path, "/execute") && method == http.MethodPost:
|
||||||
|
// POST /workflows/{id}/execute
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
if len(parts) >= 4 && parts[1] == "workflows" && parts[3] == "execute" {
|
||||||
|
s.api.ExecuteWorkflow(w, r, parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execution endpoints
|
||||||
|
case strings.HasPrefix(path, "/executions/") && method == http.MethodGet:
|
||||||
|
id := extractID(path, "/executions/")
|
||||||
|
s.api.GetExecution(w, r, id)
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractID extracts resource ID from path
|
||||||
|
func extractID(path, prefix string) string {
|
||||||
|
id := strings.TrimPrefix(path, prefix)
|
||||||
|
if idx := strings.Index(id, "/"); idx != -1 {
|
||||||
|
return id[:idx]
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the HTTP server
|
||||||
|
func (s *Server) Start(port int) error {
|
||||||
|
addr := fmt.Sprintf(":%d", port)
|
||||||
|
s.logger.Printf("Starting API server on %s", addr)
|
||||||
|
return http.ListenAndServe(addr, s)
|
||||||
|
}
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.temporal.io/sdk/client"
|
||||||
|
|
||||||
|
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||||
|
"github.com/rockliang/poimen/workflows/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WorkflowNode matches frontend node type
|
||||||
|
type WorkflowNode struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"` // "activity", "start", "end"
|
||||||
|
Position map[string]interface{} `json:"position"`
|
||||||
|
Data struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Activity string `json:"activity"`
|
||||||
|
Config map[string]interface{} `json:"config"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowEdge matches frontend edge type
|
||||||
|
type WorkflowEdge struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowDef is the request body for creating/updating workflows
|
||||||
|
type WorkflowDef struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Nodes []WorkflowNode `json:"nodes"`
|
||||||
|
Edges []WorkflowEdge `json:"edges"`
|
||||||
|
Status string `json:"status"` // "draft", "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowResponse is the workflow with metadata
|
||||||
|
type WorkflowResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Nodes []WorkflowNode `json:"nodes"`
|
||||||
|
Edges []WorkflowEdge `json:"edges"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
CreatedBy string `json:"createdBy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionRequest is the request to execute a workflow
|
||||||
|
type ExecutionRequest struct {
|
||||||
|
Inputs map[string]interface{} `json:"inputs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionResponse is the execution result
|
||||||
|
type ExecutionResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
WorkflowID string `json:"workflowId"`
|
||||||
|
Status string `json:"status"` // "pending", "running", "success", "failed"
|
||||||
|
StartedAt string `json:"startedAt"`
|
||||||
|
CompletedAt string `json:"completedAt,omitempty"`
|
||||||
|
Inputs map[string]interface{} `json:"inputs"`
|
||||||
|
Outputs map[string]interface{} `json:"outputs,omitempty"`
|
||||||
|
Errors []string `json:"errors,omitempty"`
|
||||||
|
Logs []ExecutionLog `json:"logs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionLog is a log entry from execution
|
||||||
|
type ExecutionLog struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
NodeID string `json:"nodeId"`
|
||||||
|
Level string `json:"level"` // "info", "warn", "error"
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorkflowAPI handles workflow endpoints
|
||||||
|
type WorkflowAPI struct {
|
||||||
|
db *db.DB
|
||||||
|
temporalClient client.Client
|
||||||
|
logger *log.Logger
|
||||||
|
customerID string // TODO: Extract from JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWorkflowAPI creates new API handler
|
||||||
|
func NewWorkflowAPI(database *db.DB, tc client.Client, logger *log.Logger) *WorkflowAPI {
|
||||||
|
return &WorkflowAPI{
|
||||||
|
db: database,
|
||||||
|
temporalClient: tc,
|
||||||
|
logger: logger,
|
||||||
|
customerID: "default-customer", // TODO: From auth context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWorkflow handles POST /workflows
|
||||||
|
func (api *WorkflowAPI) CreateWorkflow(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req WorkflowDef
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
http.Error(w, "Workflow name required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create workflow in database
|
||||||
|
id := uuid.New().String()
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Convert nodes and edges to JSONB
|
||||||
|
nodesJSON, err := json.Marshal(req.Nodes)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to marshal nodes: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
edgesJSON, err := json.Marshal(req.Edges)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to marshal edges: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := req.Status
|
||||||
|
if status == "" {
|
||||||
|
status = "draft"
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow := &db.Workflow{
|
||||||
|
ID: id,
|
||||||
|
CustomerID: api.customerID,
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
Status: status,
|
||||||
|
Version: 1,
|
||||||
|
Nodes: nodesJSON,
|
||||||
|
Edges: edgesJSON,
|
||||||
|
CreatedBy: "anonymous", // Use JWT claim in real implementation
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.db.SaveWorkflow(r.Context(), workflow); err != nil {
|
||||||
|
api.logger.Printf("Failed to save workflow: %v", err)
|
||||||
|
http.Error(w, "Failed to create workflow", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := WorkflowResponse{
|
||||||
|
ID: workflow.ID,
|
||||||
|
Name: workflow.Name,
|
||||||
|
Description: workflow.Description,
|
||||||
|
Status: workflow.Status,
|
||||||
|
Version: workflow.Version,
|
||||||
|
Nodes: req.Nodes,
|
||||||
|
Edges: req.Edges,
|
||||||
|
CreatedAt: workflow.CreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: workflow.UpdatedAt.Format(time.RFC3339),
|
||||||
|
CreatedBy: workflow.CreatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListWorkflows handles GET /workflows
|
||||||
|
func (api *WorkflowAPI) ListWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := 1
|
||||||
|
limit := 10
|
||||||
|
// Parse pagination params if needed
|
||||||
|
|
||||||
|
workflows, err := api.db.ListWorkflows(r.Context(), api.customerID, limit, (page-1)*limit)
|
||||||
|
if err != nil {
|
||||||
|
api.logger.Printf("Failed to list workflows: %v", err)
|
||||||
|
http.Error(w, "Failed to list workflows", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list := make([]WorkflowResponse, 0)
|
||||||
|
for _, wf := range workflows {
|
||||||
|
var nodes []WorkflowNode
|
||||||
|
var edges []WorkflowEdge
|
||||||
|
|
||||||
|
json.Unmarshal(wf.Nodes, &nodes)
|
||||||
|
json.Unmarshal(wf.Edges, &edges)
|
||||||
|
|
||||||
|
list = append(list, WorkflowResponse{
|
||||||
|
ID: wf.ID,
|
||||||
|
Name: wf.Name,
|
||||||
|
Description: wf.Description,
|
||||||
|
Status: wf.Status,
|
||||||
|
Version: wf.Version,
|
||||||
|
Nodes: nodes,
|
||||||
|
Edges: edges,
|
||||||
|
CreatedAt: wf.CreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: wf.UpdatedAt.Format(time.RFC3339),
|
||||||
|
CreatedBy: wf.CreatedBy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"workflows": list,
|
||||||
|
"total": len(list),
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkflow handles GET /workflows/{id}
|
||||||
|
func (api *WorkflowAPI) GetWorkflow(w http.ResponseWriter, r *http.Request, id string) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow, err := api.db.FetchWorkflow(r.Context(), id, api.customerID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Workflow not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes []WorkflowNode
|
||||||
|
var edges []WorkflowEdge
|
||||||
|
|
||||||
|
json.Unmarshal(workflow.Nodes, &nodes)
|
||||||
|
json.Unmarshal(workflow.Edges, &edges)
|
||||||
|
|
||||||
|
response := WorkflowResponse{
|
||||||
|
ID: workflow.ID,
|
||||||
|
Name: workflow.Name,
|
||||||
|
Description: workflow.Description,
|
||||||
|
Status: workflow.Status,
|
||||||
|
Version: workflow.Version,
|
||||||
|
Nodes: nodes,
|
||||||
|
Edges: edges,
|
||||||
|
CreatedAt: workflow.CreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: workflow.UpdatedAt.Format(time.RFC3339),
|
||||||
|
CreatedBy: workflow.CreatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWorkflow handles PUT /workflows/{id}
|
||||||
|
func (api *WorkflowAPI) UpdateWorkflow(w http.ResponseWriter, r *http.Request, id string) {
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch existing workflow
|
||||||
|
workflow, err := api.db.FetchWorkflow(r.Context(), id, api.customerID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Workflow not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req WorkflowDef
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if req.Name != "" {
|
||||||
|
workflow.Name = req.Name
|
||||||
|
}
|
||||||
|
if req.Description != "" {
|
||||||
|
workflow.Description = req.Description
|
||||||
|
}
|
||||||
|
if req.Nodes != nil {
|
||||||
|
nodesJSON, _ := json.Marshal(req.Nodes)
|
||||||
|
workflow.Nodes = nodesJSON
|
||||||
|
}
|
||||||
|
if req.Edges != nil {
|
||||||
|
edgesJSON, _ := json.Marshal(req.Edges)
|
||||||
|
workflow.Edges = edgesJSON
|
||||||
|
}
|
||||||
|
if req.Status != "" {
|
||||||
|
workflow.Status = req.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow.Version++
|
||||||
|
workflow.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := api.db.SaveWorkflow(r.Context(), workflow); err != nil {
|
||||||
|
api.logger.Printf("Failed to update workflow: %v", err)
|
||||||
|
http.Error(w, "Failed to update workflow", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes []WorkflowNode
|
||||||
|
var edges []WorkflowEdge
|
||||||
|
|
||||||
|
json.Unmarshal(workflow.Nodes, &nodes)
|
||||||
|
json.Unmarshal(workflow.Edges, &edges)
|
||||||
|
|
||||||
|
response := WorkflowResponse{
|
||||||
|
ID: workflow.ID,
|
||||||
|
Name: workflow.Name,
|
||||||
|
Description: workflow.Description,
|
||||||
|
Status: workflow.Status,
|
||||||
|
Version: workflow.Version,
|
||||||
|
Nodes: nodes,
|
||||||
|
Edges: edges,
|
||||||
|
CreatedAt: workflow.CreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: workflow.UpdatedAt.Format(time.RFC3339),
|
||||||
|
CreatedBy: workflow.CreatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWorkflow handles DELETE /workflows/{id}
|
||||||
|
func (api *WorkflowAPI) DeleteWorkflow(w http.ResponseWriter, r *http.Request, id string) {
|
||||||
|
if r.Method != http.MethodDelete {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.db.DeleteWorkflow(r.Context(), id, api.customerID); err != nil {
|
||||||
|
http.Error(w, "Workflow not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteWorkflow handles POST /workflows/{id}/execute
|
||||||
|
func (api *WorkflowAPI) ExecuteWorkflow(w http.ResponseWriter, r *http.Request, id string) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow, err := api.db.FetchWorkflow(r.Context(), id, api.customerID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Workflow not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req ExecutionRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal nodes and edges
|
||||||
|
var nodes []WorkflowNode
|
||||||
|
var edges []WorkflowEdge
|
||||||
|
json.Unmarshal(workflow.Nodes, &nodes)
|
||||||
|
json.Unmarshal(workflow.Edges, &edges)
|
||||||
|
|
||||||
|
// Convert to workflow response for spec conversion
|
||||||
|
workflowResp := &WorkflowResponse{
|
||||||
|
ID: workflow.ID,
|
||||||
|
Name: workflow.Name,
|
||||||
|
Description: workflow.Description,
|
||||||
|
Status: workflow.Status,
|
||||||
|
Version: workflow.Version,
|
||||||
|
Nodes: nodes,
|
||||||
|
Edges: edges,
|
||||||
|
CreatedBy: workflow.CreatedBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert nodes/edges to WorkflowSpec
|
||||||
|
spec := api.nodesToWorkflowSpec(workflowResp, req.Inputs)
|
||||||
|
|
||||||
|
// Execute via Temporal RoutingWorkflow
|
||||||
|
execID := uuid.New().String()
|
||||||
|
workflowOptions := client.StartWorkflowOptions{
|
||||||
|
ID: execID,
|
||||||
|
TaskQueue: "default",
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err = api.temporalClient.ExecuteWorkflow(ctx, workflowOptions, "RoutingWorkflow", spec)
|
||||||
|
if err != nil {
|
||||||
|
api.logger.Printf("Failed to execute workflow: %v", err)
|
||||||
|
http.Error(w, fmt.Sprintf("Execution failed: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save execution to database
|
||||||
|
inputsJSON, _ := json.Marshal(req.Inputs)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
execution := &db.WorkflowExecution{
|
||||||
|
ID: execID,
|
||||||
|
WorkflowID: id,
|
||||||
|
CustomerID: api.customerID,
|
||||||
|
TemporalID: execID,
|
||||||
|
Status: "running",
|
||||||
|
Inputs: inputsJSON,
|
||||||
|
StartedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.db.SaveExecution(r.Context(), execution); err != nil {
|
||||||
|
api.logger.Printf("Failed to save execution: %v", err)
|
||||||
|
http.Error(w, "Failed to save execution", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create execution response
|
||||||
|
execResp := ExecutionResponse{
|
||||||
|
ID: execID,
|
||||||
|
WorkflowID: id,
|
||||||
|
Status: "running",
|
||||||
|
StartedAt: now.Format(time.RFC3339),
|
||||||
|
Inputs: req.Inputs,
|
||||||
|
Outputs: make(map[string]interface{}),
|
||||||
|
Logs: []ExecutionLog{},
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(execResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExecution handles GET /executions/{id}
|
||||||
|
func (api *WorkflowAPI) GetExecution(w http.ResponseWriter, r *http.Request, id string) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
execution, err := api.db.FetchExecution(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Execution not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs from database
|
||||||
|
logs, err := api.db.FetchExecutionLogs(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
api.logger.Printf("Failed to fetch logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
execLogs := make([]ExecutionLog, 0)
|
||||||
|
for _, log := range logs {
|
||||||
|
execLogs = append(execLogs, ExecutionLog{
|
||||||
|
Timestamp: log.LoggedAt.Format(time.RFC3339),
|
||||||
|
NodeID: log.NodeID,
|
||||||
|
Level: log.Level,
|
||||||
|
Message: log.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse inputs/outputs
|
||||||
|
var inputs map[string]interface{}
|
||||||
|
var outputs map[string]interface{}
|
||||||
|
json.Unmarshal(execution.Inputs, &inputs)
|
||||||
|
if execution.Outputs != nil {
|
||||||
|
json.Unmarshal(execution.Outputs, &outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Temporal workflow status
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
desc, err := api.temporalClient.DescribeWorkflowExecution(ctx, execution.TemporalID, "")
|
||||||
|
status := execution.Status
|
||||||
|
if err == nil && desc != nil {
|
||||||
|
switch desc.Status.String() {
|
||||||
|
case "RUNNING":
|
||||||
|
status = "running"
|
||||||
|
case "COMPLETED":
|
||||||
|
status = "success"
|
||||||
|
case "FAILED":
|
||||||
|
status = "failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completedAtStr := ""
|
||||||
|
if execution.CompletedAt != nil {
|
||||||
|
completedAtStr = execution.CompletedAt.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
execResp := ExecutionResponse{
|
||||||
|
ID: execution.ID,
|
||||||
|
WorkflowID: execution.WorkflowID,
|
||||||
|
Status: status,
|
||||||
|
StartedAt: execution.StartedAt.Format(time.RFC3339),
|
||||||
|
CompletedAt: completedAtStr,
|
||||||
|
Inputs: inputs,
|
||||||
|
Outputs: outputs,
|
||||||
|
Logs: execLogs,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(execResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListExecutions handles GET /workflows/{id}/executions
|
||||||
|
func (api *WorkflowAPI) ListExecutions(w http.ResponseWriter, r *http.Request, workflowID string) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement query by workflow_id in database
|
||||||
|
// For now, return empty list (needs DB method for filtering by workflow_id)
|
||||||
|
list := make([]ExecutionResponse, 0)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodesToWorkflowSpec converts frontend nodes/edges to routing.WorkflowSpec
|
||||||
|
func (api *WorkflowAPI) nodesToWorkflowSpec(wf *WorkflowResponse, inputs map[string]interface{}) *routing.WorkflowSpec {
|
||||||
|
spec := &routing.WorkflowSpec{
|
||||||
|
Name: wf.Name,
|
||||||
|
Input: inputs,
|
||||||
|
States: []routing.State{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build states from nodes
|
||||||
|
stateMap := make(map[string]*routing.State)
|
||||||
|
|
||||||
|
// Create all states
|
||||||
|
for _, node := range wf.Nodes {
|
||||||
|
if node.Type == "activity" {
|
||||||
|
state := &routing.State{
|
||||||
|
Name: node.ID,
|
||||||
|
Type: routing.StateTypeTask,
|
||||||
|
Resource: node.Data.Activity,
|
||||||
|
Parameters: node.Data.Config,
|
||||||
|
End: false,
|
||||||
|
}
|
||||||
|
stateMap[node.ID] = state
|
||||||
|
spec.States = append(spec.States, *state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire edges (transitions)
|
||||||
|
for _, edge := range wf.Edges {
|
||||||
|
if state, exists := stateMap[edge.Source]; exists {
|
||||||
|
state.Next = edge.Target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark last state as End
|
||||||
|
if len(spec.States) > 0 {
|
||||||
|
// Find state with no outgoing edge
|
||||||
|
for i := range spec.States {
|
||||||
|
hasNext := false
|
||||||
|
for _, edge := range wf.Edges {
|
||||||
|
if edge.Source == spec.States[i].Name {
|
||||||
|
hasNext = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasNext {
|
||||||
|
spec.States[i].End = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -28,16 +28,12 @@ type State struct {
|
|||||||
Type StateType `json:"type"`
|
Type StateType `json:"type"`
|
||||||
|
|
||||||
// Task fields
|
// Task fields
|
||||||
Activity string `json:"activity,omitempty"`
|
|
||||||
Resource string `json:"resource,omitempty"`
|
Resource string `json:"resource,omitempty"`
|
||||||
Parameters map[string]interface{} `json:"parameters,omitempty"`
|
Parameters map[string]interface{} `json:"parameters,omitempty"`
|
||||||
Timeout string `json:"timeout,omitempty"`
|
Timeout string `json:"timeout,omitempty"`
|
||||||
Retry *RetryPolicy `json:"retry,omitempty"`
|
Retry *RetryPolicy `json:"retry,omitempty"`
|
||||||
Catch []CatchClause `json:"catch,omitempty"`
|
Catch []CatchClause `json:"catch,omitempty"`
|
||||||
|
|
||||||
// Parallel fields
|
|
||||||
Branches []interface{} `json:"branches,omitempty"`
|
|
||||||
|
|
||||||
// Pass fields
|
// Pass fields
|
||||||
Result interface{} `json:"result,omitempty"`
|
Result interface{} `json:"result,omitempty"`
|
||||||
|
|
||||||
@@ -54,18 +50,14 @@ type State struct {
|
|||||||
type StateType string
|
type StateType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StateTypeTask StateType = "Task"
|
StateTypeTask StateType = "Task"
|
||||||
StateTypePass StateType = "Pass"
|
StateTypePass StateType = "Pass"
|
||||||
StateTypeFail StateType = "Fail"
|
StateTypeFail StateType = "Fail"
|
||||||
StateTypeParallel StateType = "Parallel"
|
|
||||||
|
|
||||||
TaskActivity = "Task"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RetryPolicy defines retry behavior for activities
|
// RetryPolicy defines retry behavior for activities
|
||||||
type RetryPolicy struct {
|
type RetryPolicy struct {
|
||||||
MaxAttempts int32 `json:"maxAttempts"`
|
MaxAttempts int32 `json:"maxAttempts"`
|
||||||
BackoffSeconds int32 `json:"backoffSeconds,omitempty"`
|
|
||||||
BackoffRate float64 `json:"backoffRate"`
|
BackoffRate float64 `json:"backoffRate"`
|
||||||
InitialInterval string `json:"initialInterval"`
|
InitialInterval string `json:"initialInterval"`
|
||||||
MaxInterval string `json:"maxInterval,omitempty"`
|
MaxInterval string `json:"maxInterval,omitempty"`
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
apiVersion: ENC[AES256_GCM,data:IxE=,iv:nh5IQck87AsRYnvxMLxn2rZFBUTHcc9obvsYoHnvC6g=,tag:ink4m5kv1NqX2TeRrpCaqg==,type:str]
|
|
||||||
kind: ENC[AES256_GCM,data:2U5oBCqywkR0,iv:EVBlp6G1SlznoP7Zx9Y0mQOnxbzcosP28+UbJUWFjHM=,tag:v1lot4q06QBqU2M5q2RJ/g==,type:str]
|
|
||||||
metadata:
|
|
||||||
name: ENC[AES256_GCM,data:pFOLyxGAW7/KqUqijkPM1mGYaKo=,iv:jttNW2Ef0itpuWupOQkGEiFaanpcd/HuvfLg9l2tSEg=,tag:6ijOdYdjnUaCOf29z8vkxg==,type:str]
|
|
||||||
namespace: ENC[AES256_GCM,data:FSZFmVEp,iv:liL0yGuEbjZjf6egh8KS9zi6H3AEF9MvynGBO+49GW0=,tag:4iJNECe8vsXqzQBV/Q7c0g==,type:str]
|
|
||||||
data:
|
|
||||||
#ENC[AES256_GCM,data:d+Z/w/n69JI5,iv:/w2xIrmNXg53kz+tfebT0P6rD3TC909YTDFXXUc/yHo=,tag:ao4yA2lW2a1/ELT8ZIBOgw==,type:comment]
|
|
||||||
TEMPORAL_NAMESPACE: ENC[AES256_GCM,data:X0D/DVfVgzhFh+oB8nU=,iv:biiLtXRALsUMNIW4qGj8JOc9C1RJK93pOw74ZmymM/g=,tag:tW5+AwUGj8jzvxPSZAf41A==,type:str]
|
|
||||||
TEMPORAL_HOSTPORT: ENC[AES256_GCM,data:OK8Cu7nA2fKhXmBko8MORoD6hvJBMFfE/dTBZ99ac420xjPBWjhrz2XR3WRiWhUIIQ==,iv:HKDNRT/30HNzN3pX91e63i/ZeIUUYkycknjjKTFlVwo=,tag:OjPle4p+nV4ZQYN/yuUFEw==,type:str]
|
|
||||||
#ENC[AES256_GCM,data:1jnEt80ScfSqnCv9Rf/XLAlYTfuZVxoRdV7Sx9EgIGmzvAM=,iv:3NKsB9ZytMxuMipSLApKX7hj2jQG+2e2Hp8+ofFin9k=,tag:s4eRatHTQJa37xBO5UYI5Q==,type:comment]
|
|
||||||
LOCAL_LLM_BASE_URL: ENC[AES256_GCM,data:rTWBhNovjp5WdFI2jMaMRRg41uhh1dOFqVnplmMADiubu/FGXCvUPD2d9IuU,iv:1RdS5UfD6aOXnaEfddRd4PxJ1+K7AwKlMUYxzUQxVJ4=,tag:K0g2KDCFLOtxDFkHxoRbMw==,type:str]
|
|
||||||
MEMORY_SERVICE_URL: ENC[AES256_GCM,data:CarE5ENFtDvBWVzB7qTZQqoL0OsN2uAO9aoPXu1LPWtdoSQ2rQsVs2scTA08jaWLD+o=,iv:e7M/aibmYdSk4NbY6fYw0UPjd/VQvWJ1tC8n0r2y6r0=,tag:bWuCTBRfKkaB2fauPR51Fg==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBV0RJU2wzN1Bib0lZRlFl
|
|
||||||
K2UzSVBNUjNHUVVFeUFEWTJBUHNZT0l3RWprCmp2cE1HM2xOWnVjVGlqdWI0SndG
|
|
||||||
Q1VOaklVdlg4eGp6V29uVDVJRUtmKzgKLS0tIGp3MWJMdmdyZmRnWmxOWEhVZUhm
|
|
||||||
Rm5DNzBhUWt0SVBDK09mSWw2SHVRTWMKLS6Cz2nhz1RSoV+VUvFw9EsjlWbE2nEk
|
|
||||||
4P1FdNgr+v1MTNczKZGzh9HbTAmYxCRgVBszXR5ov2JfukaZAWb9mg==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
|
||||||
lastmodified: "2026-09-06T12:56:33Z"
|
|
||||||
mac: ENC[AES256_GCM,data:Ra89XdYHoJV+uFBlUMdw+I36UQfhM+r0Q4WjI3V/A7jBY2t00I0xSP+KRKPXlAA18HXpLCc5VkWklPMU4gfyYied1bPuFw6yLvvuup/ev4UAg/LofThKjtuRfXPy82Ltvro6R+ilWzJ8pRtgtk+jC4xkkMsUOACJMVwgn979D7M=,iv:UWR+CqXAreW/fx3tt1kJsOKYZkrAkxLXlU5QVy5p3gE=,tag:3t9c6631D4XrDmkR6Iv/4Q==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.2
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: poimen-config
|
||||||
|
namespace: poimen
|
||||||
|
data:
|
||||||
|
TEMPORAL_NAMESPACE: "poimen-harness"
|
||||||
|
TEMPORAL_HOSTPORT: "temporal-frontend.temporal:7233"
|
||||||
|
LOCAL_LLM_BASE_URL: "http://api-gateway.api:8080"
|
||||||
|
POIMEN_MEMORY_URL: "http://poimen-memory.poimen.svc.cluster.local:8080"
|
||||||
+1
-1
@@ -9,6 +9,6 @@ metadata:
|
|||||||
app.kubernetes.io/name: poimen
|
app.kubernetes.io/name: poimen
|
||||||
app.kubernetes.io/component: orchestrator
|
app.kubernetes.io/component: orchestrator
|
||||||
data:
|
data:
|
||||||
GIT_COMMIT: "84b4ca120" # Updated automatically by CI/CD
|
GIT_COMMIT: "eb4c4e989" # Updated automatically by CI/CD
|
||||||
GIT_BRANCH: "main"
|
GIT_BRANCH: "main"
|
||||||
DEPLOYMENT_DATE: "2026-09-05"
|
DEPLOYMENT_DATE: "2026-09-05"
|
||||||
|
|||||||
+7
-11
@@ -4,19 +4,15 @@ kind: Kustomization
|
|||||||
namespace: poimen
|
namespace: poimen
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- poimen-application.yaml
|
- worker-deployment.yaml
|
||||||
|
- configmap.yaml
|
||||||
|
|
||||||
commonLabels:
|
commonLabels:
|
||||||
app.kubernetes.io/name: poimen
|
app.kubernetes.io/name: poimen
|
||||||
app.kubernetes.io/component: worker
|
app.kubernetes.io/component: worker
|
||||||
|
|
||||||
images:
|
secretGenerator:
|
||||||
- name: forgejo.riotpiao.com/rock/poimen-memory
|
- name: poimen-secrets
|
||||||
newName: forgejo.riotpiao.com/rock/poimen-memory
|
envs:
|
||||||
newTag: latest
|
- secrets.env
|
||||||
- name: forgejo.riotpiao.com/rock/poimen-workflows
|
behavior: create
|
||||||
newName: forgejo.riotpiao.com/rock/poimen-workflows
|
|
||||||
newTag: latest
|
|
||||||
- name: forgejo.riotpiao.com/rock/poimen-frontend
|
|
||||||
newName: forgejo.riotpiao.com/rock/poimen-frontend
|
|
||||||
newTag: latest
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# NOTE: This file is for reference only.
|
||||||
|
# Kustomize will auto-generate secrets from secrets.env
|
||||||
|
# See kustomization.yaml for details
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: poimen-secrets
|
||||||
|
namespace: poimen
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
ANTHROPIC_API_KEY: "" # Generated from secrets.env by Kustomize
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY_HERE
|
||||||
@@ -13,7 +13,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: poimen-worker
|
app: poimen-worker
|
||||||
annotations:
|
annotations:
|
||||||
git-commit: "84b4ca120" # ✅ Updated on each push, triggers rolling restart
|
git-commit: "eb4c4e989" # ✅ Updated on each push, triggers rolling restart
|
||||||
deployment-date: "2026-09-05"
|
deployment-date: "2026-09-05"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
|
|||||||
+1
-80
@@ -17,7 +17,7 @@ type DB struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new database connection to memory-db (K8s CNPG)
|
// New creates a new database connection to memory-db (K8s CNPG)
|
||||||
// Expected DSN format: postgresql://app:password@host:5432/dbname?sslmode=disable
|
// Expected DSN format: postgresql://app:password@memory-db-rw.poimen.svc.cluster.local:5432/memory?sslmode=disable
|
||||||
func New(dsn string) (*DB, error) {
|
func New(dsn string) (*DB, error) {
|
||||||
if dsn == "" {
|
if dsn == "" {
|
||||||
// Fallback: try to construct from K8s env vars
|
// Fallback: try to construct from K8s env vars
|
||||||
@@ -454,82 +454,3 @@ func (db *DB) FetchActivityTraces(ctx context.Context, executionID string) ([]Ac
|
|||||||
|
|
||||||
return traces, rows.Err()
|
return traces, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWorkflowRelations retrieves all relations for a workflow version
|
|
||||||
func (db *DB) GetWorkflowRelations(ctx context.Context, workflowID string, version int) ([]WorkflowRelation, error) {
|
|
||||||
var relations []WorkflowRelation
|
|
||||||
|
|
||||||
query := `
|
|
||||||
SELECT id, workflow_id, version, source_node_id, target_node_id,
|
|
||||||
relation_type, label, relation_wording, metadata, created_at
|
|
||||||
FROM workflow_relations
|
|
||||||
WHERE workflow_id = $1 AND version = $2
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := db.conn.QueryContext(ctx, query, workflowID, version)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query relations: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var rel WorkflowRelation
|
|
||||||
if err := rows.Scan(
|
|
||||||
&rel.ID,
|
|
||||||
&rel.WorkflowID,
|
|
||||||
&rel.Version,
|
|
||||||
&rel.SourceNodeID,
|
|
||||||
&rel.TargetNodeID,
|
|
||||||
&rel.RelationType,
|
|
||||||
&rel.Label,
|
|
||||||
&rel.RelationWording,
|
|
||||||
&rel.Metadata,
|
|
||||||
&rel.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to scan relation: %w", err)
|
|
||||||
}
|
|
||||||
relations = append(relations, rel)
|
|
||||||
}
|
|
||||||
|
|
||||||
return relations, rows.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRelationVersions retrieves version history for a specific relation
|
|
||||||
func (db *DB) GetRelationVersions(ctx context.Context, workflowID string, edgeID string) ([]WorkflowRelationVersion, error) {
|
|
||||||
var versions []WorkflowRelationVersion
|
|
||||||
|
|
||||||
query := `
|
|
||||||
SELECT id, workflow_id, edge_id, version_num, operation, snapshot,
|
|
||||||
changed_at, changed_by, fields_changed
|
|
||||||
FROM workflow_relation_versions
|
|
||||||
WHERE workflow_id = $1 AND edge_id = $2
|
|
||||||
ORDER BY version_num ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
rows, err := db.conn.QueryContext(ctx, query, workflowID, edgeID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query relation versions: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var v WorkflowRelationVersion
|
|
||||||
if err := rows.Scan(
|
|
||||||
&v.ID,
|
|
||||||
&v.WorkflowID,
|
|
||||||
&v.EdgeID,
|
|
||||||
&v.VersionNum,
|
|
||||||
&v.Operation,
|
|
||||||
&v.Snapshot,
|
|
||||||
&v.ChangedAt,
|
|
||||||
&v.ChangedBy,
|
|
||||||
&v.FieldsChanged,
|
|
||||||
); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to scan version: %w", err)
|
|
||||||
}
|
|
||||||
versions = append(versions, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return versions, rows.Err()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ type WorkflowEdge struct {
|
|||||||
|
|
||||||
// Canvas represents the full React Flow canvas (nodes + edges)
|
// Canvas represents the full React Flow canvas (nodes + edges)
|
||||||
type Canvas struct {
|
type Canvas struct {
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Nodes []WorkflowNode `json:"nodes"`
|
Nodes []WorkflowNode `json:"nodes"`
|
||||||
Edges []WorkflowEdge `json:"edges"`
|
Edges []WorkflowEdge `json:"edges"`
|
||||||
}
|
}
|
||||||
@@ -112,30 +111,3 @@ type WorkflowMemoryLink struct {
|
|||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
Notes string `db:"notes"`
|
Notes string `db:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorkflowRelation represents a semantic relation between two canvas nodes
|
|
||||||
type WorkflowRelation struct {
|
|
||||||
ID string `db:"id"`
|
|
||||||
WorkflowID string `db:"workflow_id"`
|
|
||||||
Version int `db:"version"`
|
|
||||||
SourceNodeID string `db:"source_node_id"`
|
|
||||||
TargetNodeID string `db:"target_node_id"`
|
|
||||||
RelationType string `db:"relation_type"` // "data-flow", "dependency", "conditional"
|
|
||||||
Label string `db:"label"` // Human-readable relation description
|
|
||||||
RelationWording []byte `db:"relation_wording"` // JSONB with verb, outputs, inputs, confidence
|
|
||||||
Metadata []byte `db:"metadata"` // JSONB for extensibility
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WorkflowRelationVersion represents versioned history of relation changes
|
|
||||||
type WorkflowRelationVersion struct {
|
|
||||||
ID string `db:"id"`
|
|
||||||
WorkflowID string `db:"workflow_id"`
|
|
||||||
EdgeID string `db:"edge_id"`
|
|
||||||
VersionNum int `db:"version_num"`
|
|
||||||
Operation string `db:"operation"` // "CREATE", "UPDATE", "DELETE"
|
|
||||||
Snapshot []byte `db:"snapshot"` // JSONB full state at this version
|
|
||||||
ChangedAt time.Time `db:"changed_at"`
|
|
||||||
ChangedBy string `db:"changed_by"`
|
|
||||||
FieldsChanged []byte `db:"fields_changed"` // JSONB array of changed field names
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
// Package types defines the shared domain model for Poimen workflows.
|
|
||||||
// Both workflow/ (orchestration) and activity/ (execution) import from here.
|
|
||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ===== LLM Configuration =====
|
|
||||||
|
|
||||||
type ModelSpec struct {
|
|
||||||
ModelID string
|
|
||||||
Thinking string // "adaptive" or ""
|
|
||||||
Effort string // "low", "medium", "high", "xhigh", "max"
|
|
||||||
}
|
|
||||||
|
|
||||||
type PromptSpec struct {
|
|
||||||
TemplateRef string
|
|
||||||
RawTemplate string
|
|
||||||
Variables map[string]any
|
|
||||||
Model ModelSpec
|
|
||||||
LessonsRef string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SkillRef struct {
|
|
||||||
Name string
|
|
||||||
URL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Retry & Tuning =====
|
|
||||||
|
|
||||||
type PiRetryPolicy struct {
|
|
||||||
ScheduleToCloseTimeout time.Duration
|
|
||||||
InitialInterval time.Duration
|
|
||||||
MaximumInterval time.Duration
|
|
||||||
BackoffCoefficient float64
|
|
||||||
StreamTimeout time.Duration
|
|
||||||
StreamTimeoutMax time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActivityTuning struct {
|
|
||||||
ImplementerBaseTimeout time.Duration
|
|
||||||
ImplementerMaxRetries int
|
|
||||||
JudgeTimeout time.Duration
|
|
||||||
PiRetry PiRetryPolicy
|
|
||||||
InitialRetryInterval time.Duration
|
|
||||||
MaxRetryInterval time.Duration
|
|
||||||
RetryBackoffCoefficient float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Orchestrator =====
|
|
||||||
|
|
||||||
type OrchestratorConfig struct {
|
|
||||||
SystemPrompt string
|
|
||||||
Skills []SkillRef
|
|
||||||
RolePrompts map[string]PromptSpec
|
|
||||||
Tuning ActivityTuning
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrchestratorInput struct {
|
|
||||||
TargetRepoPath string
|
|
||||||
RemoteURL string
|
|
||||||
Milestone string
|
|
||||||
Config OrchestratorConfig
|
|
||||||
DryRun bool
|
|
||||||
CycleCount int
|
|
||||||
MaxCyclesBeforeCAN int
|
|
||||||
PiProvider string
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrchestratorOutput struct {
|
|
||||||
MilestoneComplete bool
|
|
||||||
Done bool
|
|
||||||
LastError string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TaskUnit =====
|
|
||||||
|
|
||||||
type TaskUnitInput struct {
|
|
||||||
TaskID string
|
|
||||||
RemoteURL string
|
|
||||||
TargetRepoPath string
|
|
||||||
Milestone string
|
|
||||||
Config OrchestratorConfig
|
|
||||||
DryRun bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskUnitOutput struct {
|
|
||||||
TaskID string
|
|
||||||
Status string
|
|
||||||
Verdict string
|
|
||||||
Critique string
|
|
||||||
Branch string
|
|
||||||
Reason string
|
|
||||||
Changes string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Canvas & Relations =====
|
|
||||||
|
|
||||||
type RelationWording struct {
|
|
||||||
Verb string `json:"verb"`
|
|
||||||
SourceOutput string `json:"source_output"`
|
|
||||||
TargetInput string `json:"target_input"`
|
|
||||||
ConnectionType string `json:"connection_type"`
|
|
||||||
Confidence float64 `json:"confidence"`
|
|
||||||
SemanticMatch string `json:"semantic_match"`
|
|
||||||
TransformerNeeded string `json:"transformer_needed,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EdgeWithWording struct {
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
Source string `json:"source"`
|
|
||||||
Target string `json:"target"`
|
|
||||||
RelationType string `json:"relation_type"`
|
|
||||||
RelationLabel string `json:"relation_label"`
|
|
||||||
RelationWording RelationWording `json:"relation_wording"`
|
|
||||||
CreatedAt string `json:"created_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CanvasWithRelationsData struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
Nodes []db.WorkflowNode `json:"nodes"`
|
|
||||||
Edges []db.WorkflowEdge `json:"edges"`
|
|
||||||
Relations []EdgeWithWording `json:"relations"`
|
|
||||||
UpdatedAt string `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Activity I/O =====
|
|
||||||
|
|
||||||
type FetchCanvasRelationsInput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CanvasReasonerInput struct {
|
|
||||||
Nodes []db.WorkflowNode `json:"nodes"`
|
|
||||||
Edges []db.WorkflowEdge `json:"edges"`
|
|
||||||
PreserveExisting bool `json:"preserve_existing,omitempty"`
|
|
||||||
AuthToken string `json:"auth_token,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryPathData struct {
|
|
||||||
SourceID string `json:"source_id"`
|
|
||||||
TargetID string `json:"target_id"`
|
|
||||||
Distance int `json:"distance"`
|
|
||||||
PathCount int `json:"path_count"`
|
|
||||||
NodeIDs []string `json:"node_ids"`
|
|
||||||
Confidence float64 `json:"total_confidence"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GraphRAGQueryInput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Query string `json:"query"`
|
|
||||||
SearchType string `json:"search_type"`
|
|
||||||
RelationType string `json:"relation_type"`
|
|
||||||
ConfidenceFloor float64 `json:"confidence_floor"`
|
|
||||||
TopK int `json:"top_k"`
|
|
||||||
RankingProfile string `json:"ranking_profile"`
|
|
||||||
Canvas CanvasWithRelationsData `json:"canvas"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GraphRAGQueryOutput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Query string `json:"query"`
|
|
||||||
Edges []EdgeWithWording `json:"edges"`
|
|
||||||
Paths []QueryPathData `json:"paths"`
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
ExecutionMs int64 `json:"execution_time_ms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CanvasCompatibilityInput struct {
|
|
||||||
Nodes []db.WorkflowNode `json:"nodes"`
|
|
||||||
Edges []db.WorkflowEdge `json:"edges"`
|
|
||||||
Query string `json:"query,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IndexGraphRAGInput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
Nodes []db.WorkflowNode `json:"nodes"`
|
|
||||||
Relations []EdgeWithWording `json:"relations"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IndexGraphRAGOutput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
IndexedEntities int `json:"indexed_entities"`
|
|
||||||
IndexedEdges int `json:"indexed_edges"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
GraphRAGChecksum string `json:"graph_rag_checksum"`
|
|
||||||
IndexedAt string `json:"indexed_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PromptUpdate struct {
|
|
||||||
Role string
|
|
||||||
Spec PromptSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Defaults =====
|
|
||||||
|
|
||||||
func NewPiRetryPolicy() PiRetryPolicy {
|
|
||||||
return PiRetryPolicy{
|
|
||||||
ScheduleToCloseTimeout: 5 * time.Minute,
|
|
||||||
InitialInterval: 2 * time.Second,
|
|
||||||
MaximumInterval: 30 * time.Second,
|
|
||||||
BackoffCoefficient: 2.0,
|
|
||||||
StreamTimeout: 30 * time.Second,
|
|
||||||
StreamTimeoutMax: 2 * time.Minute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewActivityTuning() ActivityTuning {
|
|
||||||
return ActivityTuning{
|
|
||||||
ImplementerBaseTimeout: 10 * time.Minute,
|
|
||||||
ImplementerMaxRetries: 3,
|
|
||||||
JudgeTimeout: 5 * time.Minute,
|
|
||||||
PiRetry: NewPiRetryPolicy(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package workflow
|
package statemachine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package workflow
|
package statemachine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package workflow
|
package statemachine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
package workflow
|
package statemachine
|
||||||
|
|
||||||
// Empty stub - will be filled in T0.7
|
// Empty stub - will be filled in T0.7
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package workflow
|
package statemachine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package workflow
|
package statemachine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"go.temporal.io/sdk/workflow"
|
"go.temporal.io/sdk/workflow"
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package statemachine
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ModelSpec defines LLM model configuration.
|
||||||
|
type ModelSpec struct {
|
||||||
|
ModelID string // e.g. "claude-opus-5", "claude-sonnet-5"
|
||||||
|
Thinking string // "adaptive" or ""
|
||||||
|
Effort string // "low", "medium", "high", "xhigh", "max"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptSpec defines a prompt template with variables and model.
|
||||||
|
type PromptSpec struct {
|
||||||
|
TemplateRef string // e.g. "planner/default.tmpl"
|
||||||
|
RawTemplate string // overrides TemplateRef if non-empty
|
||||||
|
Variables map[string]any // template variables
|
||||||
|
Model ModelSpec // which LLM to use
|
||||||
|
LessonsRef string // key into lessons store
|
||||||
|
}
|
||||||
|
|
||||||
|
// PiRetryPolicy defines retry and timeout settings for Pi command execution.
|
||||||
|
type PiRetryPolicy struct {
|
||||||
|
ScheduleToCloseTimeout time.Duration // default: 5m
|
||||||
|
InitialInterval time.Duration // default: 2s
|
||||||
|
MaximumInterval time.Duration // default: 30s
|
||||||
|
BackoffCoefficient float64 // default: 2.0
|
||||||
|
StreamTimeout time.Duration // default: 30s
|
||||||
|
StreamTimeoutMax time.Duration // default: 2m
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityTuning defines timeouts and retry counts for activities.
|
||||||
|
type ActivityTuning struct {
|
||||||
|
ImplementerBaseTimeout time.Duration // default: 10m
|
||||||
|
ImplementerMaxRetries int // default: 3
|
||||||
|
JudgeTimeout time.Duration // default: 5m
|
||||||
|
PiRetry PiRetryPolicy
|
||||||
|
// Retry policy settings
|
||||||
|
InitialRetryInterval time.Duration // default: 2s
|
||||||
|
MaxRetryInterval time.Duration // default: 5m
|
||||||
|
RetryBackoffCoefficient float64 // default: 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrchestratorConfig holds all runtime configuration for the orchestrator.
|
||||||
|
type OrchestratorConfig struct {
|
||||||
|
SystemPrompt string // shared prompt prefix
|
||||||
|
Skills []SkillRef // required skill sources
|
||||||
|
RolePrompts map[string]PromptSpec // per-role: "planner", "judge", "implementer"
|
||||||
|
Tuning ActivityTuning
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrchestratorInput is the input to the Orchestrator workflow.
|
||||||
|
type OrchestratorInput struct {
|
||||||
|
TargetRepoPath string
|
||||||
|
RemoteURL string
|
||||||
|
Milestone string // e.g. "T0"
|
||||||
|
Config OrchestratorConfig
|
||||||
|
DryRun bool
|
||||||
|
CycleCount int
|
||||||
|
MaxCyclesBeforeCAN int // default: 100
|
||||||
|
PiProvider string // pi provider name (e.g., "local-llm"); required for skill preparation
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrchestratorOutput is the output of the Orchestrator workflow.
|
||||||
|
type OrchestratorOutput struct {
|
||||||
|
MilestoneComplete bool
|
||||||
|
Done bool
|
||||||
|
LastError string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskUnitInput is the input to the TaskUnit workflow.
|
||||||
|
type TaskUnitInput struct {
|
||||||
|
TaskID string
|
||||||
|
RemoteURL string
|
||||||
|
TargetRepoPath string
|
||||||
|
Milestone string
|
||||||
|
Config OrchestratorConfig
|
||||||
|
DryRun bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskUnitOutput is the output of the TaskUnit workflow.
|
||||||
|
type TaskUnitOutput struct {
|
||||||
|
TaskID string
|
||||||
|
Status string // "success" or "failed"
|
||||||
|
Verdict string // "pass" or "fail" from judge
|
||||||
|
Critique string // feedback from judge
|
||||||
|
Branch string
|
||||||
|
Reason string // error reason if failed
|
||||||
|
Changes string // summary of changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkillRef references a skill source.
|
||||||
|
type SkillRef struct {
|
||||||
|
Name string // skill identifier
|
||||||
|
URL string // source to clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values for types.
|
||||||
|
const (
|
||||||
|
defaultScheduleToCloseTimeout = 5 * time.Minute
|
||||||
|
defaultInitialInterval = 2 * time.Second
|
||||||
|
defaultMaximumInterval = 30 * time.Second
|
||||||
|
defaultBackoffCoefficient = 2.0
|
||||||
|
defaultStreamTimeout = 30 * time.Second
|
||||||
|
defaultStreamTimeoutMax = 2 * time.Minute
|
||||||
|
defaultImplementerBaseTimeout = 10 * time.Minute
|
||||||
|
defaultImplementerMaxRetries = 3
|
||||||
|
defaultJudgeTimeout = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPiRetryPolicy returns a PiRetryPolicy with defaults.
|
||||||
|
func NewPiRetryPolicy() PiRetryPolicy {
|
||||||
|
return PiRetryPolicy{
|
||||||
|
ScheduleToCloseTimeout: defaultScheduleToCloseTimeout,
|
||||||
|
InitialInterval: defaultInitialInterval,
|
||||||
|
MaximumInterval: defaultMaximumInterval,
|
||||||
|
BackoffCoefficient: defaultBackoffCoefficient,
|
||||||
|
StreamTimeout: defaultStreamTimeout,
|
||||||
|
StreamTimeoutMax: defaultStreamTimeoutMax,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewActivityTuning returns an ActivityTuning with defaults.
|
||||||
|
func NewActivityTuning() ActivityTuning {
|
||||||
|
return ActivityTuning{
|
||||||
|
ImplementerBaseTimeout: defaultImplementerBaseTimeout,
|
||||||
|
ImplementerMaxRetries: defaultImplementerMaxRetries,
|
||||||
|
JudgeTimeout: defaultJudgeTimeout,
|
||||||
|
PiRetry: NewPiRetryPolicy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptUpdate represents an update to a role prompt.
|
||||||
|
type PromptUpdate struct {
|
||||||
|
Role string
|
||||||
|
Spec PromptSpec
|
||||||
|
}
|
||||||
+15
-15
@@ -9,7 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/rockliang/poimen/workflows/activity"
|
"github.com/rockliang/poimen/workflows/action"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGitCloneAndFetch(t *testing.T) {
|
func TestGitCloneAndFetch(t *testing.T) {
|
||||||
@@ -56,7 +56,7 @@ func TestGitCloneAndFetch(t *testing.T) {
|
|||||||
|
|
||||||
// Test clone into empty path
|
// Test clone into empty path
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := activity.CloneRepoActivity(ctx, activity.CloneRepoInput{
|
err := action.CloneRepoActivity(ctx, action.CloneRepoInput{
|
||||||
RemoteURL: sourceDir,
|
RemoteURL: sourceDir,
|
||||||
TargetRepoPath: targetDir,
|
TargetRepoPath: targetDir,
|
||||||
})
|
})
|
||||||
@@ -89,7 +89,7 @@ func TestGitCloneAndFetch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test fetch on existing repo
|
// Test fetch on existing repo
|
||||||
err = activity.CloneRepoActivity(ctx, activity.CloneRepoInput{
|
err = action.CloneRepoActivity(ctx, action.CloneRepoInput{
|
||||||
RemoteURL: sourceDir,
|
RemoteURL: sourceDir,
|
||||||
TargetRepoPath: targetDir,
|
TargetRepoPath: targetDir,
|
||||||
})
|
})
|
||||||
@@ -139,14 +139,14 @@ func TestGitWorktreeAdd(t *testing.T) {
|
|||||||
|
|
||||||
// Clone the repo
|
// Clone the repo
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := activity.CloneRepoActivity(ctx, activity.CloneRepoInput{
|
err := action.CloneRepoActivity(ctx, action.CloneRepoInput{
|
||||||
RemoteURL: sourceDir,
|
RemoteURL: sourceDir,
|
||||||
TargetRepoPath: repoDir,
|
TargetRepoPath: repoDir,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "clone should succeed")
|
assert.NoError(t, err, "clone should succeed")
|
||||||
|
|
||||||
// Test worktree add
|
// Test worktree add
|
||||||
worktreePath, err := activity.GitWorktreeAddActivity(ctx, activity.GitWorktreeAddInput{
|
worktreePath, err := action.GitWorktreeAddActivity(ctx, action.GitWorktreeAddInput{
|
||||||
RepoPath: repoDir,
|
RepoPath: repoDir,
|
||||||
TaskID: "T0.1",
|
TaskID: "T0.1",
|
||||||
})
|
})
|
||||||
@@ -207,14 +207,14 @@ func TestGitCommit(t *testing.T) {
|
|||||||
|
|
||||||
// Clone the repo
|
// Clone the repo
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := activity.CloneRepoActivity(ctx, activity.CloneRepoInput{
|
err := action.CloneRepoActivity(ctx, action.CloneRepoInput{
|
||||||
RemoteURL: sourceDir,
|
RemoteURL: sourceDir,
|
||||||
TargetRepoPath: repoDir,
|
TargetRepoPath: repoDir,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "clone should succeed")
|
assert.NoError(t, err, "clone should succeed")
|
||||||
|
|
||||||
// Create a worktree
|
// Create a worktree
|
||||||
worktreePath, err := activity.GitWorktreeAddActivity(ctx, activity.GitWorktreeAddInput{
|
worktreePath, err := action.GitWorktreeAddActivity(ctx, action.GitWorktreeAddInput{
|
||||||
RepoPath: repoDir,
|
RepoPath: repoDir,
|
||||||
TaskID: "T0.1",
|
TaskID: "T0.1",
|
||||||
})
|
})
|
||||||
@@ -227,7 +227,7 @@ func TestGitCommit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Commit changes
|
// Commit changes
|
||||||
err = activity.GitCommitActivity(ctx, activity.GitCommitInput{
|
err = action.GitCommitActivity(ctx, action.GitCommitInput{
|
||||||
WorktreePath: worktreePath,
|
WorktreePath: worktreePath,
|
||||||
Message: "Add new file",
|
Message: "Add new file",
|
||||||
})
|
})
|
||||||
@@ -283,14 +283,14 @@ func TestGitDiff(t *testing.T) {
|
|||||||
|
|
||||||
// Clone the repo
|
// Clone the repo
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := activity.CloneRepoActivity(ctx, activity.CloneRepoInput{
|
err := action.CloneRepoActivity(ctx, action.CloneRepoInput{
|
||||||
RemoteURL: sourceDir,
|
RemoteURL: sourceDir,
|
||||||
TargetRepoPath: repoDir,
|
TargetRepoPath: repoDir,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "clone should succeed")
|
assert.NoError(t, err, "clone should succeed")
|
||||||
|
|
||||||
// Create a worktree
|
// Create a worktree
|
||||||
worktreePath, err := activity.GitWorktreeAddActivity(ctx, activity.GitWorktreeAddInput{
|
worktreePath, err := action.GitWorktreeAddActivity(ctx, action.GitWorktreeAddInput{
|
||||||
RepoPath: repoDir,
|
RepoPath: repoDir,
|
||||||
TaskID: "T0.1",
|
TaskID: "T0.1",
|
||||||
})
|
})
|
||||||
@@ -309,7 +309,7 @@ func TestGitDiff(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get diff (should show the staged change)
|
// Get diff (should show the staged change)
|
||||||
diffOutput, err := activity.GitDiffActivity(ctx, activity.GitDiffInput{
|
diffOutput, err := action.GitDiffActivity(ctx, action.GitDiffInput{
|
||||||
WorktreePath: worktreePath,
|
WorktreePath: worktreePath,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "diff should succeed")
|
assert.NoError(t, err, "diff should succeed")
|
||||||
@@ -378,7 +378,7 @@ func TestGitSquashMerge(t *testing.T) {
|
|||||||
|
|
||||||
// Clone for the orchestrator to use
|
// Clone for the orchestrator to use
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := activity.CloneRepoActivity(ctx, activity.CloneRepoInput{
|
err := action.CloneRepoActivity(ctx, action.CloneRepoInput{
|
||||||
RemoteURL: sourceDir,
|
RemoteURL: sourceDir,
|
||||||
TargetRepoPath: repoDir,
|
TargetRepoPath: repoDir,
|
||||||
})
|
})
|
||||||
@@ -387,7 +387,7 @@ func TestGitSquashMerge(t *testing.T) {
|
|||||||
// Create multiple worktrees with changes
|
// Create multiple worktrees with changes
|
||||||
for i := 1; i <= 2; i++ {
|
for i := 1; i <= 2; i++ {
|
||||||
taskID := fmt.Sprintf("T0.%d", i)
|
taskID := fmt.Sprintf("T0.%d", i)
|
||||||
worktreePath, err := activity.GitWorktreeAddActivity(ctx, activity.GitWorktreeAddInput{
|
worktreePath, err := action.GitWorktreeAddActivity(ctx, action.GitWorktreeAddInput{
|
||||||
RepoPath: repoDir,
|
RepoPath: repoDir,
|
||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
})
|
})
|
||||||
@@ -400,7 +400,7 @@ func TestGitSquashMerge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Commit changes
|
// Commit changes
|
||||||
err = activity.GitCommitActivity(ctx, activity.GitCommitInput{
|
err = action.GitCommitActivity(ctx, action.GitCommitInput{
|
||||||
WorktreePath: worktreePath,
|
WorktreePath: worktreePath,
|
||||||
Message: fmt.Sprintf("Task %s implementation", taskID),
|
Message: fmt.Sprintf("Task %s implementation", taskID),
|
||||||
})
|
})
|
||||||
@@ -408,7 +408,7 @@ func TestGitSquashMerge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform squash merge
|
// Perform squash merge
|
||||||
err = activity.GitSquashMergeActivity(ctx, activity.GitSquashMergeInput{
|
err = action.GitSquashMergeActivity(ctx, action.GitSquashMergeInput{
|
||||||
RepoPath: repoDir,
|
RepoPath: repoDir,
|
||||||
Branches: []string{"task/T0.1", "task/T0.2"},
|
Branches: []string{"task/T0.1", "task/T0.2"},
|
||||||
Message: "Milestone T0: completed all tasks",
|
Message: "Milestone T0: completed all tasks",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||||
"github.com/rockliang/poimen/workflows/workflow"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.temporal.io/sdk/testsuite"
|
"go.temporal.io/sdk/testsuite"
|
||||||
)
|
)
|
||||||
@@ -45,14 +45,14 @@ func TestRoutingWorkflow_SimpleWorkflow(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
input := workflow.RoutingWorkflowInput{Spec: spec}
|
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||||
|
|
||||||
env.ExecuteWorkflow(workflow.RoutingWorkflow, input)
|
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||||
|
|
||||||
require.True(t, env.IsWorkflowCompleted())
|
require.True(t, env.IsWorkflowCompleted())
|
||||||
require.NoError(t, env.GetWorkflowError())
|
require.NoError(t, env.GetWorkflowError())
|
||||||
|
|
||||||
var output workflow.RoutingWorkflowOutput
|
var output statemachine.RoutingWorkflowOutput
|
||||||
require.NoError(t, env.GetWorkflowResult(&output))
|
require.NoError(t, env.GetWorkflowResult(&output))
|
||||||
require.Equal(t, "COMPLETED", output.Status)
|
require.Equal(t, "COMPLETED", output.Status)
|
||||||
require.NotNil(t, output.FinalOutput)
|
require.NotNil(t, output.FinalOutput)
|
||||||
@@ -96,14 +96,14 @@ func TestRoutingWorkflow_MultiStepWorkflow(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
input := workflow.RoutingWorkflowInput{Spec: spec}
|
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||||
|
|
||||||
env.ExecuteWorkflow(workflow.RoutingWorkflow, input)
|
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||||
|
|
||||||
require.True(t, env.IsWorkflowCompleted())
|
require.True(t, env.IsWorkflowCompleted())
|
||||||
require.NoError(t, env.GetWorkflowError())
|
require.NoError(t, env.GetWorkflowError())
|
||||||
|
|
||||||
var output workflow.RoutingWorkflowOutput
|
var output statemachine.RoutingWorkflowOutput
|
||||||
require.NoError(t, env.GetWorkflowResult(&output))
|
require.NoError(t, env.GetWorkflowResult(&output))
|
||||||
t.Logf("Output: %+v", output)
|
t.Logf("Output: %+v", output)
|
||||||
t.Logf("Error: %s", output.Error)
|
t.Logf("Error: %s", output.Error)
|
||||||
@@ -130,14 +130,14 @@ func TestRoutingWorkflow_PassState(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
input := workflow.RoutingWorkflowInput{Spec: spec}
|
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||||
|
|
||||||
env.ExecuteWorkflow(workflow.RoutingWorkflow, input)
|
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||||
|
|
||||||
require.True(t, env.IsWorkflowCompleted())
|
require.True(t, env.IsWorkflowCompleted())
|
||||||
require.NoError(t, env.GetWorkflowError())
|
require.NoError(t, env.GetWorkflowError())
|
||||||
|
|
||||||
var output workflow.RoutingWorkflowOutput
|
var output statemachine.RoutingWorkflowOutput
|
||||||
require.NoError(t, env.GetWorkflowResult(&output))
|
require.NoError(t, env.GetWorkflowResult(&output))
|
||||||
require.Equal(t, "COMPLETED", output.Status)
|
require.Equal(t, "COMPLETED", output.Status)
|
||||||
}
|
}
|
||||||
@@ -160,14 +160,14 @@ func TestRoutingWorkflow_FailState(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
input := workflow.RoutingWorkflowInput{Spec: spec}
|
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||||
|
|
||||||
env.ExecuteWorkflow(workflow.RoutingWorkflow, input)
|
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||||
|
|
||||||
require.True(t, env.IsWorkflowCompleted())
|
require.True(t, env.IsWorkflowCompleted())
|
||||||
require.NoError(t, env.GetWorkflowError())
|
require.NoError(t, env.GetWorkflowError())
|
||||||
|
|
||||||
var output workflow.RoutingWorkflowOutput
|
var output statemachine.RoutingWorkflowOutput
|
||||||
require.NoError(t, env.GetWorkflowResult(&output))
|
require.NoError(t, env.GetWorkflowResult(&output))
|
||||||
require.Equal(t, "FAILED", output.Status)
|
require.Equal(t, "FAILED", output.Status)
|
||||||
require.Contains(t, output.Error, "WorkflowError")
|
require.Contains(t, output.Error, "WorkflowError")
|
||||||
@@ -219,14 +219,14 @@ func TestRoutingWorkflow_ErrorCatch(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
input := workflow.RoutingWorkflowInput{Spec: spec}
|
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||||
|
|
||||||
env.ExecuteWorkflow(workflow.RoutingWorkflow, input)
|
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||||
|
|
||||||
require.True(t, env.IsWorkflowCompleted())
|
require.True(t, env.IsWorkflowCompleted())
|
||||||
require.NoError(t, env.GetWorkflowError())
|
require.NoError(t, env.GetWorkflowError())
|
||||||
|
|
||||||
var output workflow.RoutingWorkflowOutput
|
var output statemachine.RoutingWorkflowOutput
|
||||||
require.NoError(t, env.GetWorkflowResult(&output))
|
require.NoError(t, env.GetWorkflowResult(&output))
|
||||||
require.Equal(t, "FAILED", output.Status)
|
require.Equal(t, "FAILED", output.Status)
|
||||||
require.Contains(t, output.Error, "CaughtError")
|
require.Contains(t, output.Error, "CaughtError")
|
||||||
@@ -237,14 +237,14 @@ func TestRoutingWorkflow_EmptySpec(t *testing.T) {
|
|||||||
env := testSuite.NewTestWorkflowEnvironment()
|
env := testSuite.NewTestWorkflowEnvironment()
|
||||||
|
|
||||||
// Empty spec
|
// Empty spec
|
||||||
input := workflow.RoutingWorkflowInput{Spec: nil}
|
input := statemachine.RoutingWorkflowInput{Spec: nil}
|
||||||
|
|
||||||
env.ExecuteWorkflow(workflow.RoutingWorkflow, input)
|
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
|
||||||
|
|
||||||
require.True(t, env.IsWorkflowCompleted())
|
require.True(t, env.IsWorkflowCompleted())
|
||||||
require.NoError(t, env.GetWorkflowError())
|
require.NoError(t, env.GetWorkflowError())
|
||||||
|
|
||||||
var output workflow.RoutingWorkflowOutput
|
var output statemachine.RoutingWorkflowOutput
|
||||||
require.NoError(t, env.GetWorkflowResult(&output))
|
require.NoError(t, env.GetWorkflowResult(&output))
|
||||||
require.Equal(t, "FAILED", output.Status)
|
require.Equal(t, "FAILED", output.Status)
|
||||||
require.Contains(t, output.Error, "empty")
|
require.Contains(t, output.Error, "empty")
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.temporal.io/sdk/client"
|
"go.temporal.io/sdk/client"
|
||||||
"github.com/rockliang/poimen/workflows/internal/config"
|
"github.com/rockliang/poimen/workflows/internal/config"
|
||||||
"github.com/rockliang/poimen/workflows/workflow"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestTemporalConnection verifies the worker is connected and healthy
|
// TestTemporalConnection verifies the worker is connected and healthy
|
||||||
@@ -67,7 +67,7 @@ func TestActivityExecution(t *testing.T) {
|
|||||||
runResp, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
runResp, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||||
ID: workflowID,
|
ID: workflowID,
|
||||||
TaskQueue: "poimen-taskqueue",
|
TaskQueue: "poimen-taskqueue",
|
||||||
}, workflow.TestWorkflow)
|
}, statemachine.TestWorkflow)
|
||||||
|
|
||||||
assert.NoError(t, err, "failed to execute test workflow")
|
assert.NoError(t, err, "failed to execute test workflow")
|
||||||
assert.NotNil(t, runResp, "workflow response should not be nil")
|
assert.NotNil(t, runResp, "workflow response should not be nil")
|
||||||
@@ -136,28 +136,28 @@ func TestOrchestratorWorkflowIntegration(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Create minimal orchestrator input
|
// Create minimal orchestrator input
|
||||||
input := workflow.OrchestratorInput{
|
input := statemachine.OrchestratorInput{
|
||||||
RemoteURL: "https://forgejo.riotpiao.com/rock/poimen",
|
RemoteURL: "https://forgejo.riotpiao.com/rock/poimen",
|
||||||
TargetRepoPath: "/tmp/test-poimen-integration",
|
TargetRepoPath: "/tmp/test-poimen-integration",
|
||||||
Milestone: "T0",
|
Milestone: "T0",
|
||||||
Config: workflow.OrchestratorConfig{
|
Config: statemachine.OrchestratorConfig{
|
||||||
SystemPrompt: "You are a code generation assistant. Generate simple test code.",
|
SystemPrompt: "You are a code generation assistant. Generate simple test code.",
|
||||||
RolePrompts: map[string]workflow.PromptSpec{
|
RolePrompts: map[string]statemachine.PromptSpec{
|
||||||
"planner": {
|
"planner": {
|
||||||
TemplateRef: "planner/default.tmpl",
|
TemplateRef: "planner/default.tmpl",
|
||||||
Model: workflow.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: "ornith",
|
ModelID: "ornith",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"judge": {
|
"judge": {
|
||||||
TemplateRef: "judge/default.tmpl",
|
TemplateRef: "judge/default.tmpl",
|
||||||
Model: workflow.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: "ornith",
|
ModelID: "ornith",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"implementer": {
|
"implementer": {
|
||||||
TemplateRef: "implementer/default.tmpl",
|
TemplateRef: "implementer/default.tmpl",
|
||||||
Model: workflow.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: "claude-sonnet-5",
|
ModelID: "claude-sonnet-5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -170,7 +170,7 @@ func TestOrchestratorWorkflowIntegration(t *testing.T) {
|
|||||||
runResp, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
runResp, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||||
ID: workflowID,
|
ID: workflowID,
|
||||||
TaskQueue: "poimen-taskqueue",
|
TaskQueue: "poimen-taskqueue",
|
||||||
}, workflow.OrchestratorWorkflow, input)
|
}, statemachine.OrchestratorWorkflow, input)
|
||||||
|
|
||||||
assert.NoError(t, err, "failed to execute orchestrator workflow")
|
assert.NoError(t, err, "failed to execute orchestrator workflow")
|
||||||
t.Logf("✅ Orchestrator workflow started: %s", workflowID)
|
t.Logf("✅ Orchestrator workflow started: %s", workflowID)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/internal/routing"
|
"github.com/rockliang/poimen/workflows/internal/routing"
|
||||||
"github.com/rockliang/poimen/workflows/workflow"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.temporal.io/sdk/client"
|
"go.temporal.io/sdk/client"
|
||||||
)
|
)
|
||||||
@@ -69,12 +69,12 @@ func TestTemporalRoutingWorkflow(t *testing.T) {
|
|||||||
|
|
||||||
// Submit to Temporal
|
// Submit to Temporal
|
||||||
workflowID := "test-routing-" + time.Now().Format("20060102-150405")
|
workflowID := "test-routing-" + time.Now().Format("20060102-150405")
|
||||||
input := workflow.RoutingWorkflowInput{Spec: output.Spec}
|
input := statemachine.RoutingWorkflowInput{Spec: output.Spec}
|
||||||
|
|
||||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||||
ID: workflowID,
|
ID: workflowID,
|
||||||
TaskQueue: "poimen-taskqueue",
|
TaskQueue: "poimen-taskqueue",
|
||||||
}, workflow.RoutingWorkflow, input)
|
}, statemachine.RoutingWorkflow, input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Logf("Workflow submitted: ID=%s, RunID=%s", run.GetID(), run.GetRunID())
|
t.Logf("Workflow submitted: ID=%s, RunID=%s", run.GetID(), run.GetRunID())
|
||||||
@@ -118,18 +118,18 @@ func TestTemporalRoutingWorkflow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workflowID := "test-pass-only-" + time.Now().Format("20060102-150405")
|
workflowID := "test-pass-only-" + time.Now().Format("20060102-150405")
|
||||||
input := workflow.RoutingWorkflowInput{Spec: spec}
|
input := statemachine.RoutingWorkflowInput{Spec: spec}
|
||||||
|
|
||||||
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
||||||
ID: workflowID,
|
ID: workflowID,
|
||||||
TaskQueue: "poimen-taskqueue",
|
TaskQueue: "poimen-taskqueue",
|
||||||
}, workflow.RoutingWorkflow, input)
|
}, statemachine.RoutingWorkflow, input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Logf("Pass-only workflow submitted: ID=%s", run.GetID())
|
t.Logf("Pass-only workflow submitted: ID=%s", run.GetID())
|
||||||
|
|
||||||
// Wait for result (Pass states don't need workers)
|
// Wait for result (Pass states don't need workers)
|
||||||
var result workflow.RoutingWorkflowOutput
|
var result statemachine.RoutingWorkflowOutput
|
||||||
err = run.Get(ctx, &result)
|
err = run.Get(ctx, &result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -5,12 +5,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/rockliang/poimen/workflows/workflow"
|
"github.com/rockliang/poimen/workflows/statemachine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTypesDefaults(t *testing.T) {
|
func TestTypesDefaults(t *testing.T) {
|
||||||
// Test PiRetryPolicy defaults
|
// Test PiRetryPolicy defaults
|
||||||
pr := workflow.NewPiRetryPolicy()
|
pr := statemachine.NewPiRetryPolicy()
|
||||||
assert.Equal(t, 5*time.Minute, pr.ScheduleToCloseTimeout, "ScheduleToCloseTimeout should be 5m")
|
assert.Equal(t, 5*time.Minute, pr.ScheduleToCloseTimeout, "ScheduleToCloseTimeout should be 5m")
|
||||||
assert.Equal(t, 2*time.Second, pr.InitialInterval, "InitialInterval should be 2s")
|
assert.Equal(t, 2*time.Second, pr.InitialInterval, "InitialInterval should be 2s")
|
||||||
assert.Equal(t, 30*time.Second, pr.MaximumInterval, "MaximumInterval should be 30s")
|
assert.Equal(t, 30*time.Second, pr.MaximumInterval, "MaximumInterval should be 30s")
|
||||||
@@ -19,7 +19,7 @@ func TestTypesDefaults(t *testing.T) {
|
|||||||
assert.Equal(t, 2*time.Minute, pr.StreamTimeoutMax, "StreamTimeoutMax should be 2m")
|
assert.Equal(t, 2*time.Minute, pr.StreamTimeoutMax, "StreamTimeoutMax should be 2m")
|
||||||
|
|
||||||
// Test ActivityTuning defaults
|
// Test ActivityTuning defaults
|
||||||
at := workflow.NewActivityTuning()
|
at := statemachine.NewActivityTuning()
|
||||||
assert.Equal(t, 10*time.Minute, at.ImplementerBaseTimeout, "ImplementerBaseTimeout should be 10m")
|
assert.Equal(t, 10*time.Minute, at.ImplementerBaseTimeout, "ImplementerBaseTimeout should be 10m")
|
||||||
assert.Equal(t, 3, at.ImplementerMaxRetries, "ImplementerMaxRetries should be 3")
|
assert.Equal(t, 3, at.ImplementerMaxRetries, "ImplementerMaxRetries should be 3")
|
||||||
assert.Equal(t, 5*time.Minute, at.JudgeTimeout, "JudgeTimeout should be 5m")
|
assert.Equal(t, 5*time.Minute, at.JudgeTimeout, "JudgeTimeout should be 5m")
|
||||||
@@ -31,7 +31,7 @@ func TestTypesDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestModelSpec(t *testing.T) {
|
func TestModelSpec(t *testing.T) {
|
||||||
spec := workflow.ModelSpec{
|
spec := statemachine.ModelSpec{
|
||||||
ModelID: "claude-opus-5",
|
ModelID: "claude-opus-5",
|
||||||
Thinking: "adaptive",
|
Thinking: "adaptive",
|
||||||
Effort: "high",
|
Effort: "high",
|
||||||
@@ -42,13 +42,13 @@ func TestModelSpec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPromptSpec(t *testing.T) {
|
func TestPromptSpec(t *testing.T) {
|
||||||
spec := workflow.PromptSpec{
|
spec := statemachine.PromptSpec{
|
||||||
TemplateRef: "planner/default.tmpl",
|
TemplateRef: "planner/default.tmpl",
|
||||||
RawTemplate: "",
|
RawTemplate: "",
|
||||||
Variables: map[string]any{
|
Variables: map[string]any{
|
||||||
"key": "value",
|
"key": "value",
|
||||||
},
|
},
|
||||||
Model: workflow.ModelSpec{
|
Model: statemachine.ModelSpec{
|
||||||
ModelID: "claude-opus-5",
|
ModelID: "claude-opus-5",
|
||||||
},
|
},
|
||||||
LessonsRef: "T0.1",
|
LessonsRef: "T0.1",
|
||||||
@@ -61,18 +61,18 @@ func TestPromptSpec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOrchestratorConfig(t *testing.T) {
|
func TestOrchestratorConfig(t *testing.T) {
|
||||||
cfg := workflow.OrchestratorConfig{
|
cfg := statemachine.OrchestratorConfig{
|
||||||
SystemPrompt: "You are an expert",
|
SystemPrompt: "You are an expert",
|
||||||
Skills: []workflow.SkillRef{
|
Skills: []statemachine.SkillRef{
|
||||||
{Name: "golang-skills", URL: "https://example.com/skill1"},
|
{Name: "golang-skills", URL: "https://example.com/skill1"},
|
||||||
},
|
},
|
||||||
RolePrompts: map[string]workflow.PromptSpec{
|
RolePrompts: map[string]statemachine.PromptSpec{
|
||||||
"planner": {
|
"planner": {
|
||||||
TemplateRef: "planner/default.tmpl",
|
TemplateRef: "planner/default.tmpl",
|
||||||
Model: workflow.ModelSpec{ModelID: "claude-opus-5"},
|
Model: statemachine.ModelSpec{ModelID: "claude-opus-5"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Tuning: workflow.NewActivityTuning(),
|
Tuning: statemachine.NewActivityTuning(),
|
||||||
}
|
}
|
||||||
assert.Equal(t, "You are an expert", cfg.SystemPrompt)
|
assert.Equal(t, "You are an expert", cfg.SystemPrompt)
|
||||||
assert.Len(t, cfg.Skills, 1)
|
assert.Len(t, cfg.Skills, 1)
|
||||||
@@ -81,7 +81,7 @@ func TestOrchestratorConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTaskUnitInput(t *testing.T) {
|
func TestTaskUnitInput(t *testing.T) {
|
||||||
input := workflow.TaskUnitInput{
|
input := statemachine.TaskUnitInput{
|
||||||
TaskID: "T0.1",
|
TaskID: "T0.1",
|
||||||
RemoteURL: "https://github.com/example/repo",
|
RemoteURL: "https://github.com/example/repo",
|
||||||
TargetRepoPath: "/tmp/repo",
|
TargetRepoPath: "/tmp/repo",
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import "github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
|
|
||||||
// Re-export from pkg/types — single source of truth.
|
|
||||||
type ModelSpec = types.ModelSpec
|
|
||||||
type PromptSpec = types.PromptSpec
|
|
||||||
type SkillRef = types.SkillRef
|
|
||||||
type PiRetryPolicy = types.PiRetryPolicy
|
|
||||||
type ActivityTuning = types.ActivityTuning
|
|
||||||
type OrchestratorConfig = types.OrchestratorConfig
|
|
||||||
type OrchestratorInput = types.OrchestratorInput
|
|
||||||
type OrchestratorOutput = types.OrchestratorOutput
|
|
||||||
type TaskUnitInput = types.TaskUnitInput
|
|
||||||
type TaskUnitOutput = types.TaskUnitOutput
|
|
||||||
type PromptUpdate = types.PromptUpdate
|
|
||||||
type EdgeWithWording = types.EdgeWithWording
|
|
||||||
|
|
||||||
var NewPiRetryPolicy = types.NewPiRetryPolicy
|
|
||||||
var NewActivityTuning = types.NewActivityTuning
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package workflow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.temporal.io/sdk/temporal"
|
|
||||||
"go.temporal.io/sdk/workflow"
|
|
||||||
|
|
||||||
"github.com/rockliang/poimen/workflows/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WorkflowGraphQueryInput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Query string `json:"query"`
|
|
||||||
SearchType string `json:"search_type"`
|
|
||||||
RelationType string `json:"relation_type"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
ConfidenceFloor float64 `json:"confidence_floor"`
|
|
||||||
TopK int `json:"top_k"`
|
|
||||||
FindPaths bool `json:"find_paths"`
|
|
||||||
TargetNodeID string `json:"target_node_id"`
|
|
||||||
MaxPathDepth int `json:"max_path_depth"`
|
|
||||||
RankingProfile string `json:"ranking_profile"`
|
|
||||||
IncludeReasoning bool `json:"include_reasoning"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkflowGraphQueryOutput struct {
|
|
||||||
WorkflowID string `json:"workflow_id"`
|
|
||||||
Query string `json:"query"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
ExecutionTimeMs int64 `json:"execution_time_ms"`
|
|
||||||
Results []types.EdgeWithWording `json:"results"`
|
|
||||||
Paths []QueryPath `json:"paths"`
|
|
||||||
TotalCount int `json:"total_count"`
|
|
||||||
HasMore bool `json:"has_more"`
|
|
||||||
RankingProfile string `json:"ranking_profile"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryPath struct {
|
|
||||||
SourceID string `json:"source_id"`
|
|
||||||
TargetID string `json:"target_id"`
|
|
||||||
Distance int `json:"distance"`
|
|
||||||
PathCount int `json:"path_count"`
|
|
||||||
NodeIDs []string `json:"node_ids"`
|
|
||||||
Confidence float64 `json:"total_confidence"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func WorkflowGraphQuery(ctx workflow.Context, input WorkflowGraphQueryInput) (WorkflowGraphQueryOutput, error) {
|
|
||||||
startTime := time.Now()
|
|
||||||
output := WorkflowGraphQueryOutput{
|
|
||||||
WorkflowID: input.WorkflowID,
|
|
||||||
Query: input.Query,
|
|
||||||
Version: input.Version,
|
|
||||||
RankingProfile: input.RankingProfile,
|
|
||||||
Results: []types.EdgeWithWording{},
|
|
||||||
Paths: []QueryPath{},
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := workflow.ActivityOptions{
|
|
||||||
StartToCloseTimeout: 120 * time.Second,
|
|
||||||
RetryPolicy: &temporal.RetryPolicy{
|
|
||||||
InitialInterval: 2 * time.Second,
|
|
||||||
BackoffCoefficient: 2.0,
|
|
||||||
MaximumInterval: 10 * time.Second,
|
|
||||||
MaximumAttempts: 3,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ctx = workflow.WithActivityOptions(ctx, opts)
|
|
||||||
|
|
||||||
var canvasData types.CanvasWithRelationsData
|
|
||||||
err := workflow.ExecuteActivity(ctx, "FetchCanvasRelationsActivity",
|
|
||||||
types.FetchCanvasRelationsInput{
|
|
||||||
WorkflowID: input.WorkflowID,
|
|
||||||
Version: input.Version,
|
|
||||||
},
|
|
||||||
).Get(ctx, &canvasData)
|
|
||||||
if err != nil {
|
|
||||||
return output, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var graphResults types.GraphRAGQueryOutput
|
|
||||||
err = workflow.ExecuteActivity(ctx, "QueryGraphRAGActivity",
|
|
||||||
types.GraphRAGQueryInput{
|
|
||||||
WorkflowID: input.WorkflowID,
|
|
||||||
Query: input.Query,
|
|
||||||
SearchType: input.SearchType,
|
|
||||||
RelationType: input.RelationType,
|
|
||||||
ConfidenceFloor: input.ConfidenceFloor,
|
|
||||||
TopK: input.TopK,
|
|
||||||
RankingProfile: input.RankingProfile,
|
|
||||||
Canvas: canvasData,
|
|
||||||
},
|
|
||||||
).Get(ctx, &graphResults)
|
|
||||||
if err != nil {
|
|
||||||
return output, err
|
|
||||||
}
|
|
||||||
|
|
||||||
output.Results = graphResults.Edges
|
|
||||||
output.TotalCount = graphResults.TotalCount
|
|
||||||
output.HasMore = graphResults.HasMore
|
|
||||||
output.ExecutionTimeMs = time.Since(startTime).Milliseconds()
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user