Documentation

From install to daily use, plus what your IT team needs for a production deployment.

Installation

Three ways to install. Pick one.

Download the installer and click through. It creates a desktop and start menu shortcut. First launch takes 20 to 40 seconds while it initialises the database and loads model configuration.

Windows 10 and above. For macOS and Linux, install from source for now.

Docker

Best when you want it on an internal server for the whole team.

docker run -d --name yuan \
  -p 8080:8080 \
  -v $(pwd)/data:/app/data \
  -v $(pwd)/config:/app/config \
  --restart unless-stopped \
  yuan-assistant:latest

Then open http://your-server:8080.

data/ holds the database, uploads and logs. config/ holds configuration. Mount both, otherwise you lose everything when the container is rebuilt.

From source

Requires Python 3.10 or later.

git clone <your source> yuan && cd yuan
pip install -r requirements.txt
python main.py web

To rebuild the frontend:

cd frontend && npm install && npm run build

Requirements

MinimumRecommended
CPU2 cores4+ cores
Memory4 GB8 GB+
Disk5 GB20 GB+
GPUNot required8 GB VRAM (local models only)

You do not need a GPU if you only use cloud models. A GPU is only for running models locally. It will still work on CPU alone, just considerably slower.

First run

On first start the system generates an admin account. The password is shown once and never again.

The desktop app pops it up in a window. On a server it goes to the startup output:

============================================
  Admin account created
  Username: admin
  Password: xK9mP2vL8qR4nW7t
  Sign in and change this now
============================================
This password is never written to a file and cannot be recovered. If you miss it, the practical fix is deleting the database in data/ and starting over, which wipes everything. Write it down.

First thing after signing in: go to Settings → Account and set your own password.

Configure a model

Yuan cannot answer anything until a model is configured. Two options.

Cloud models — go to Settings → Models and paste an API key. OpenAI, Anthropic and Google are supported. Keys are encrypted into the database, never written to the config file.

Local models — go to Settings → Inference and click install Ollama. See Local inference.

You can set up both and switch between them above the chat box.

Chat

Conversations on the left, the thread in the middle, status panel on the right.

Modes

Three modes in the top left. They change the answering style and which tools are available.

ModeFor
ChatQuestions, writing, translation
WorkDocuments, analysis, scheduling
CodeReading and writing code, Git, terminal

Thinking depth

Next to the chat box you can set how hard it thinks:

  • ⚡ Quick — answers directly, fine for simple questions
  • ⚖️ Balanced — the default
  • 🧐 Deep — reasons before answering. Slower and more expensive, worth it for hard problems
  • 🎨 Creative — brainstorming and ideation

Only models that support reasoning (o-series, Claude, Gemini and similar) actually honour this. Others ignore it.

Attachments

Drag files onto the chat box or use the paperclip. PDF, Word, Excel, PowerPoint, plain text, source files and images.

25 MB per file. Images go through a vision model; documents have their text extracted into context.

Screen context

Toggle the eye icon next to the chat box and each message will also tell Yuan which application you currently have in front and what the window is called.

Means you can ask "what does this error mean" without describing where you are. Toggle it off and nothing is collected.

Voice

Voice input

Press Alt+V or click the microphone. Speak, and the transcript lands in the input box so you can edit it before sending.

Two recognition engines, switchable next to the microphone:

  • Local — browser speech recognition or a local faster-whisper model. Audio never leaves the machine.
  • Cloud — sent to the Whisper API. More accurate.

The first local run downloads a speech model, roughly 150 MB, with a progress bar.

Live voice conversation

Press Alt+V twice for live mode. It is full duplex: it answers as you finish and you can talk over it.

Good for meetings, driving, or when your hands are busy. This mode uses a cloud realtime service, so audio does leave the machine. Use local recognition above if you need everything to stay in.

Tools and automation

Yuan does not just answer, it picks up tools and finishes the job. Ask it to merge the CSVs in a folder, chart them and email Kevin, and it breaks that into reading files, computing, generating a chart and sending mail on its own.

While it works, small grey labels appear in the thread showing what it is doing.

What it can use

CategoryCapability
FilesRead, write, search, copy, move, delete
CodeRun code, run shell commands
WebSearch, fetch pages, drive a browser
BrowserNavigate, click, fill forms, scroll, extract, screenshot
Version controlstatus, diff, commit, log, branch
DesktopScreenshot and analyse the screen
MemoryRemember, recall, search past conversations
SchedulingCreate recurring jobs
MessagingSend email and platform messages

43 in total. How many are available depends on your licence tier.

Permissions

Destructive actions (deleting files, running shell commands, writing files) ask for confirmation by default. You can switch to auto-approve under Settings → Permissions, though it is not recommended.

File operations are confined to the sandbox and any paths you explicitly authorise. Yuan cannot reach outside that.

Browser automation

It can drive a real browser:

You: Go to example.com, sign in as [email protected], then pull the data off the orders page

It navigates, fills the form, clicks and extracts in sequence. The browser session keeps its login for the rest of that conversation.

Local inference

Local inference keeps everything on your own hardware. This is the option for regulated data.

Setup

Settings → Inference, then install Ollama. It reads your VRAM and suggests a model size:

VRAMSuggested
None (CPU only)qwen2.5:3b, phi3:mini
4–7 GBqwen2.5:3b
8–15 GBqwen2.5:7b, llama3.1:8b
16–23 GBqwen2.5:14b
24 GB+qwen2.5:32b

Download progress is shown inline. Once finished the local model appears in the picker above the chat box.

37 model families are supported, including Qwen, Llama, Gemma, DeepSeek, Mistral and Phi.

Privacy mode

Settings → Privacy has a privacy mode. With it on, the system refuses to use anything but local models even if a cloud model is selected by mistake.

vLLM

With multiple GPUs, vLLM gives better throughput. Select it under Settings → Inference and name the model. vLLM requires a GPU and will not run CPU-only.

Honestly: local models do not match the best cloud models on reasoning. If your work needs the strongest model available you will need a cloud API, and that means giving up the data-stays-put argument. It is a trade-off.

Messaging platforms

Yuan can sit on messaging apps so customers and colleagues talk to it directly. Every channel shares one memory and knowledge base.

LINE Official Account, Telegram, Discord and Feishu are supported.

They do not all run the same way, which affects how many processes you start and whether you need a public URL:

PlatformHow it runsPublic URL needed
FeishuInside the main web processYes
LINESeparate process, own portYes
TelegramSeparate process, polls for updatesNo
DiscordSeparate process, long-lived connectionNo

Feishu

Create a custom app in the Feishu open platform:

feishu:
  app_id: "cli_xxxx"
  app_secret: "your secret"
  verification_token: "event subscription token"

Set the event subscription URL to https://your-domain/api/feishu/webhook and subscribe to im.message.receive_v1. This is the only channel served by the main web process, so it needs no extra process.

LINE Official Account

  1. Create a Messaging API channel in LINE Developers
  2. Copy the Channel Access Token and Channel Secret
  3. Add them to config.yaml:
line:
  channel_access_token: "your token"
  channel_secret: "your secret"
  webhook_port: 8443
  1. Start a second process:
python main.py line
  1. Point the webhook URL at https://your-domain:8443/webhook
LINE runs as its own process on its own port (8443 by default) and its path is /webhook — it is not under the main app's /api. Point your reverse proxy at that port.

Telegram

Get a token from BotFather:

telegram:
  token: "your token"
python main.py telegram

Telegram polls for updates, so it needs no public URL and works from inside a private network.

Discord

Create an application and bot in the Discord Developer Portal, then enable the Message Content Intent:

discord:
  token: "your bot token"
python main.py discord

Discord holds a long-lived connection, so it also needs no public URL.


Restart the relevant process after changing configuration. To run several channels at once, start one process each; they share the same database.

Projects and files

A project ties together related conversations, files and rules.

Creating one

Projects icon in the sidebar, then new project. Give it a name and description.

Linking a folder

Inside a project you can link a local folder. Yuan scans the structure, skipping .git, node_modules, __pycache__ and anything in .gitignore, and from then on knows what files exist while you work in that project.

The desktop app opens the native folder picker.

Working agreements

Each project can carry its own rules:

Always reply in British English.
Comment code in English.
Always state the currency when quoting amounts.

These apply only inside that project.

Project memory

Knowledge accumulated in a project stays with it. Switching projects keeps them cleanly separated.

Settings

Models

Manage API keys and default models. Keys are stored encrypted; the interface only shows the last four characters, revealed with the eye icon when needed.

You can nominate a primary and a fallback model. If the primary fails, it switches automatically.

Appearance and language

Traditional Chinese, Simplified Chinese, English and Japanese. Dark and light themes.

Keyboard shortcuts

Global shortcuts in the desktop app, available from any application:

ShortcutAction
Alt+MShow / hide the main window
Alt+VVoice input
Alt+SScreenshot and analyse
Alt+DOpen the dashboard
Alt+PRecall the floating orb to the centre

Personal API tokens

To call Yuan from your own code, create a token under Settings → Tokens. The full token is shown once; afterwards only the prefix.

Backups

Settings → Backup creates backups on demand or on a schedule, covering the database and uploaded files. You can also export everything as JSON.

Licensing

What each tier includes

CommunityProEnterprise
Seats15Unlimited
Messages per day1001,000Unlimited
Chat and memory
Local inference
Voice assistant
Work automation
Messaging platforms
Projects
SSO
PostgreSQL / Redis / K8s
Multi-organisation
Audit and compliance export

Activating

Settings → Licence shows this machine's device ID (HWID). Send it to us when you purchase and we will issue a licence key.

Paste the key into the same page and activate.

A key binds to one machine. Contact us to move it.

How it stays valid

The licence checks in with the licence server periodically. Offline you get a one day grace period before it stops working. If we adjust your features or expiry, the change arrives on the next check-in. No re-activation needed.

One day is a short window, so watch out if your deployment spends long stretches disconnected. If it has no outbound internet at all, talk to us about an offline licence.

Enterprise deployment

This section is for whoever runs your infrastructure.

PostgreSQL

SQLite is the default and is fine for a single machine. For several machines sharing state, switch:

database:
  url: "postgresql://user:password@db-host:5432/yuan"
  pool_size: 20

Restart and the schema is created automatically.

Redis

Needed when multiple instances must share cache and rate limits:

cache:
  backend: "redis"
  redis_url: "redis://redis-host:6379/0"

Without it each instance keeps its own in-memory cache, which is fine on one box and wrong on several.

Background queue

Long jobs can go to Celery workers so they do not tie up the web process:

task_queue:
  backend: "celery"
  broker_url: "redis://redis-host:6379/1"

Start a worker with:

celery -A worker worker --loglevel=info

Single sign-on

OIDC (Google, Microsoft Entra ID, WeChat, QQ) and SAML 2.0 (Okta, OneLogin and others).

sso:
  oidc:
    google:
      client_id: "..."
      client_secret: "..."
    azure:
      client_id: "..."
      client_secret: "..."
      tenant: "your tenant id"
  saml:
    idp_entity_id: "..."
    idp_sso_url: "..."
    idp_x509_cert: "..."

Redirect URI is https://your-domain/api/sso/{provider}/callback. SP metadata for SAML is at https://your-domain/api/sso/saml/metadata.

Access control

security:
  ip_whitelist: ["10.0.0.0/8", "192.168.1.0/24"]
  ip_blacklist: []
  max_sessions: 5
  login_lockout_attempts: 5
  login_lockout_minutes: 15

An empty ip_whitelist means no restriction. CIDR notation is supported.

These need an Enterprise licence. The protection itself is on for every tier — five failed logins lock an account for fifteen minutes, and each user is capped at five concurrent sessions. What Enterprise buys is the ability to change those numbers, not whether the protection exists. Without session_limit / login_lockout the configured values are ignored with a warning and the built-in defaults stay in force.

>

ip_whitelist and ip_blacklist behave differently: without the entitlement the service refuses to start rather than ignoring them. Otherwise you would believe you had IP restrictions when you did not.

Permissions come in four tiers: guest, member, power user and admin. Each tier gets a different tool set, adjustable under Settings → Permissions.

Audit and compliance

Every action is logged with actor, action, target, IP and timestamp. Admins can review this in the console.

Compliance reports export as:

GET /api/admin/compliance/report?start=<unix>&end=<unix>&format=csv

Admin credentials required. Covers audit logs, sign-in history and usage.

GDPR deletion: the console can erase everything belonging to one user, including conversations, memories, documents and uploads.

Kubernetes

deploy/k8s/ has manifests ready to apply: Deployment with 3 replicas, Service, Ingress and an HPA scaling 2 to 10 replicas at 70% CPU.

kubectl apply -f deploy/k8s/

Database credentials and keys live in a Secret; configuration is mounted from a ConfigMap.

Encryption

Sensitive configuration values (API keys, tokens, passwords) are encrypted at rest and stored with an ENC:: prefix. The key is machine-bound, so moving hosts means re-entering them.

The database can be encrypted too. Install pysqlcipher3 and it is detected and enabled automatically, migrating an existing plaintext database in place.

API reference

Yuan's API runs on your machine, not ours. The base URL is wherever you installed it: http://localhost:8080, or https://yuan.yourcompany.com.

Use it to wire Yuan into systems you already have: an internal ticketing tool, a CRM, a cron script, your own front end.

Every endpoint is mounted under both /api and /api/v1. They are identical; the examples below use /api.

Authentication

Create a personal access token under Settings → Tokens. The full value is returned once:

{ "token": "sk-asst-xxxxxxxxxxxxxxxxxxxx", "name": "My integration" }

Send it on every request:

Authorization: Bearer sk-asst-xxxxxxxxxxxxxxxxxxxx
Tokens are shown once and stored only as a hash. Lost one, delete it and make another.

Permissions follow the account, not the token — downgrade the account and every token it owns is downgraded with it. Deleting a token under Settings → Tokens revokes it immediately.

There is a second route: on the desktop build, requests from the local machine (127.0.0.1) are authenticated as the local user with no header at all. Scripts running on the same box can call the API directly without creating a token first.

Yuan has no username/password login endpoint. Those two routes are the whole story, plus SSO on the Enterprise tier.

A first request

The shortest complete example — send a message, get a reply:

curl -X POST http://localhost:8080/api/chat \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "Summarise last week sales", "stream": false}'

Returns:

{ "conversation_id": 42, "content": "Total sales last week were ..." }

Chat

POST /api/chat is the main endpoint.

FieldTypeMeaning
messagestringRequired, the text to send
streamboolDefaults to true. Set false for a single JSON reply, which is easier in a script
conversation_idintContinue an existing thread. Omit to start a new one
model_namestringPick a model, otherwise the default is used
modestringchat, work or code
project_idstringAttach to a project so its rules apply

With stream: true the response is text/event-stream; each event is a data: line containing one JSON object:

EventMeaning
{"content": "..."}A fragment of the reply, concatenate these
{"done": true, "conversation_id": 42}Finished
{"error": "..."}Failed

Managing conversations

MethodPathMeaning
GET/api/conversationsList. Accepts search, limit (max 200, default 50) and offset
POST/api/conversationsCreate. Optional title, model_name, template_id
GET/api/conversations/{id}/messagesFetch messages (most recent 100)
PUT/api/conversations/{id}/titleRename
DELETE/api/conversations/{id}Delete
GET/api/conversations/{id}/exportExport as a file
curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/api/conversations?search=sales&limit=20"

Documents

MethodPathMeaning
POST/api/documents/uploadUpload one file (multipart, field name file)
POST/api/documents/upload-batchUpload several
GET/api/documentsList
GET/api/documents/searchFull-text search, query in q
DELETE/api/documents/{id}Delete
curl -H "Authorization: Bearer $TOKEN" \
  -F "[email protected]" \
  http://localhost:8080/api/documents/upload

Uploads are chunked and indexed automatically so they can be cited in conversation.

Voice and images

MethodPathMeaning
POST/api/voice/sttSpeech to text. Multipart, field name file; optional engine (local/cloud) and language
POST/api/voice/ttsText to speech, returns audio/mpeg
GET/api/voice/statusEngine status
POST/api/image/analyzeDescribe contents
POST/api/image/ocrExtract text
POST/api/image/describeAccessibility description
POST/api/image/compareCompare several images

System

MethodPathMeaning
GET/api/healthHealth check, no auth required
GET/api/license/infoCurrent licence status
GET/api/userCurrent user
GET/api/tokensList your tokens
POST/api/tokensCreate a token, name in the body
DELETE/api/tokens/{id}Delete a token

/api/health needs no authentication, which makes it suitable for load balancer and monitoring probes.

WebSocket

The interface itself uses WebSocket so it can push tool-execution state live. You only need this if you are writing your own interactive front end — for ordinary integrations /api/chat is enough.

Browser clients must first exchange their personal token or JWT for a short-lived, single-use WebSocket ticket. Keep the credential in the HTTP Authorization header; never put it in a URL.

POST http://localhost:8080/api/ws-ticket
Authorization: Bearer <your token>

Use the returned ticket for one connection attempt. Mint a fresh ticket before every reconnect:

ws://localhost:8080/api/ws?ticket=<single-use ticket>

Non-browser clients that can set an Authorization: Bearer header on the WebSocket upgrade may use that instead of a ticket.

Once connected you must send a message first; the server consumes the first one as an init frame. This frame configures the session; it does not authenticate the connection:

{ "type": "init" }

Then send the real payload:

{ "type": "chat", "message": "...", "conversation_id": 42, "mode": "work" }

Inbound types include typing, tool_call, tool_result, chunk, done, error and conversation_created.


Errors

Errors always come back in this shape:

{
  "error": "AUTH_1002",
  "message": "Token expired"
}

Codes carry a prefix:

PrefixRangeCategory
AUTH_1xxxAuthentication
RES_2xxxResource
VAL_3xxxValidation
RATE_4xxxRate limit
AI_5xxxAI service
CHAT_6xxxChat
SRV_9xxxServer

Branch on error. The message text changes with the interface language.

Rate limits

Exceeding a limit returns 429. Daily message allowances depend on your tier, see Licensing.

Troubleshooting

It will not start

Check the logs in data/logs/. The desktop app can open that folder from the tray menu.

Usual causes:

Port already in use — 8080 by default. Change web.port in config.yaml or stop whatever else is on it.

Database locked — another instance is running. The desktop app detects this and focuses the existing window; on a server, confirm the old process has exited.

No model configured — the UI says so. Add an API key or install a local model.

Responses are slow

  • On local models, check GPU utilisation under Settings → Inference. If the model does not fit in VRAM it falls back to CPU and gets dramatically slower. Use a smaller model.
  • Deep thinking mode is noticeably slower. Balanced is the right default.
  • Very long conversations grow the context and slow things down. Starting a new conversation helps a lot.

Voice does nothing

  • Check that the browser or OS granted microphone permission
  • Local recognition downloads a model on first use, wait for the progress bar
  • If Alt+V does nothing in the desktop app, another application may have claimed that shortcut

Messaging platform receives nothing

  • The webhook URL must be HTTPS and reachable from the internet
  • Look in data/logs/ to see whether requests are arriving at all
  • Telegram can be tested in polling mode first, which needs no public URL

Licence stopped working

  • Confirm the machine can reach the licence server
  • Replacing hardware (motherboard, NIC, disk) changes the device ID and needs a re-bind
  • More than one day offline suspends it; it recovers once back online

Still stuck

Send us the recent files from data/logs/ along with what you did, what you expected, and what happened instead.