X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/37d5f21b23da5fbd6b44fed41f653927669571ed..2501e78d10204fac2298db0dee84b3dce379e09b:/README.rst diff --git a/README.rst b/README.rst index 7712c0e..9fbf6dd 100644 --- a/README.rst +++ b/README.rst @@ -129,6 +129,80 @@ prompt cache key comes from it. how their files are organized, and how they want work done. The prompt ends with them, and a change applies at the next call. +************** + User service +************** + +The Nix package includes ``share/systemd/user/onechat.service``. It runs +``onechat daemon`` from that package, starts at login when enabled, and +restarts 5 seconds after a failure. A stop sends SIGTERM to the daemon +and allows up to 120 seconds for cleanup. Its output goes to the user +journal. The service has the rights of the user, as the terminal daemon +does. + +Build a package with a separate result link. Keep this link so that Nix +does not collect the package while the service uses it: + +.. code:: console + + nix build --out-link result-service + systemctl --user link --force "$(realpath result-service/share/systemd/user/onechat.service)" + systemctl --user edit onechat.service + +In the editor, set the data directory with this drop-in. Replace +``DATA_DIR`` with an absolute path to the data directory. Set the same +``ONECHAT_DIR`` for each terminal client. Without a drop-in, the data +directory rules above apply to the environment of the user manager, not +to the development shell. + +.. code:: ini + + [Service] + Environment="ONECHAT_DIR=DATA_DIR" + +The service does not read the environment of an open terminal. If the +agent needs tools from a shell, set ``Environment="PATH=TOOLS_PATH"`` in +the same drop-in, with ``TOOLS_PATH`` replaced by the complete search +path, including bash. For the API endpoint, use +``EnvironmentFile=ENV_FILE``, with ``ENV_FILE`` replaced by an absolute +path to a mode 0600 file that sets ``OPENAI_API_KEY``. Keep secrets out +of the unit and Nix sources. + +Use the existing data directory, or copy the development data before the +first service start. For a copy, first stop the terminal daemon with +Ctrl-C and wait for it to exit. Replace ``DATA_DIR`` below with a new, +empty destination. If the development shell uses a custom +``ONECHAT_DIR``, replace ``.devenv/onechat`` with that source. Copy the +whole directory so that ``auth.json``, ``telegram.json``, +``config.json``, ``agent/``, and ``whatsapp/`` stay together, including +the WhatsApp session and its database files. The copy preserves the file +modes and the source as a backup. Do not copy or use the same data from +two running daemons. + +.. code:: console + + install -d -m 0700 DATA_DIR + cp -a .devenv/onechat/. DATA_DIR/ + +After the terminal daemon exits and the data is ready, enable and start +the service: + +.. code:: console + + systemctl --user daemon-reload + systemctl --user enable --now onechat.service + systemctl --user status onechat.service + ONECHAT_DIR=DATA_DIR ./result-service/bin/onechat chat + journalctl --user -u onechat.service -f + +The service starts with the user manager at login, not at boot without a +login. To stop it, or also remove its start at login: + +.. code:: console + + systemctl --user stop onechat.service + systemctl --user disable onechat.service + ********** Telegram ********** @@ -172,6 +246,150 @@ write of a message can make the message repeat once. A network failure or a rate limit makes the bot wait and try again, and does not stop the daemon. +********** + WhatsApp +********** + +The daemon connects to the WhatsApp account of the user as a linked +device. The terminal client and Telegram take three commands: + +``/whatsapp pair PHONE`` + Links the account of PHONE, the number with its country code, such as + ``+15550001111``. The daemon answers with the steps and then with a + pairing code in its own message: in WhatsApp on the phone, open + Linked devices, tap Link a device, then tap Link with phone number + instead, and enter the code. The code expires in about 2 minutes, and + the same command gives a new code. When the link succeeds, the daemon + answers with the linked number. Linked devices on the phone shows the + device as OneChat. The name applies at the pairing, so an existing + link keeps its old name until ``/whatsapp unlink`` and a new + ``/whatsapp pair PHONE``. + +``/whatsapp status`` + Shows the link: not linked, linked and connected, or linked and + disconnected, with the times of the connection. + +``/whatsapp unlink`` + Ends the link and deletes the session. When WhatsApp is disconnected, + the phone does not get the logout, so remove the device in Linked + devices on the phone. + +The answers go only to the client that asked, and the commands do not go +to the agent, so the agent never gets the phone number or the code. The +chat gets only the notes that WhatsApp is linked or unlinked. + +The session is ``whatsapp/session.db`` in the data directory, with mode +0600 in a directory with mode 0700. It gives full access to the WhatsApp +account, so the daemon refuses the file when its mode is not 0600. The +shell of the agent runs as the user, so it can read the session. If the +session leaks, remove the linked device in WhatsApp on the phone, and +the session stops working. Without the file, WhatsApp is not linked. + +The daemon reads the text messages of the private chats and the groups, +from other people and from the user. It does not read media, reactions, +edits, deletions, view-once and disappearing messages, or the history +from before the link. The messages of one chat form one batch until the +chat is quiet for 20 seconds, for 2 minutes or 50 messages at the most. +Each batch goes to the chat of the agent as one work message that starts +with ``[WhatsApp]``, with the name and the ID of the chat, and the time, +the sender, and the text of each message. The messages of the user show +as ``You``. ``whatsapp/state.json``, with mode 0600, keeps the batches +that are not in the chat yet, so they survive a restart, and the records +of the sends. A crash just after a batch goes to the chat can make the +batch repeat once. + +A batch of a private chat with a message of another person starts a +turn, and so does a batch of a group with a message that mentions the +user or replies to a message of the user. The other batches go to the +chat with no turn, and the agent sees them at its next turn. A batch +that starts a turn ends with a line that tells the agent to end its turn +with an empty reply when nothing in the batch needs the user. No client +shows an empty reply, so such a batch gives no message in Telegram. The +terminal client shows each batch, and Telegram shows none. + +The phone number of the user never goes to the chat: a mention of the +user shows as ``@you``, the number shows as ``OWN``, and the chat of the +user with itself is not read. But the IDs of the other chats go to the +chat and to the model, because the agent needs them to send. The ID of a +private chat holds the phone number of the contact, such as +``15550002222@s.whatsapp.net``, and the ID of a group can hold the +number of the person who made it. + +While the daemon is stopped or disconnected, the WhatsApp servers keep +the messages for the linked device and send them at the next connection. +They keep them only for a limited time, so a long stop can lose +messages. These messages form batches by their original times, as live +messages do. The daemon waits for the end of this catch-up, or one +minute at the most, and then gives all its batches to the agent in one +turn, not one turn for each chat. The batches of ``whatsapp/state.json`` +at the start wait for the first catch-up in the same way. + +The agent finds a chat with the tool ``whatsapp_find``: it gives the +contacts and the groups whose name holds a text, with no regard to case +and accents, with their IDs. The names of the contacts come from the +session, and the names of the groups from WhatsApp, kept in memory for +one hour. The names arrive from the phone some minutes after the link, +and until then the tool says that they are not ready. The agent sends a +text message with the tool ``whatsapp_send``, only when the user asks +for it. Each send has an ID that the agent gives: a repeated call with +the same ID gives the answer of the first call and sends nothing. The +state file keeps the last 1000 send records. When the daemon stops +during a send, the result is not known, and the tool tells the agent to +ask the user to look at the chat in WhatsApp. Without a link, both tools +tell the agent to ask the user to run ``/whatsapp pair PHONE``. Only the +agent has the WhatsApp tools, not its subagents. + +Each batch of a group goes to the chat, also with no turn, and the +memory tree summarizes it with model calls, which use the quota. + +When the session holds a linked device, the daemon connects at its +start, and after a network failure it connects again by itself. When a +disconnection lasts 10 minutes, all clients get one notice, and one more +when the connection comes back. When WhatsApp ends the connection for +good, for example because another program uses the same session, all +clients get a notice at once. When the user removes the device in the +phone, the daemon deletes the session and tells all clients one time. + +*********** + Schedules +*********** + +The agent sets reminders and timed tasks when the user asks for them, +such as "remind me tomorrow at 9" or "every weekday at 8, tell me what +my WhatsApp groups talked about". A schedule has a text, the task for +the agent, and either one time or a cron expression. The agent also +lists the schedules with their next times, and removes one when the user +asks. Only the agent has the schedule tools, not its subagents. + +A cron expression has five fields: minute, hour, day of month, month, +and day of week, where 0 and 7 are Sunday. A field is a comma list of +``*``, ``N``, and ``N-M``, and ``*`` and ``N-M`` can have a step ``/S``, +as in ``0 8 * * 1-5`` or ``*/15 9-17 * * *``. When both day fields start +with something other than ``*``, a day matches when either field +matches. Names such as ``MON``, macros such as ``@daily``, the +characters ``? L W #``, and ``N/S`` are refused. The runs must be at +least 5 minutes apart, because each run is a full turn that uses the +quota, so ``*/5 * * * *`` is the shortest schedule. + +The times use the time zone of the machine. A time that the clock skips +at the start of daylight saving time runs later by the length of the +skip. A time that the clock repeats at its end runs once, at its first +occurrence. + +At its time, a schedule goes to the chat as a work message that starts +with ``[Schedule ID]``, with its time and its text, and it starts a +turn. The message ends with a line that tells the agent to end its turn +with an empty reply when nothing needs the user. The terminal client +shows the message, and Telegram shows only the reply. After its run, a +one-time schedule goes, and a repeating schedule gets its next time. + +``agent/schedules.json``, with mode 0600, holds the schedules. Each +change writes it before the tool answers, so the schedules survive a +restart. A schedule that came due while the daemon was stopped runs once +at the next start, with its original time and a line that says that the +run is late. A repeating schedule that missed several times runs once. A +crash just after a run goes to the chat can make the run repeat once. + ******* Probe *******