Status: superseded by
pa-agent. This v2 (TypeScript) implementation is kept for reference and remains functional, but the active development line is the Python rewrite inpa-agent/. The two share the same design — three-layer local-IO · model-gateway · tool-runtime — but the new project has a cleaner pipeline, real SSE streaming, and a Streamlit web UI. See the pa-agent README for the current build.
A self-hosted AI agent with a 6-stage pipeline — understand → plan → execute → validate → reflect — wired to real tools and 10 LLM providers. React web UI + CLI + SDK, all talking to the same core.
I got tired of agent demos where the tool calls were hard-coded mocks.
This one calls real endpoints. If your shell command fails, you see the
real error. If a file write fails, you see the real EACCES. No
"success" path that's secretly a stub.
- 6-stage pipeline with live progress events you can hook a UI onto.
- 10 LLM providers — OpenAI, Anthropic, Google, DeepSeek, SiliconFlow, Aliyun, Zhipu, Baidu, Ollama, LM Studio.
- 7 real tools —
writeFile,readFile,shell,search,remember,analyze,respond. No mock layer. - React web UI with SSE streaming, 13 pages, a knowledge graph view, a personality switcher, an emotion-state panel.
- CLI for interactive chat and one-shot agent tasks.
- SDK (
@personal-assistant/sdk) so you can embed the agent in your own app without forking the runtime. - i18n: English + Simplified Chinese (348 keys).
- Clean Architecture: ports, adapters, pure domain logic. The core has zero framework dependencies; the web UI is one adapter among many.
git clone https://github.com/badhope/Personal-Assistant.git cd Personal-Assistant npm run install:all # edit .env with your LLM provider key npm run dev:web # React UI on :5173 npm run dev:cli # interactive CLI
- 6-stage pipeline — understand → plan → execute → validate → reflect, with live progress events
- 10 LLM providers — OpenAI, Anthropic, Google, DeepSeek, SiliconFlow, Aliyun, Zhipu, Baidu, Ollama, LM Studio
- 7 real tools — writeFile, readFile, shell, search, remember, analyze, respond
- React web UI — 13 pages with SSE streaming, knowledge graph, personality switcher, emotion state
- CLI — interactive chat and one-shot agent tasks
- SDK —
@personal-assistant/sdkfor embedding in your own app - i18n — English and Simplified Chinese (348 keys)
- Clean Architecture — ports, adapters, pure domain logic; the core has zero framework dependencies
git clone https://github.com/badhope/Personal-Assistant.git cd Personal-Assistant npm run install:all npm run build npm run web:build npm run server # Express on :3000, override with PORT
Open http://localhost:3000.
export SILICONFLOW_API_KEY=sk-your-real-key npx tsx src/cli/index.ts agent "Introduce yourself in one sentence"
node dist/cli/index.js config set-key siliconflow sk-your-real-key node dist/cli/index.js config set-default siliconflow node dist/cli/index.js chat
Why
npm run install:alland not justnpm install? Root andfrontend/each have their ownpackage.json(this is not a workspaces monorepo). A plainnpm installat the root gets the CLI and the server but leaves the web UI'snode_modulesempty, sonpm run web:dev/npm run web:buildfail withCannot find module 'react'.install:allruns both installs.
| Route | What it does | Backend |
|---|---|---|
/dashboard |
Counts: chats, memories, agent runs, provider readiness | /api/dashboard, /api/config/snapshot |
/chat, /chat/:id |
Real SSE streaming, 10-model switcher | /api/chat/stream (SSE) |
/agent |
6-stage pipeline with live progress | /api/agent/run (SSE) |
/agents |
Agent management and configuration | /api/agents |
/tools |
The 7 tools, with execute buttons | /api/tools, /api/tools/execute |
/tool-templates |
Pre-built tool templates for common tasks | /api/tool-templates |
/memory |
Browse / search / tag-filter / delete / clear long-term memory | /api/memory/* |
/config |
10 providers, key management, live connectivity test | /api/config/* |
/graph |
Canvas 2D force-directed knowledge graph | /api/graph |
/settings |
4 personality switcher, emotion state, theme | /api/personality, /api/emotion |
/personality |
Personality configuration and system prompts | /api/personality |
/emotion |
Emotion state visualization and history | /api/emotion |
/skills |
Skill management and configuration | /api/skills |
The project has no mock layer. When an LLM call fails (401, 403, network error, rate limit), the caller sees the real error — there is no synthetic "success" path that hides it.
| Provider | Type | Default base URL | Default model | Protocol | API key |
|---|---|---|---|---|---|
openai |
Cloud | https://api.openai.com/v1 |
gpt-4o |
OpenAI | Required |
anthropic |
Cloud | https://api.anthropic.com |
claude-3-5-sonnet-... |
Anthropic | Required |
google |
Cloud | https://generativelanguage.googleapis.com/v1beta |
gemini-2.0-flash |
Required | |
deepseek |
Cloud | https://api.deepseek.com/v1 |
deepseek-chat |
OpenAI-compatible | Required |
siliconflow |
Cloud | https://api.siliconflow.cn/v1 |
Qwen/Qwen2.5-7B-Instruct |
OpenAI-compatible | Required |
aliyun |
Cloud | https://dashscope.aliyuncs.com/compatible-mode/v1 |
qwen-plus |
OpenAI-compatible | Required |
zhipu |
Cloud | https://open.bigmodel.cn/api/paas/v4 |
glm-4 |
OpenAI-compatible | Required |
baidu |
Cloud | https://aip.baidubce.com/rpc/2.0/ai_custom/v1 |
ernie-4.0-8k |
Baidu | Required |
ollama |
Local | http://localhost:11434/v1 |
llama3.2 |
OpenAI-compatible | Not needed |
lmstudio |
Local | http://localhost:1234/v1 |
local-model |
OpenAI-compatible | Not needed |
Default: siliconflow — Chinese vendor, free tier, ships Qwen2.5 / DeepSeek / GLM.
node dist/cli/index.js config set-key <provider> <YOUR_API_KEY> node dist/cli/index.js config set-default <provider> node dist/cli/index.js config show node dist/cli/index.js config providers
Keys are stored in conf (default ~/.config/personal-assistant-nodejs/config.json, mode 0600).
Lookup order: conf → PERSONAL_ASSISTANT_<PROVIDER> → <PROVIDER>_API_KEY.
export OPENAI_API_KEY=sk-xxxxxxxx node dist/cli/index.js chat -p openai export ANTHROPIC_API_KEY=sk-ant-xxxxxxxx node dist/cli/index.js agent "Write a quicksort" -p anthropic export SILICONFLOW_API_KEY=sk-xxxxxxxx node dist/cli/index.js chat export DEEPSEEK_API_KEY=sk-xxxxxxxx node dist/cli/index.js chat -p deepseek export ALIYUN_API_KEY=sk-xxxxxxxx node dist/cli/index.js chat -p aliyun export ZHIPU_API_KEY=xxxxxxxx.xxxxxxxx node dist/cli/index.js chat -p zhipu export BAIDU_API_KEY=xxxxxxxx node dist/cli/index.js chat -p baidu
Every provider reads <PROVIDER>_BASE_URL:
# Azure OpenAI export OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deploy export OPENAI_API_KEY=your-azure-key node dist/cli/index.js chat -p openai -m your-deploy-name # OpenAI-compatible proxy export OPENAI_BASE_URL=https://api.your-proxy.com/v1 export OPENAI_API_KEY=sk-proxy
All ten: OPENAI_BASE_URL / ANTHROPIC_BASE_URL / GOOGLE_BASE_URL / DEEPSEEK_BASE_URL / SILICONFLOW_BASE_URL / ALIYUN_BASE_URL / ZHIPU_BASE_URL / BAIDU_BASE_URL / OLLAMA_BASE_URL / LMSTUDIO_BASE_URL.
# Ollama ollama serve & ollama pull llama3.2 node dist/cli/index.js chat -p ollama -m llama3.2 # LM Studio — start the local server on :1234 first node dist/cli/index.js chat -p lmstudio
# Interactive multi-turn chat (readline) node dist/cli/index.js chat [-p provider] [-m model] # One-shot 6-stage pipeline node dist/cli/index.js agent "<task>" [-p provider] [-m model] # Config node dist/cli/index.js config show node dist/cli/index.js config set-key <provider> [key] # reads from stdin if omitted node dist/cli/index.js config remove-key <provider> node dist/cli/index.js config set-default <provider> node dist/cli/index.js config providers
Each agent task runs these stages in order:
- initialize — set up context (taskId, metadata, error list)
- understand — LLM classifies intent (
chat/code/analyze/refactor/shell/search) and scans the workspace - plan — LLM drafts the step list + tool picks, with a template fallback if the LLM call fails
- execute — invoke the tools step by step
- validate — check syntax, exit codes, file existence
- reflect — self-reflection, emotion update, write to long-term memory
| Tool | Purpose | Backend |
|---|---|---|
writeFile |
Write to file | StorageAdapter (real filesystem) |
readFile |
Read from file | StorageAdapter (real filesystem) |
shell |
Run a shell command | child_process (real process) |
search |
Search long-term memory | MemoryAdapter.recall (MiniSearch) |
remember |
Write to long-term memory | MemoryAdapter.remember |
analyze |
LLM analysis of a piece of text | LLMAdapter.chat (real LLM) |
respond |
LLM conversational reply | LLMAdapter.chat (real LLM) |
A couple of sanity checks you can run yourself to confirm nothing is being stubbed:
# 1. No key — should not silently fall back, should error $ node dist/cli/index.js chat ✗ No API key configured for provider "siliconflow". Run: personal-assistant config set-key siliconflow <YOUR_KEY> Or set the env var: PERSONAL_ASSISTANT_SILICONFLOW / SILICONFLOW_API_KEY # 2. Fake key — real HTTP call gets a real vendor 401 $ SILICONFLOW_API_KEY=sk-fake-key node dist/cli/index.js agent "hi" 📍 execute Running step: Generate conversational response ✗ LLM request failed (siliconflow): 401 "Api key is invalid"
Every LLM call is a real fetch to the vendor's endpoint.
Personal-Assistant/
├── src/ # Core (Clean Architecture)
│ ├── adapters/ # LLM / Config / Memory / Storage / Shell / Tools / Git / Context
│ ├── core/ # Pipeline + EventBus
│ ├── stages/ # 6 stages: init / understand / plan / execute / validate / reflect
│ ├── ports/ # Interface contracts
│ ├── domain/ # personality / memory / trust / emotion
│ ├── infrastructure/ # Circuit Breaker (opossum), logger, secret storage
│ ├── machines/ # XState agent FSM
│ ├── cli/ # CLI entry (chat / agent / config)
│ └── __tests__/ # Unit + integration + e2e tests
├── server/ # Express backend
│ ├── index.ts # Entry
│ ├── app.ts # Routes + static hosting of frontend/dist
│ ├── providers.ts # 10-provider metadata
│ ├── session-store.ts # Chat session persistence
│ ├── tools-setup.ts # 7-tool registration
│ └── routes/ # REST + SSE routes
├── frontend/ # React 18 + TypeScript + Vite + Tailwind
│ ├── src/
│ │ ├── api/ # fetch client + sseStream()
│ │ ├── stores/ # zustand: theme / config / locale
│ │ ├── i18n/ # EN / 中文 translations
│ │ ├── pages/ # 13 pages
│ │ ├── components/ # Layout + UI components
│ │ ├── types/ # TypeScript types
│ │ └── styles/ # Tailwind + CSS variables
│ └── dist/ # Build output (served by /server)
├── .github/
│ ├── workflows/ci-cd.yml # CI/CD (Node 20/22, frontend build)
│ ├── ISSUE_TEMPLATE/ # bug / feature / docs / new_provider
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── CODEOWNERS # Code review owners
├── sdk/ # Standalone npm package (@personal-assistant/sdk)
├── docs/ # ARCHITECTURE / SETUP / TROUBLESHOOTING / DEPLOYMENT / API / SDK
├── README.md # English
├── README_zh.md # 中文
├── CHANGELOG.md # Keep-a-Changelog
├── CONTRIBUTING.md
├── SECURITY.md
├── CODE_OF_CONDUCT.md # Contributor Covenant 2.1
├── LICENSE # MIT
├── .editorconfig
├── .prettierrc
├── .npmignore
├── codecov.yml
├── typedoc.json
└── package.json
The web UI ships in English and Simplified Chinese. The globe icon in the header switches between them; the choice is persisted to localStorage and survives page refreshes. Both locales cover the same 348 keys across all 13 pages.
Translation files live in frontend/src/i18n/ and follow the standard i18next JSON format. To add a new language:
- Copy
frontend/src/i18n/en.jsontofrontend/src/i18n/<code>.jsonand translate - Register it in
frontend/src/i18n/index.ts—SUPPORTED_LANGUAGESandresources - Add a tab to the language switcher in
Layout.tsx
See docs/DEPLOYMENT.md for:
- Recommended topology (nginx / Caddy reverse proxy + TLS)
- nginx + Caddy config snippets
- Dockerfile + docker-compose
- systemd unit with security hardening
- Production security checklist
- Backup & restore
npm install # Backend deps npm run build # Compile backend (tsc) npm run server # Express with tsx watch npm run web:dev # Vite frontend (HMR, /api proxy → :3000) npm run web:build # Frontend production build npm test # vitest run npm run test:coverage # vitest with v8 coverage npm run lint # ESLint 9.x flat config npm run format # Prettier npm run verify # lint + test + tsc + frontend build (CI in a box)
| Symptom | What to check |
|---|---|
No API key configured for provider "..." |
1) No key set; 2) key name typo (note PERSONAL_ASSISTANT_<PROVIDER> uppercase) |
LLM request failed (openai): 401 |
Invalid or expired key — regenerate in the vendor console |
LLM request failed (openai): 429 |
Rate limit — wait a few seconds or switch model |
fetch failed |
Network blocked or proxy unreachable — curl https://api.openai.com/v1/models to test |
Anthropic request failed: 403 |
1) Wrong key; 2) IP in a region Anthropic denies (common in CN) |
ollama / lmstudio unreachable |
Local service not started — ollama serve or launch LM Studio's local server |
| Want to change base URL | Set <PROVIDER>_BASE_URL |
- OpenAI-compatible (7 providers):
POST {base_url}/chat/completions,Authorization: Bearer <key> - Anthropic:
POST {base_url}/v1/messages,x-api-key: <key>+anthropic-version: 2023年06月01日 - Google:
POST {base_url}/models/{model}:generateContent?key=<key> - Baidu:
POST {base_url}/wenxinworkshop/chat/{model}?access_token=<key>
All calls go straight to the vendor via fetch.
See CONTRIBUTING.md for the workflow. Use the issue and PR templates.
By participating, you agree to the Code of Conduct. For security issues, follow SECURITY.md and use private disclosure instead of a public issue.
MIT — see LICENSE.