Paperclip with Python: run a company of AI agents from a scriptPaperclip กับ Python: บริหารบริษัทของเอเจนต์ AI ด้วยสคริปต์用 Python 操作 Paperclip:用脚本经营一家由 AI 智能体组成的公司

This page teaches you to control Paperclip with Python. Paperclip is a free, open-source program that manages a team of AI agents the way a company manages its staff. You will open a company, hire two agents, give them tasks and set a budget, all with short Python files and the requests library. You will also write the employee yourself: a Python file of 100 lines that Paperclip wakes whenever it has work. You need Node.js to run Paperclip, Python, and a terminal. For the first four tutorials you do not need an AI model, a key or any money. The fifth hires four agents that review a research paper for a software company, look up facts and write it again. They use a small model that runs on your own computer, and that is free too.หน้านี้สอนวิธีควบคุม Paperclip ด้วย Python Paperclip เป็นโปรแกรมโอเพนซอร์สที่ใช้ได้ฟรี มันบริหารทีมเอเจนต์ AI แบบเดียวกับที่บริษัทบริหารพนักงาน คุณจะเปิดบริษัท จ้างเอเจนต์สองตัว มอบงานให้ และตั้งงบประมาณ ทั้งหมดนี้ทำด้วยไฟล์ Python สั้น ๆ กับไลบรารี requests คุณจะเขียนตัวพนักงานเองด้วย เป็นไฟล์ Python 100 บรรทัดที่ Paperclip ปลุกขึ้นมาทุกครั้งที่มีงาน สิ่งที่ต้องมีคือ Node.js สำหรับรัน Paperclip กับ Python และเทอร์มินัล สี่บทแรกไม่ต้องมีโมเดล AI ไม่ต้องมีคีย์ และไม่ต้องเสียเงิน บทที่ห้าจ้างเอเจนต์สี่ตัวมารีวิวรายงานวิจัยของบริษัทซอฟต์แวร์ ค้นหาข้อเท็จจริง และเขียนรายงานใหม่ พวกมันใช้โมเดลขนาดเล็กที่รันบนคอมพิวเตอร์ของคุณเอง ซึ่งก็ฟรีเช่นกัน这一页教你用 Python 来控制 Paperclip。Paperclip 是一个免费的开源程序,它管理一组 AI 智能体的方式,就像公司管理员工一样。你会开一家公司,雇两个智能体,给它们派任务,再定一份预算,全都只用几个很短的 Python 文件和 requests 库。员工也由你自己来写:一个 100 行的 Python 文件,只要有活,Paperclip 就把它叫醒。你需要 Node.js(用来运行 Paperclip)、Python 和一个终端。前四个教程不需要 AI 模型,不需要密钥,也不用花钱。第五个教程雇四个智能体,为一家软件公司审一篇研究报告、查找事实、再把它重写一遍。它们用的是一个在你自己电脑上运行的小模型,同样免费。

🇬🇧 English

One AI agent is easy to follow. You give it a job in a terminal and you watch it work. Ten agents are a different problem. Which one is doing what? Did two of them start the same job? What has it all cost this month, and who stops an agent that will not stop? These are the questions a company answers for its human staff with job titles, a list of tasks, a budget and a record of who did what.

Paperclip answers them for agents in the same way. It is a program that you run on your own computer. Inside it you open a company, hire agents into an org chart, and hand out tasks. Paperclip wakes each agent when it has work, makes sure only one agent holds a task, counts what each one spends, and writes down everything that happens.

Paperclip is not written in Python. It is a Node.js server. But everything it can do is offered through a web API, and a web API can be used from any language. So this page uses Python for both sides of the story. First you are the owner, and your scripts send requests to Paperclip. Then you are the employee: you write the program that Paperclip starts when a task arrives.

The company on this page is Three Geckos Noodles, the Chiang Mai noodle shop from RAG with Python and Fine-tune a small language model. Its cashier is a Python file that adds up bills from a price list. It does not use an AI model, and that is on purpose. What this page teaches is the company around the agent: how an agent is woken, how it claims a task, how it reports, and how it is stopped. All of that is the same whether the program inside adds up a bill or asks a large model to write code. Tutorial 5 then puts a model inside: four agents that review a research paper, look up facts and write it again.

Nothing here is copied from a manual in the hope that it works. Every file was run on 9 or 10 October 2026 against Paperclip 2026.1005.0 on a Windows 11 laptop, and every output box on this page is what that run printed. In three places the program did something other than what the API pages in its repository say. In two of them its documentation site, docs.paperclip.ing, has it right. The page tells you each time, and shows the check that found it.

🇹🇭 ไทย

เอเจนต์ AI ตัวเดียวนั้นตามดูได้ง่าย คุณสั่งงานมันในเทอร์มินัล แล้วนั่งดูมันทำ แต่เอเจนต์สิบตัวเป็นปัญหาอีกแบบ ตัวไหนกำลังทำอะไรอยู่ มีสองตัวที่เริ่มทำงานเดียวกันหรือเปล่า เดือนนี้ทั้งหมดใช้เงินไปเท่าไร และใครจะหยุดเอเจนต์ที่ไม่ยอมหยุด คำถามเหล่านี้คือสิ่งที่บริษัทตอบให้พนักงานที่เป็นคน ด้วยตำแหน่งงาน รายการงาน งบประมาณ และบันทึกว่าใครทำอะไร

Paperclip ตอบคำถามเดียวกันให้เอเจนต์ด้วยวิธีเดียวกัน มันเป็นโปรแกรมที่คุณรันบนคอมพิวเตอร์ของคุณเอง ข้างในนั้นคุณเปิดบริษัท จ้างเอเจนต์เข้ามาอยู่ในผังองค์กร แล้วแจกงาน Paperclip ปลุกเอเจนต์แต่ละตัวเมื่อมีงาน ดูแลให้งานหนึ่งงานมีเอเจนต์ถืออยู่เพียงตัวเดียว นับว่าแต่ละตัวใช้เงินไปเท่าไร และจดทุกอย่างที่เกิดขึ้น

Paperclip ไม่ได้เขียนด้วย Python มันเป็นเซิร์ฟเวอร์ Node.js แต่ทุกอย่างที่มันทำได้เปิดให้ใช้ผ่านเว็บ API และเว็บ API ใช้จากภาษาไหนก็ได้ หน้านี้จึงใช้ Python กับทั้งสองฝั่งของเรื่อง ตอนแรกคุณเป็นเจ้าของ และสคริปต์ของคุณส่งคำขอไปหา Paperclip จากนั้นคุณเป็นพนักงาน คือคุณเขียนโปรแกรมที่ Paperclip สั่งให้เริ่มทำงานเมื่อมีงานเข้ามา

บริษัทในหน้านี้คือ Three Geckos Noodles ร้านก๋วยเตี๋ยวในเชียงใหม่จากหน้า RAG ด้วย Python และ ฟายน์จูนโมเดลภาษาขนาดเล็ก แคชเชียร์ของร้านเป็นไฟล์ Python ที่คิดเงินจากตารางราคา มันไม่ได้ใช้โมเดล AI และนั่นเป็นความตั้งใจ สิ่งที่หน้านี้สอนคือบริษัทที่อยู่รอบตัวเอเจนต์ ว่าเอเจนต์ถูกปลุกอย่างไร จองงานอย่างไร รายงานอย่างไร และถูกหยุดอย่างไร ทั้งหมดนี้เหมือนกันทุกประการ ไม่ว่าโปรแกรมข้างในจะบวกบิล หรือจะขอให้โมเดลขนาดใหญ่เขียนโค้ด จากนั้นบทที่ 5 จะใส่โมเดลเข้าไปข้างใน คือเอเจนต์สี่ตัวที่รีวิวรายงานวิจัย ค้นหาข้อเท็จจริง และเขียนรายงานใหม่

ไม่มีอะไรในหน้านี้ที่ลอกมาจากคู่มือแล้วหวังว่าจะใช้ได้ ทุกไฟล์ถูกรันเมื่อวันที่ 9 หรือ 10 ตุลาคม 2026 กับ Paperclip 2026.1005.0 บนแล็ปท็อป Windows 11 และกล่องผลลัพธ์ทุกกล่องในหน้านี้คือสิ่งที่การรันครั้งนั้นพิมพ์ออกมา มีสามจุดที่โปรแกรมทำไม่ตรงกับที่หน้าเอกสาร API ใน repository ของมันเขียนไว้ ในสองจุดนั้น เว็บไซต์เอกสารของมัน docs.paperclip.ing เขียนไว้ถูกต้อง หน้านี้บอกทุกครั้ง และแสดงสคริปต์ตรวจสอบที่เจอเรื่องนั้น

🇨🇳 中文

一个 AI 智能体很容易盯住。你在终端里给它一件事,然后看着它做。十个智能体就是另一回事了。哪一个在做什么?有没有两个同时做起了同一件事?这个月一共花了多少钱?一个停不下来的智能体,由谁来叫停?公司对它的人类员工,是用职位、任务清单、预算和“谁做了什么”的记录来回答这些问题的。

Paperclip 用同样的办法替智能体回答这些问题。它是一个运行在你自己电脑上的程序。在它里面,你开一家公司,把智能体雇进组织架构图,再把任务派下去。有活的时候,Paperclip 把相应的智能体叫醒;它保证一个任务只在一个智能体手里;它统计每个智能体花了多少钱;发生的每一件事,它都记下来。

Paperclip 不是用 Python 写的,它是一个 Node.js 服务器。但它能做的一切都通过 Web API 提供,而 Web API 用哪种语言都能调用。所以这一页在故事的两边都用 Python。先是你当老板,你的脚本向 Paperclip 发请求。然后你当员工:任务一到,Paperclip 启动的那个程序由你来写。

这一页里的公司叫 Three Geckos Noodles,就是《用 Python 做 RAG》和《微调一个小语言模型》里那家清迈面馆。它的收银员是一个 Python 文件,照着价目表算账单。它没有用 AI 模型,这是故意的。这一页要教的是智能体周围的那家公司:智能体怎样被叫醒,怎样认领任务,怎样汇报,又怎样被叫停。不管里面的程序是在算一张账单,还是在请一个大模型写代码,这些都一模一样。然后教程 5 把模型放了进去:四个智能体审一篇研究报告、查找事实、再把它重写一遍。

这里没有哪一步是从手册上抄来、指望它能用的。每个文件都在 2026 年 10 月 9 日或 10 日、在一台 Windows 11 笔记本上、对着 Paperclip 2026.1005.0 运行过,页面上的每个输出框都是那次运行打印出来的东西。有三处,程序的做法和它仓库里的 API 文档写的不一样。其中两处,它的文档网站 docs.paperclip.ing 写的是对的。每一处页面都会告诉你,并给出发现它的那个检查脚本。

The page in three and a half minutes. Watch it first, so that you know where the code is going. The voice is a computer's and the drawings are made by code: every number and every line of terminal text in them is taken from this page's saved outputs. The subtitles follow the language you switch on above.ทั้งหน้านี้ในสามนาทีครึ่ง ดูก่อนเริ่ม จะได้รู้ว่าโค้ดกำลังพาไปทางไหน เสียงบรรยายเป็นเสียงคอมพิวเตอร์ และภาพทั้งหมดวาดด้วยโค้ด ตัวเลขทุกตัวและข้อความเทอร์มินัลทุกบรรทัดในภาพมาจากผลลัพธ์ที่บันทึกไว้ของหน้านี้ คำบรรยายใต้ภาพเปลี่ยนตามภาษาที่คุณเปิดไว้ด้านบน三分半钟看完这一页。先看一遍,这样你就知道代码要往哪里走。旁白是电脑的声音,画面是用代码画出来的:里面的每个数字、每一行终端文字,都取自这一页保存下来的输出。字幕跟着你在上面打开的语言走。

There is also a ten-minute video in Thai, with Thai subtitles, that walks through the whole page: Krueng AI Lab on YouTube. It is public from 11 October 2026, 18:00 Thailand time.มีวิดีโอภาษาไทยยาวสิบนาที พร้อมคำบรรยายไทย ที่พาเดินดูทั้งหน้านี้: Krueng AI Lab บน YouTube เปิดให้ดูตั้งแต่วันที่ 11 ตุลาคม 2026 เวลา 18:00 ตามเวลาประเทศไทย还有一个十分钟的泰语视频,配泰语字幕,把这一整页讲了一遍:YouTube 上的 Krueng AI Lab。它从 2026 年 10 月 11 日泰国时间 18:00 起公开。

PAPERCLIP IN ITS MAKERS' WORDS The project's README describes it like this: Paperclip is a Node.js server and React UI that orchestrates a team of AI agents to run a business. It also says what Paperclip is not: Not an agent framework. We don't tell you how to build agents. We tell you how to run a company made of them. And it has one line that explains this whole page: If it can receive a heartbeat, it's hired. A heartbeat is Paperclip's word for waking an agent. A Python file can be woken, so a Python file can be hired.README ของโครงการอธิบายตัวเองไว้ว่า Paperclip is a Node.js server and React UI that orchestrates a team of AI agents to run a business. แปลว่า Paperclip คือเซิร์ฟเวอร์ Node.js กับหน้าจอ React ที่ประสานทีมเอเจนต์ AI ให้ดำเนินธุรกิจ และยังบอกด้วยว่า Paperclip ไม่ใช่อะไร Not an agent framework. We don't tell you how to build agents. We tell you how to run a company made of them. คือมันไม่ใช่เฟรมเวิร์กสำหรับสร้างเอเจนต์ มันไม่บอกว่าต้องสร้างเอเจนต์อย่างไร แต่บอกว่าจะบริหารบริษัทที่ประกอบด้วยเอเจนต์อย่างไร และมีอยู่ประโยคหนึ่งที่อธิบายหน้านี้ได้ทั้งหน้า If it can receive a heartbeat, it's hired. ถ้ามันรับฮาร์ตบีตได้ มันก็ถูกจ้างได้ ฮาร์ตบีตคือคำที่ Paperclip ใช้เรียกการปลุกเอเจนต์ ไฟล์ Python ถูกปลุกได้ ไฟล์ Python จึงถูกจ้างได้项目的 README 是这样介绍自己的:Paperclip is a Node.js server and React UI that orchestrates a team of AI agents to run a business. 意思是:Paperclip 是一个 Node.js 服务器加一个 React 界面,它协调一组 AI 智能体来经营一门生意。它也说了 Paperclip 不是什么:Not an agent framework. We don't tell you how to build agents. We tell you how to run a company made of them. 也就是说,它不是用来造智能体的框架;它不管你怎么造智能体,只管怎样经营一家由智能体组成的公司。还有一句话,能把这一整页讲明白:If it can receive a heartbeat, it's hired. 只要能接收心跳,就能被雇用。“心跳”是 Paperclip 对“叫醒一个智能体”的叫法。一个 Python 文件可以被叫醒,所以一个 Python 文件可以被雇用。
Fileไฟล์文件What it doesทำอะไร它做什么Stepขั้นที่步骤
hello.pyAsks Paperclip whether it is running. One request.ถาม Paperclip ว่ากำลังทำงานอยู่ไหม ด้วยคำขอครั้งเดียว问 Paperclip 是否在运行。只发一次请求。2
company.pyOpens the company and gives it a goal.เปิดบริษัทและกำหนดเป้าหมายให้开公司,并给它一个目标。3
cashier.pyThe employee. Paperclip starts this file when the cashier has a task.ตัวพนักงาน Paperclip สั่งรันไฟล์นี้เมื่อแคชเชียร์มีงาน员工本人。收银员有任务时,Paperclip 就启动这个文件。4
hire.pyHires a boss and the cashier, and prints the org chart.จ้างหัวหน้ากับแคชเชียร์ แล้วพิมพ์ผังองค์กร雇一位老板和一个收银员,并打印组织架构图。5
order.pyGives the cashier a task and waits for the answer.มอบงานให้แคชเชียร์ แล้วรอคำตอบ给收银员一个任务,然后等结果。6, 7, 10
correct.pyWrites a comment on a task, which wakes the agent again.เขียนคอมเมนต์ลงในงาน ซึ่งจะปลุกเอเจนต์อีกครั้ง在任务上写一条评论,这会再次把智能体叫醒。7, 10
runs.pyLists each time the cashier was started, and what it printed.แสดงทุกครั้งที่แคชเชียร์ถูกสั่งรัน และสิ่งที่มันพิมพ์列出收银员每一次被启动的记录,以及它打印了什么。8
race.pyTwo cashiers ask for one task. One gets it.แคชเชียร์สองตัวขอรับงานเดียวกัน มีตัวเดียวที่ได้两个收银员要同一个任务,只有一个拿得到。9
budget.pySays how much the cashier may spend in a month.กำหนดว่าแคชเชียร์ใช้เงินได้เดือนละเท่าไร规定收银员一个月最多能花多少钱。10
spend.pyReports one pretend cost of 40 cents.รายงานค่าใช้จ่ายสมมุติหนึ่งครั้ง 40 เซนต์上报一笔假装的花费:40 美分。10
report.pyPrints the state of the company and its activity log.พิมพ์สภาพของบริษัทและบันทึกกิจกรรม打印公司的现状和它的活动日志。11
paper_agent.pyFour employees in one file: a reviewer, a researcher, a writer and an overseer.พนักงานสี่คนในไฟล์เดียว คือผู้รีวิว นักค้นคว้า นักเขียน และผู้คุมงาน一个文件里的四个员工:审稿人、研究员、写手和监督员。13
paper.pyHires those four and hands them a paper to improve.จ้างทั้งสี่ตัว แล้วส่งรายงานให้พวกมันปรับปรุง雇用这四个,并把一篇文章交给它们去改进。14
WHAT YOU NEED Node.js 24.11 or newer, because Paperclip is a Node.js program. Python 3.8 or newer, with the requests library. About 1,374 MB of free disk for Paperclip itself, which npm downloads the first time. A terminal: PowerShell on Windows, Terminal on a Mac. You do not need an account with anyone, an API key or a graphics card. Tutorial 5 also needs Ollama, 1.9 GB of disk for its model, and the ddgs library: step 12 installs them.Node.js 24.11 ขึ้นไป เพราะ Paperclip เป็นโปรแกรม Node.js กับ Python 3.8 ขึ้นไปที่มีไลบรารี requests พื้นที่ดิสก์ว่างราว 1,374 MB สำหรับตัว Paperclip ซึ่ง npm จะดาวน์โหลดในครั้งแรก และเทอร์มินัล บน Windows คือ PowerShell บนแมคคือ Terminal ไม่ต้องมีบัญชีกับใคร ไม่ต้องมีคีย์ API และไม่ต้องมีการ์ดจอ บทที่ 5 ต้องมี Ollama พื้นที่ดิสก์ 1.9 GB สำหรับโมเดล และไลบรารี ddgs เพิ่มด้วย ขั้นที่ 12 ติดตั้งทั้งหมดนี้Node.js 24.11 或更新的版本,因为 Paperclip 是一个 Node.js 程序。Python 3.8 或更新的版本,装好 requests 库。大约 1,374 MB 的空闲磁盘,留给 Paperclip 本身,npm 会在第一次运行时把它下载下来。一个终端:Windows 上是 PowerShell,Mac 上是“终端”。不需要在任何地方注册账号,不需要 API 密钥,也不需要显卡。教程 5 还需要 Ollama、给模型用的 1.9 GB 磁盘,以及 ddgs 库:第 12 步会把它们装好。
Wordคำ词What it means on this pageความหมายในหน้านี้在这一页里的意思
agentA program that is given tasks and does them without a person steering each step. Usually it has an AI model inside. Ours has a price list.เอเจนต์: โปรแกรมที่รับงานไปทำเองได้ โดยไม่ต้องมีคนคอยบังคับทีละขั้น ปกติข้างในมีโมเดล AI ส่วนของเรามีตารางราคา智能体:接到任务后自己去完成的程序,不用人一步一步地指挥。它里面通常有一个 AI 模型,我们的这个里面是一张价目表。
APIThe set of requests that a program accepts from other programs. Paperclip's API is at http://127.0.0.1:3100/api.ชุดคำขอที่โปรแกรมหนึ่งรับจากโปรแกรมอื่น API ของ Paperclip อยู่ที่ http://127.0.0.1:3100/api一个程序愿意接受的、来自其他程序的全部请求。Paperclip 的 API 在 http://127.0.0.1:3100/api。
requestOne message to an API, and the answer that comes back. GET asks for something, POST makes something new, PATCH changes something.คำขอ: ข้อความหนึ่งฉบับที่ส่งไปยัง API และคำตอบที่ได้กลับมา GET คือขอดูข้อมูล POST คือสร้างของใหม่ PATCH คือแก้ของที่มีอยู่请求:发给 API 的一条消息,以及回来的那个答复。GET 是要东西,POST 是新建东西,PATCH 是修改东西。
JSONThe text form in which the API sends data. In Python it becomes dictionaries and lists.รูปแบบข้อความที่ API ใช้ส่งข้อมูล ใน Python มันจะกลายเป็นดิกชันนารีและลิสต์API 传数据时用的文本格式。到了 Python 里,它变成字典和列表。
status codeThe number at the start of every answer. 200 and 201 mean yes. 409 means somebody else already has it.รหัสสถานะ: ตัวเลขที่อยู่ต้นคำตอบทุกครั้ง 200 และ 201 แปลว่าได้ ส่วน 409 แปลว่ามีคนอื่นได้ไปแล้ว状态码:每个答复开头的那个数字。200 和 201 表示可以;409 表示已经被别人拿走了。
companyIn Paperclip, one group of agents with its own tasks, goal and budget. One Paperclip can hold many companies.บริษัท: ใน Paperclip คือเอเจนต์หนึ่งกลุ่มที่มีงาน เป้าหมาย และงบประมาณเป็นของตัวเอง Paperclip หนึ่งตัวมีได้หลายบริษัท公司:在 Paperclip 里,是一组智能体,有自己的任务、目标和预算。一个 Paperclip 里可以有很多家公司。
boardThe people who own the company. On this page the board is you.บอร์ด: คนที่เป็นเจ้าของบริษัท ในหน้านี้บอร์ดคือคุณ董事会:拥有这家公司的人。在这一页里,董事会就是你。
issuePaperclip's name for a task. Each has a short name such as THR-1.ชื่อที่ Paperclip ใช้เรียกงานหนึ่งงาน แต่ละงานมีชื่อสั้น เช่น THR-1Paperclip 对“任务”的叫法。每个任务都有一个短名字,比如 THR-1。
to assignTo give a task to one agent. In Paperclip this is what wakes the agent.มอบหมาย: ยกงานให้เอเจนต์ตัวหนึ่ง ใน Paperclip การทำแบบนี้คือสิ่งที่ปลุกเอเจนต์指派:把一个任务交给某个智能体。在 Paperclip 里,正是这个动作把智能体叫醒。
heartbeatOne waking of an agent: Paperclip starts it, it works, it stops. Also called a run.ฮาร์ตบีต: การปลุกเอเจนต์หนึ่งครั้ง คือ Paperclip สั่งให้มันเริ่ม มันทำงาน แล้วมันหยุด เรียกอีกอย่างว่าการรันหนึ่งรอบ (run)心跳:把智能体叫醒一次,即 Paperclip 启动它,它干活,然后停下。也叫一次运行(run)。
adapterHow Paperclip starts one kind of agent. The process adapter starts a program on your computer.อะแดปเตอร์: วิธีที่ Paperclip ใช้สั่งเริ่มเอเจนต์ชนิดหนึ่ง อะแดปเตอร์ process สั่งรันโปรแกรมบนคอมพิวเตอร์ของคุณ适配器:Paperclip 启动某一类智能体的方式。process 适配器的做法是在你的电脑上启动一个程序。
checkoutThe request by which an agent claims a task, so that no other agent can.คำขอที่เอเจนต์ใช้จองงานหนึ่งงาน เพื่อไม่ให้เอเจนต์ตัวอื่นเอาไปได้智能体用来认领一个任务的请求,认领之后别的智能体就拿不走了。
environment variableA named piece of text that one program hands to a program it starts. Python reads them from os.environ.ตัวแปรสภาพแวดล้อม: ข้อความที่มีชื่อกำกับ ซึ่งโปรแกรมหนึ่งส่งให้โปรแกรมที่มันสั่งรัน Python อ่านค่าพวกนี้ได้จาก os.environ环境变量:一段有名字的文本,由一个程序交给它所启动的程序。Python 从 os.environ 里读取它们。
budgetThe most that an agent may cost in one month, in cents.งบประมาณ: จำนวนเงินสูงสุดที่เอเจนต์ตัวหนึ่งใช้ได้ในหนึ่งเดือน หน่วยเป็นเซนต์预算:一个智能体一个月最多可以花掉的钱,以美分计。

Tutorial 1: Start Paperclip and say helloบทที่ 1: เปิด Paperclip แล้วทักทาย教程 1:启动 Paperclip,打个招呼

🇬🇧 English

Paperclip runs as a server: a program that stays running and waits for requests. In this tutorial you start it, and then send it the smallest request there is.

🇹🇭 ไทย

Paperclip ทำงานเป็นเซิร์ฟเวอร์ คือโปรแกรมที่เปิดค้างไว้และคอยรับคำขอ ในบทนี้คุณจะเปิดมันขึ้นมา แล้วส่งคำขอที่เล็กที่สุดเท่าที่มีไปหามัน

🇨🇳 中文

Paperclip 以服务器的方式运行:一个一直开着、等着接收请求的程序。在这个教程里,你先把它启动起来,再给它发一个最小的请求。

1 Start Paperclipเปิด Paperclip启动 Paperclip

🇬🇧 English

Make a new folder for this page and open a terminal in it. One command downloads Paperclip and starts it. npx comes with Node.js: it fetches a program from the npm registry and runs it, without installing it for good.

Each part of the command has a reason. @2026.1005.0 asks for the exact version this page was written with, so that what you see matches. onboard --yes sets Paperclip up with its ordinary choices and starts it. --no-install-service tells it not to make itself a background service that starts with your computer. --data-dir paperclip-data keeps everything Paperclip stores in one folder beside your files, and not in your home folder.

The first start takes several minutes, because npm downloads 1,374 MB in 295 packages. Later starts take seconds. When Paperclip is ready it prints a summary like the one below, and it opens its own web page in your browser. Leave this terminal alone from now on, because Paperclip lives in it. Open a second terminal in the same folder for everything else.

🇹🇭 ไทย

สร้างโฟลเดอร์ใหม่สำหรับหน้านี้ แล้วเปิดเทอร์มินัลในโฟลเดอร์นั้น คำสั่งเดียวจะดาวน์โหลด Paperclip และเปิดมันขึ้นมา npx มากับ Node.js มันไปดึงโปรแกรมจากคลัง npm มารันให้ โดยไม่ได้ติดตั้งถาวร

แต่ละส่วนของคำสั่งมีเหตุผล @2026.1005.0 ขอเวอร์ชันเดียวกับที่ใช้เขียนหน้านี้ เพื่อให้สิ่งที่คุณเห็นตรงกับในหน้า onboard --yes ตั้งค่า Paperclip ด้วยตัวเลือกปกติแล้วเปิดมัน --no-install-service บอกว่าไม่ต้องทำตัวเองเป็นบริการเบื้องหลังที่เปิดพร้อมคอมพิวเตอร์ ส่วน --data-dir paperclip-data ให้ Paperclip เก็บทุกอย่างไว้ในโฟลเดอร์เดียวข้าง ๆ ไฟล์ของคุณ ไม่ใช่ในโฮมโฟลเดอร์

การเปิดครั้งแรกใช้เวลาหลายนาที เพราะ npm ต้องดาวน์โหลด 1,374 MB ใน 295 แพ็กเกจ ครั้งต่อ ๆ ไปใช้เวลาไม่กี่วินาที เมื่อ Paperclip พร้อม มันจะพิมพ์สรุปแบบข้างล่าง และเปิดหน้าเว็บของมันเองในเบราว์เซอร์ จากนี้ไปอย่าแตะเทอร์มินัลนี้ เพราะ Paperclip อยู่ในนั้น ให้เปิดเทอร์มินัลที่สองในโฟลเดอร์เดียวกันไว้ใช้กับอย่างอื่นทั้งหมด

🇨🇳 中文

为这一页新建一个文件夹,在里面打开一个终端。一条命令就能把 Paperclip 下载下来并启动。npx 是 Node.js 自带的:它从 npm 仓库取来一个程序并运行,但不把它长期安装在电脑上。

命令的每一部分都有原因。@2026.1005.0 指定了写这一页时用的那个版本,这样你看到的才和页面一致。onboard --yes 按常用选项把 Paperclip 设置好并启动。--no-install-service 告诉它不要把自己装成随电脑开机启动的后台服务。--data-dir paperclip-data 让 Paperclip 把它保存的一切都放进你这些文件旁边的一个文件夹里,而不是放到你的主目录里。

第一次启动要好几分钟,因为 npm 要下载 1,374 MB、共 295 个包。之后再启动只要几秒。Paperclip 就绪后会打印一份像下面这样的摘要,并在浏览器里打开它自己的网页。从现在起别再碰这个终端,因为 Paperclip 就住在里面。在同一个文件夹里再开第二个终端,其余的事都在那里做。

TERMINAL 1 · START PAPERCLIP, AND LEAVE IT RUNNING

npx paperclipai@2026.1005.0 onboard --yes --no-install-service --data-dir paperclip-data

IT PRINTS · OUR DATA FOLDER WAS C:\pclab\data

Mode             embedded-postgres  |  static-ui
Deploy           local_trusted (private)
Bind             loopback (127.0.0.1)
Auth             ready
Server           3100
API              http://127.0.0.1:3100/api (health: http://127.0.0.1:3100/api/health)
UI               http://127.0.0.1:3100
Database         C:\pclab\data\instances\default\db (pg:54329)
Agent JWT        set
Heartbeat        enabled (30000ms)
Config           C:\pclab\data\instances\default\config.json
THERE IS NO PASSWORD, AND THAT IS SAFE ONLY ON YOUR OWN COMPUTER Read two lines of that summary. Deploy local_trusted means Paperclip asks nobody to log in: every request it receives is treated as coming from the owner. Bind loopback (127.0.0.1) is what makes that acceptable. 127.0.0.1 is the address by which a computer talks to itself, so only programs on your own computer can reach Paperclip, and no other computer on the network can. Do not change the bind setting while you follow this page. Paperclip also reports anonymous usage figures to its makers unless you ask it not to. To switch that off, set the environment variable DO_NOT_TRACK to 1 before you start it. We ran it that way, and with PAPERCLIP_NO_BROWSER=1, which stops it opening the browser.อ่านสองบรรทัดในสรุปนั้นให้ดี Deploy local_trusted แปลว่า Paperclip ไม่ให้ใครล็อกอิน คำขอทุกครั้งที่มันได้รับถือว่ามาจากเจ้าของ ส่วน Bind loopback (127.0.0.1) คือสิ่งที่ทำให้เรื่องนี้ยอมรับได้ 127.0.0.1 คือที่อยู่ที่คอมพิวเตอร์ใช้คุยกับตัวเอง จึงมีแต่โปรแกรมบนคอมพิวเตอร์ของคุณเองที่เข้าถึง Paperclip ได้ คอมพิวเตอร์เครื่องอื่นในเครือข่ายเข้าไม่ได้ ระหว่างทำตามหน้านี้อย่าเปลี่ยนค่า bind นอกจากนี้ Paperclip ยังส่งตัวเลขการใช้งานแบบไม่ระบุตัวตนไปให้ผู้พัฒนา เว้นแต่คุณจะบอกว่าไม่ต้อง ถ้าจะปิด ให้ตั้งตัวแปรสภาพแวดล้อม DO_NOT_TRACK เป็น 1 ก่อนเปิดมัน เรารันแบบนั้น และตั้ง PAPERCLIP_NO_BROWSER=1 ด้วย ซึ่งทำให้มันไม่เปิดเบราว์เซอร์那份摘要里有两行要看仔细。Deploy local_trusted 的意思是 Paperclip 不要求任何人登录:它收到的每一个请求,都被当作是老板发来的。Bind loopback (127.0.0.1) 才让这件事说得过去。127.0.0.1 是电脑跟自己说话时用的地址,所以只有你自己电脑上的程序能连到 Paperclip,网络上别的电脑都连不到。跟着这一页做的时候,不要改 bind 这个设置。另外,除非你要求它不要这样做,Paperclip 会把匿名的使用统计发给它的开发者。想关掉,就在启动之前把环境变量 DO_NOT_TRACK 设成 1。我们就是这样运行的,同时还设了 PAPERCLIP_NO_BROWSER=1,它让 Paperclip 不去打开浏览器。

🇬🇧 English

Now the Python side, in the second terminal. Make a virtual environment, which is a separate copy of Python for this project, and install the one library the page uses. requests sends web requests and reads the answers.

From here on, every command on this page begins with the Python inside that environment. On Windows that is venv\Scripts\python. On a Mac, write venv/bin/python in its place.

🇹🇭 ไทย

ต่อไปเป็นฝั่ง Python ในเทอร์มินัลที่สอง สร้างสภาพแวดล้อมเสมือน ซึ่งเป็น Python อีกชุดที่แยกไว้สำหรับโปรเจกต์นี้ แล้วติดตั้งไลบรารีตัวเดียวที่หน้านี้ใช้ requests ทำหน้าที่ส่งคำขอทางเว็บและอ่านคำตอบ

นับจากนี้ ทุกคำสั่งในหน้านี้ขึ้นต้นด้วย Python ที่อยู่ในสภาพแวดล้อมนั้น บน Windows คือ venv\Scripts\python บนแมคให้เขียน venv/bin/python แทน

🇨🇳 中文

现在轮到 Python 这一边,在第二个终端里做。先建一个虚拟环境,也就是专给这个项目用的一份独立的 Python,再装上这一页唯一用到的库。requests 负责发送网络请求并读取答复。

从这里开始,这一页的每条命令都以那个环境里的 Python 开头。在 Windows 上是 venv\Scripts\python;在 Mac 上,把它换成 venv/bin/python。

TERMINAL 2 · WINDOWS

python -m venv venv
venv\Scripts\python -m pip install requests

TERMINAL 2 · MAC · not run by us · เราไม่ได้รันเอง · 我们没有运行过

python3 -m venv venv
venv/bin/python -m pip install requests

2 hello.py: one requesthello.py: คำขอหนึ่งครั้งhello.py:一次请求

🇬🇧 English

Every file on this page talks to Paperclip in the same way, and this file shows it with nothing else in the way. requests.get sends a request to an address and waits for the answer. The address is API plus /health, a path that Paperclip offers so that other programs can ask whether it is alive.

The answer arrives as JSON text. .json() turns that text into a Python dictionary, and from then on you read it like any dictionary: health["version"].

🇹🇭 ไทย

ทุกไฟล์ในหน้านี้คุยกับ Paperclip ด้วยวิธีเดียวกัน และไฟล์นี้แสดงวิธีนั้นให้เห็นโดยไม่มีอย่างอื่นมาบัง requests.get ส่งคำขอไปยังที่อยู่หนึ่งแล้วรอคำตอบ ที่อยู่นั้นคือ API ต่อด้วย /health ซึ่งเป็นพาธที่ Paperclip เปิดไว้ให้โปรแกรมอื่นถามได้ว่ามันยังทำงานอยู่ไหม

คำตอบกลับมาเป็นข้อความ JSON .json() เปลี่ยนข้อความนั้นเป็นดิกชันนารีของ Python แล้วจากนั้นคุณก็อ่านมันเหมือนดิกชันนารีทั่วไป: health["version"]

🇨🇳 中文

这一页的每个文件都用同一种方式跟 Paperclip 说话,这个文件把这种方式单独摆出来,旁边没有别的东西挡着。requests.get 向一个地址发出请求,然后等答复。这个地址是 API 加上 /health,这是 Paperclip 专门提供的一个路径,让别的程序可以问它是否还活着。

答复是一段 JSON 文本。.json() 把这段文本变成一个 Python 字典,之后你就像读任何字典那样读它:health["version"]。

RUN IT

venv\Scripts\python hello.py

NEW FILE · hello.py

"""hello.py - ask Paperclip whether it is running.

    python hello.py
"""
import requests

API = "http://127.0.0.1:3100/api"


def main():
    health = requests.get(API + "/health").json()
    print("Paperclip says:", health["status"])
    print("Version:", health["version"])
    print("Mode:", health["deploymentMode"])


if __name__ == "__main__":
    main()

IT PRINTS

Paperclip says: ok
Version: 2026.1005.0
Mode: local_trusted
YOU GET Three lines, and proof that Python can reach Paperclip. If you get an error that ends in Connection refused or Max retries exceeded, Paperclip is not running: look at the first terminal.สามบรรทัด และหลักฐานว่า Python ติดต่อ Paperclip ได้ ถ้าคุณเจอข้อผิดพลาดที่ลงท้ายด้วย Connection refused หรือ Max retries exceeded แปลว่า Paperclip ไม่ได้ทำงานอยู่ ให้ไปดูที่เทอร์มินัลแรก三行字,证明 Python 连得上 Paperclip。如果你得到一个以 Connection refused 或 Max retries exceeded 结尾的错误,那是 Paperclip 没有在运行:去看看第一个终端。

Tutorial 2: A company made from Pythonบทที่ 2: บริษัทที่สร้างด้วย Python教程 2:用 Python 建起来的公司

🇬🇧 English

You could do everything in this tutorial by clicking in Paperclip's web page. Doing it from Python teaches you the API, and it leaves you with files that can build the same company again on another computer.

🇹🇭 ไทย

ทุกอย่างในบทนี้คุณทำได้ด้วยการคลิกในหน้าเว็บของ Paperclip ก็ได้ แต่การทำจาก Python สอนให้คุณรู้จัก API และทำให้คุณมีไฟล์ที่ใช้สร้างบริษัทเดิมขึ้นมาใหม่บนคอมพิวเตอร์เครื่องอื่นได้

🇨🇳 中文

这个教程里的每一件事,你都可以在 Paperclip 的网页上点点鼠标完成。用 Python 来做,能让你学会它的 API,还能给你留下一批文件,换一台电脑也能把同一家公司再建一遍。

3 company.py: open the companycompany.py: เปิดบริษัทcompany.py:开一家公司

🇬🇧 English

Two small functions do most of the work in this file, and you will see them again in the files that follow.

send makes one request. It takes the kind of request ("GET" to ask, "POST" to make something new), the path, and for a POST the data to send. The line reply.raise_for_status() matters more than it looks. Without it, a refusal from Paperclip would pass silently and the program would fail a few lines later, in a confusing place. With it, the program stops at the request that was refused.

find looks through a list of dictionaries for the one whose key has a given value. The API always answers a question such as "which companies are there?" with a list, so this function turns up everywhere.

Notice the shape of main: look first, and make the company only if it is not there. Because of that you can run the file as often as you like, and you still have one company. The same goes for the goal. A goal says why the company exists, and Paperclip attaches it to every task, so that an agent can always see what its work is for.

🇹🇭 ไทย

ฟังก์ชันเล็ก ๆ สองตัวทำงานส่วนใหญ่ในไฟล์นี้ และคุณจะเจอมันอีกในไฟล์ถัด ๆ ไป

send ส่งคำขอหนึ่งครั้ง มันรับชนิดของคำขอ ("GET" เพื่อถาม "POST" เพื่อสร้างของใหม่) รับพาธ และถ้าเป็น POST ก็รับข้อมูลที่จะส่งด้วย บรรทัด reply.raise_for_status() สำคัญกว่าที่เห็น ถ้าไม่มีบรรทัดนี้ เวลา Paperclip ปฏิเสธ โปรแกรมจะผ่านไปเงียบ ๆ แล้วไปพังในอีกไม่กี่บรรทัดถัดไป ตรงจุดที่ชวนงง เมื่อมีบรรทัดนี้ โปรแกรมจะหยุดตรงคำขอที่ถูกปฏิเสธเลย

find ไล่ดูลิสต์ของดิกชันนารี เพื่อหาตัวที่คีย์มีค่าตามที่กำหนด API ตอบคำถามอย่าง "มีบริษัทอะไรบ้าง" เป็นลิสต์เสมอ ฟังก์ชันนี้จึงโผล่มาทุกที่

สังเกตรูปร่างของ main ให้ดี คือดูก่อน แล้วสร้างบริษัทเฉพาะเมื่อยังไม่มี เพราะแบบนี้คุณจึงรันไฟล์ได้กี่ครั้งก็ได้ และยังมีบริษัทเดียวเหมือนเดิม เป้าหมายก็เช่นกัน เป้าหมายบอกว่าบริษัทมีอยู่เพื่ออะไร และ Paperclip แนบมันไปกับทุกงาน เอเจนต์จึงมองเห็นได้เสมอว่างานของตัวเองทำไปเพื่ออะไร

🇨🇳 中文

这个文件里的大部分活,是两个小函数干的;在后面的文件里你还会再见到它们。

send 发一次请求。它接收请求的种类("GET" 是询问,"POST" 是新建)、路径,如果是 POST,还接收要发送的数据。reply.raise_for_status() 这一行比看上去重要得多。没有它,Paperclip 拒绝了请求,程序也会悄悄地继续往下走,过几行才在一个让人摸不着头脑的地方出错。有了它,程序就停在被拒绝的那个请求上。

find 在一个由字典组成的列表里,找出某个键等于给定值的那一个。对“有哪些公司?”这类问题,API 的回答永远是一个列表,所以这个函数到处都用得上。

注意 main 的写法:先看一看,公司不存在才去建。正因为这样,这个文件你想运行多少次都行,公司始终只有一家。目标也是一样。目标说明这家公司为什么存在,Paperclip 会把它附在每一个任务上,这样智能体随时都看得到自己的活是为了什么。

RUN IT

venv\Scripts\python company.py

NEW FILE · company.py

"""company.py - open a company in Paperclip, and give it a goal.

    python company.py

Run it twice and the second run changes nothing: it finds the company again.
"""
import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"
ABOUT = "A noodle shop in Chiang Mai. Its staff are programs."
GOAL = "Give every table a correct bill"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def main():
    company = find(send("GET", "/companies"), "name", NAME)
    if company is None:
        company = send("POST", "/companies", {"name": NAME, "description": ABOUT})
        print("Opened a new company.")
    goals_path = "/companies/" + company["id"] + "/goals"
    goal = find(send("GET", goals_path), "title", GOAL)
    if goal is None:
        goal = send("POST", goals_path,
                    {"title": GOAL, "level": "company", "status": "active"})
        print("Gave it a goal.")
    print("Company:", company["name"])
    print("Status:", company["status"])
    print("Its tasks will be numbered", company["issuePrefix"] + "-1,",
          company["issuePrefix"] + "-2, ...")
    print("Goal:", goal["title"])


if __name__ == "__main__":
    main()

IT PRINTS

Opened a new company.
Gave it a goal.
Company: Three Geckos Noodles
Status: active
Its tasks will be numbered THR-1, THR-2, ...
Goal: Give every table a correct bill
YOU GET A company. Paperclip chose the letters THR from its name, and every task in the company will carry them: THR-1, THR-2 and so on. Run the file a second time and the first two lines do not appear, because nothing new had to be made.บริษัทหนึ่งบริษัท Paperclip เลือกตัวอักษร THR จากชื่อของมัน และทุกงานในบริษัทจะใช้ตัวอักษรนี้: THR-1, THR-2 ไปเรื่อย ๆ รันไฟล์อีกครั้ง สองบรรทัดแรกจะไม่ขึ้นมา เพราะไม่มีอะไรใหม่ต้องสร้าง一家公司。Paperclip 从公司名字里取了 THR 这几个字母,公司里的每个任务都会带着它们:THR-1、THR-2,依此类推。把文件再运行一遍,前两行就不会出现了,因为没有什么新东西要建。

4 cashier.py: the employee is a Python filecashier.py: พนักงานคือไฟล์ Pythoncashier.py:员工就是一个 Python 文件

🇬🇧 English

Read the employee before you hire it. The file has two halves.

The first half is the skill. bill takes an order written as text, such as "2 khao soi, 1 water", cuts it at the commas, looks up each dish in PRICES and adds up. It returns two things at once: True and the finished bill, or False and a sentence about what went wrong. This half knows nothing about Paperclip, so you can try it alone, with the first command below.

The second half is the job. work is what runs when Paperclip starts the file, and it does four things for each task. It asks for the tasks that belong to this agent. It claims one with a checkout request. It works out the bill. Then it changes the task with a PATCH request: the status becomes done and the bill is attached as a comment.

How does the file know where Paperclip is, and which agent it is? Paperclip tells it. When Paperclip starts a program it hands over environment variables: 24 of them have names that begin with PAPERCLIP_, and this file reads five. PAPERCLIP_API_URL is the address to send requests to. PAPERCLIP_AGENT_ID and PAPERCLIP_COMPANY_ID say who the agent is and where it works. PAPERCLIP_API_KEY is a key that is valid for this one run and proves that a request really comes from this agent. PAPERCLIP_RUN_ID names the run, and the file sends it along with every change, so that Paperclip's records can say which run changed what.

That key is also how main decides what to do. If there is an order on the command line, it adds it up and stops. If the key is there, Paperclip started the file, so it works. Otherwise it prints one sentence of help.

🇹🇭 ไทย

อ่านตัวพนักงานก่อนจะจ้างมัน ไฟล์นี้มีสองครึ่ง

ครึ่งแรกคือฝีมือ bill รับออร์เดอร์ที่เขียนเป็นข้อความ เช่น "2 khao soi, 1 water" ตัดตรงเครื่องหมายจุลภาค เปิดดูราคาแต่ละจานใน PRICES แล้วบวกรวมกัน มันคืนค่าสองอย่างพร้อมกัน คือ True กับบิลที่คิดเสร็จแล้ว หรือ False กับประโยคที่บอกว่าผิดตรงไหน ครึ่งนี้ไม่รู้จัก Paperclip เลย คุณจึงลองมันเดี่ยว ๆ ได้ด้วยคำสั่งแรกข้างล่าง

ครึ่งหลังคือหน้าที่ work คือสิ่งที่ทำงานเมื่อ Paperclip สั่งรันไฟล์นี้ และกับงานแต่ละงานมันทำสี่อย่าง ขอดูงานที่เป็นของเอเจนต์ตัวนี้ จองงานหนึ่งงานด้วยคำขอ checkout คิดบิล แล้วแก้งานด้วยคำขอ PATCH ให้สถานะเป็น done พร้อมแนบบิลไว้เป็นคอมเมนต์

ไฟล์รู้ได้อย่างไรว่า Paperclip อยู่ที่ไหน และตัวเองเป็นเอเจนต์ตัวไหน Paperclip เป็นคนบอก เวลา Paperclip สั่งรันโปรแกรม มันส่งตัวแปรสภาพแวดล้อมมาให้ มี 24 ตัวที่ชื่อขึ้นต้นด้วย PAPERCLIP_ และไฟล์นี้อ่านห้าตัว PAPERCLIP_API_URL คือที่อยู่สำหรับส่งคำขอ PAPERCLIP_AGENT_ID กับ PAPERCLIP_COMPANY_ID บอกว่าเอเจนต์เป็นใครและทำงานที่ไหน PAPERCLIP_API_KEY คือคีย์ที่ใช้ได้เฉพาะการรันรอบนี้ และพิสูจน์ว่าคำขอมาจากเอเจนต์ตัวนี้จริง PAPERCLIP_RUN_ID เป็นชื่อของการรันรอบนี้ ไฟล์ส่งมันไปพร้อมกับการแก้ไขทุกครั้ง เพื่อให้บันทึกของ Paperclip บอกได้ว่าการรันรอบไหนแก้อะไร

คีย์ตัวนั้นยังเป็นสิ่งที่ main ใช้ตัดสินว่าจะทำอะไร ถ้ามีออร์เดอร์อยู่ในบรรทัดคำสั่ง มันก็คิดเงินแล้วจบ ถ้ามีคีย์อยู่ แปลว่า Paperclip เป็นคนสั่งรันไฟล์ มันจึงทำงาน ถ้าไม่ใช่ทั้งสองอย่าง มันพิมพ์ประโยคช่วยเหลือหนึ่งประโยค

🇨🇳 中文

雇用之前,先把这位员工读一遍。这个文件分两半。

前一半是本事。bill 接收一份写成文字的订单,比如 "2 khao soi, 1 water",在逗号处切开,到 PRICES 里查每道菜的价钱,再加起来。它一次返回两样东西:True 和算好的账单,或者 False 和一句说明哪里出了问题的话。这一半完全不知道 Paperclip 的存在,所以你可以用下面第一条命令单独试它。

后一半是工作。Paperclip 启动这个文件时,运行的就是 work,它对每个任务做四件事。先要来属于这个智能体的任务。再用一个 checkout 请求认领其中一个。然后算出账单。最后用一个 PATCH 请求修改任务:状态变成 done,账单作为评论附在上面。

这个文件怎么知道 Paperclip 在哪里,又怎么知道自己是哪个智能体?是 Paperclip 告诉它的。Paperclip 启动一个程序时,会交给它一些环境变量:名字以 PAPERCLIP_ 开头的有 24 个,这个文件读其中五个。PAPERCLIP_API_URL 是发送请求的地址。PAPERCLIP_AGENT_ID 和 PAPERCLIP_COMPANY_ID 说明这个智能体是谁、在哪家公司干活。PAPERCLIP_API_KEY 是只在这一次运行中有效的密钥,用来证明请求确实来自这个智能体。PAPERCLIP_RUN_ID 是这次运行的名字,文件每做一次修改都把它一起发过去,这样 Paperclip 的记录里就能写明哪一次运行改了什么。

main 也是靠这个密钥来决定做什么的。命令行上有订单,它就把账算出来,然后结束。有这个密钥,说明文件是 Paperclip 启动的,它就开始干活。两样都没有,它就打印一句提示。

TRY THE SUM ALONE

venv\Scripts\python cashier.py "2 khao soi, 1 thai iced tea"

IT PRINTS

- 2 x khao soi = 120 baht
- 1 x thai iced tea = 30 baht
- Total: 150 baht

AND WITH NOTHING AFTER ITS NAME

Give me an order to add up, or let Paperclip start me.

NEW FILE · cashier.py

"""cashier.py - an employee of the company. It is a Python file, and it adds up bills.

Paperclip starts this file each time the cashier has work to do. To try the
sum by itself, with no Paperclip, hand it an order:

    python cashier.py "2 khao soi, 1 thai iced tea"
"""
import os
import sys

import requests

PRICES = {"khao soi": 60, "nam ngiao": 50, "pad thai": 60, "pad see ew": 55,
          "thai iced tea": 30, "longan juice": 25, "water": 10}


def bill(order):
    """Read an order such as "2 khao soi, 1 water".

    Returns two things: True and the bill, or False and what went wrong.
    """
    lines = []
    total = 0
    for part in order.lower().split(","):
        words = part.split()
        if len(words) < 2 or not words[0].isdigit():
            return False, "I cannot read this part of the order: " + part.strip()
        count = int(words[0])
        dish = " ".join(words[1:])
        if dish not in PRICES:
            return False, "I do not know the price of " + dish + "."
        cost = count * PRICES[dish]
        lines.append(f"- {count} x {dish} = {cost} baht")
        total = total + cost
    lines.append(f"- Total: {total} baht")
    return True, "\n".join(lines)


def order_of(task, comments):
    """The order to add up: the newest comment a person wrote, or else the task itself."""
    for comment in comments:                 # Paperclip lists the newest comment first
        if comment["authorType"] == "user":
            return comment["body"]
    return task["description"] or ""


def ask(method, url, headers, data=None):
    """One request to Paperclip, made as this agent. Returns the answer as Python data."""
    reply = requests.request(method, url, headers=headers, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def work():
    """Do every task that Paperclip has given to this agent."""
    api = os.environ["PAPERCLIP_API_URL"] + "/api"
    agent = os.environ["PAPERCLIP_AGENT_ID"]
    company = os.environ["PAPERCLIP_COMPANY_ID"]
    # The key says who is asking. The run id ties each change to this run.
    headers = {"Authorization": "Bearer " + os.environ["PAPERCLIP_API_KEY"],
               "X-Paperclip-Run-Id": os.environ["PAPERCLIP_RUN_ID"]}

    mine = (f"{api}/companies/{company}/issues"
            f"?assigneeAgentId={agent}&status=todo,in_progress")
    for task in ask("GET", mine, headers):
        task_url = f"{api}/issues/{task['id']}"
        # No ask() here: a 409 is an answer we expect, and we carry on after it.
        claim = requests.post(task_url + "/checkout", headers=headers,
                              json={"agentId": agent,
                                    "expectedStatuses": ["todo", "in_progress"]})
        if claim.status_code == 409:         # another agent already has this task
            print(task["identifier"], "is taken")
            continue
        claim.raise_for_status()
        comments = ask("GET", task_url + "/comments", headers)
        added_up, text = bill(order_of(task, comments))
        if added_up:
            result = {"status": "done", "comment": text}
        else:
            # Paperclip refuses "blocked" unless it is told who acts next, and how.
            waiting = {"owner": {"agentId": agent},
                       "action": "Read the corrected order in the next comment."}
            result = {"status": "blocked", "comment": text,
                      "unblockDescriptor": waiting}
        ask("PATCH", task_url, headers, result)
        print(task["identifier"], result["status"])


def main():
    if len(sys.argv) > 1:                    # an order was given: just add it up
        added_up, text = bill(sys.argv[1])
        print(text)
    elif "PAPERCLIP_API_KEY" in os.environ:  # Paperclip started this file
        work()
    else:
        print("Give me an order to add up, or let Paperclip start me.")


if __name__ == "__main__":
    main()
YOU GET A bill of 150 baht, from a file that has not met Paperclip yet. Keep this habit when you write agents of your own: put the skill in a function that you can run by hand, and keep the talking to Paperclip in another.บิล 150 บาท จากไฟล์ที่ยังไม่เคยเจอ Paperclip เวลาคุณเขียนเอเจนต์ของตัวเอง ให้ทำแบบนี้จนเป็นนิสัย คือเอาฝีมือไว้ในฟังก์ชันที่รันด้วยมือได้ และแยกส่วนที่คุยกับ Paperclip ไว้ในอีกฟังก์ชันหนึ่ง一张 150 泰铢的账单,出自一个还没见过 Paperclip 的文件。你自己写智能体的时候,请保持这个习惯:把本事放进一个可以手动运行的函数里,把跟 Paperclip 打交道的部分放在另一个函数里。

5 hire.py: hire it, and draw the org charthire.py: จ้างมัน แล้ววาดผังองค์กรhire.py:雇用它,并画出组织架构图

🇬🇧 English

To hire an agent you send Paperclip its name, its role and its adapter. The adapter is the part that matters. It tells Paperclip how to wake this agent. Paperclip has adapters for well-known coding agents such as Claude Code and Codex, and one named process for everything else. process means: to wake this agent, start a program on this computer.

Which program? That is the adapterConfig: a command and its arguments. Look at how hire.py fills them in. The command is sys.executable, which is the full path of the Python that is running hire.py at this moment. The argument is the full path of cashier.py. Both choices are deliberate. Paperclip will start the cashier later, from a folder of its own, where a plain python might be a different Python that has no requests library, and where the name cashier.py alone would not be found. Full paths remove both problems.

Noi, the owner of the shop, is hired first, as the ceo. She gets no program. On this page she is a seat in the org chart and nothing more, and nobody gives her a task. The cashier is hired with reportsTo set to Noi's id, and that one field is what makes an org chart: every agent names its boss.

The last lines print the chart. Paperclip returns it as a tree: a list of agents, and inside each agent a list named reports with the agents below it. show prints one level and then calls itself for the level below, with a wider indent.

🇹🇭 ไทย

การจ้างเอเจนต์คือการส่งชื่อ ตำแหน่ง และอะแดปเตอร์ของมันไปให้ Paperclip ส่วนที่สำคัญคืออะแดปเตอร์ มันบอก Paperclip ว่าจะปลุกเอเจนต์ตัวนี้อย่างไร Paperclip มีอะแดปเตอร์สำหรับเอเจนต์เขียนโค้ดที่รู้จักกันดี เช่น Claude Code และ Codex และมีอีกตัวชื่อ process สำหรับอย่างอื่นทั้งหมด process แปลว่า ถ้าจะปลุกเอเจนต์ตัวนี้ ให้สั่งรันโปรแกรมหนึ่งตัวบนคอมพิวเตอร์เครื่องนี้

โปรแกรมตัวไหน นั่นคือ adapterConfig ซึ่งประกอบด้วยคำสั่งและอาร์กิวเมนต์ของมัน ดูว่า hire.py เติมสองอย่างนี้อย่างไร คำสั่งคือ sys.executable ซึ่งเป็นพาธเต็มของ Python ตัวที่กำลังรัน hire.py อยู่ในขณะนี้ ส่วนอาร์กิวเมนต์คือพาธเต็มของ cashier.py ทั้งสองอย่างเลือกมาโดยตั้งใจ Paperclip จะสั่งรันแคชเชียร์ทีหลัง จากโฟลเดอร์ของมันเอง ซึ่งที่นั่น python เฉย ๆ อาจเป็น Python อีกชุดที่ไม่มีไลบรารี requests และชื่อ cashier.py อย่างเดียวก็จะหาไม่เจอ พาธเต็มแก้ปัญหาทั้งสองข้อ

น้อย เจ้าของร้าน ถูกจ้างก่อน ในตำแหน่ง ceo เธอไม่มีโปรแกรม ในหน้านี้เธอเป็นแค่ตำแหน่งหนึ่งในผังองค์กร และไม่มีใครมอบงานให้เธอ แคชเชียร์ถูกจ้างโดยตั้ง reportsTo เป็น id ของน้อย และฟิลด์เดียวนี้เองที่ทำให้เกิดผังองค์กร คือเอเจนต์ทุกตัวระบุว่าหัวหน้าของตัวเองคือใคร

บรรทัดสุดท้ายพิมพ์ผังออกมา Paperclip ส่งผังกลับมาเป็นโครงสร้างต้นไม้ คือลิสต์ของเอเจนต์ และในเอเจนต์แต่ละตัวมีลิสต์ชื่อ reports ที่เก็บเอเจนต์ใต้บังคับบัญชา show พิมพ์หนึ่งชั้น แล้วเรียกตัวเองซ้ำสำหรับชั้นถัดลงไป โดยเยื้องเข้ามากขึ้น

🇨🇳 中文

雇一个智能体,就是把它的名字、职位和适配器发给 Paperclip。要紧的是适配器。它告诉 Paperclip 怎样叫醒这个智能体。Paperclip 为 Claude Code、Codex 这些有名的编程智能体准备了适配器,另外还有一个叫 process 的,用来对付其余的一切。process 的意思是:要叫醒这个智能体,就在这台电脑上启动一个程序。

哪个程序?这由 adapterConfig 来说明:一条命令,加上它的参数。看看 hire.py 是怎么填的。命令是 sys.executable,也就是此刻正在运行 hire.py 的那个 Python 的完整路径。参数是 cashier.py 的完整路径。这两处都是有意这样写的。Paperclip 以后启动收银员时,是在它自己的一个文件夹里启动的;在那里,单写一个 python 可能指向另一份没装 requests 库的 Python,单写 cashier.py 这个名字也找不到文件。写完整路径,两个问题都没有了。

店主 Noi 最先被雇用,职位是 ceo。她没有程序。在这一页里,她只是组织架构图上的一个位置,没有人给她派任务。雇收银员的时候,把 reportsTo 设成 Noi 的 id,组织架构图就是靠这一个字段搭起来的:每个智能体都写明自己的上司是谁。

最后几行把架构图打印出来。Paperclip 返回的是一棵树:一个智能体的列表,每个智能体里面又有一个叫 reports 的列表,装着它手下的智能体。show 打印一层,然后调用它自己去打印下一层,缩进更宽一些。

RUN IT

venv\Scripts\python hire.py

NEW FILE · hire.py

"""hire.py - hire two agents into the company, and print the org chart.

    python hire.py

Noi is the boss. The cashier is cashier.py, which must be in this folder.
"""
import sys
from pathlib import Path

import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def hire(company, name, more):
    """The agent with this name. It is hired first if the company lacks it."""
    agents_path = "/companies/" + company["id"] + "/agents"
    agent = find(send("GET", agents_path), "name", name)
    if agent is None:
        details = {"name": name, "adapterType": "process"}
        details.update(more)
        agent = send("POST", agents_path, details)
        print("Hired", name + ".")
    return agent


def show(people, depth):
    """Print a list of agents, and under each one the agents who report to it."""
    for person in people:
        print("    " * depth + person["name"], "(" + person["role"] + ")")
        show(person["reports"], depth + 1)


def main():
    company = find(send("GET", "/companies"), "name", NAME)
    if company is None:
        print("There is no company yet. Run company.py first.")
        return
    noi = hire(company, "Noi", {"role": "ceo", "title": "Owner"})
    # "process" means: to wake this agent, start a program. This is the program.
    script = Path(__file__).resolve().parent / "cashier.py"
    start = {"command": sys.executable, "args": [str(script)]}
    hire(company, "Cashier", {"role": "general", "title": "Cashier",
                              "reportsTo": noi["id"], "adapterConfig": start})
    print("The org chart:")
    show(send("GET", "/companies/" + company["id"] + "/org"), 1)


if __name__ == "__main__":
    main()

IT PRINTS

Hired Noi.
Hired Cashier.
The org chart:
    Noi (ceo)
        Cashier (general)
THE BOARD You, with your Python files CEO Noi · no program GENERAL Cashier · runs cashier.py
The company after step 5. The words in small capitals are the roles that Paperclip stores. Only the yellow box has a program behind it.บริษัทหลังขั้นที่ 5 คำตัวพิมพ์ใหญ่ขนาดเล็กคือตำแหน่งที่ Paperclip เก็บไว้ มีแต่กล่องสีเหลืองเท่านั้นที่มีโปรแกรมอยู่เบื้องหลัง第 5 步之后的公司。小号大写字母是 Paperclip 保存的职位。只有黄色的那个方框背后有程序。
YOUR TURN
  1. Open http://127.0.0.1:3100 in a browser. Find Three Geckos Noodles, its goal, and the two agents you hired from Python.
  2. Run hire.py again. Why does it print the chart, and not hire anybody a second time?
  1. เปิด http://127.0.0.1:3100 ในเบราว์เซอร์ หา Three Geckos Noodles เป้าหมายของมัน และเอเจนต์สองตัวที่คุณจ้างจาก Python
  2. รัน hire.py อีกครั้ง ทำไมมันพิมพ์ผัง แต่ไม่จ้างใครซ้ำเป็นครั้งที่สอง
  1. 在浏览器里打开 http://127.0.0.1:3100。找到 Three Geckos Noodles、它的目标,以及你用 Python 雇来的两个智能体。
  2. 再运行一次 hire.py。为什么它只打印架构图,而没有把谁再雇一遍?

Tutorial 3: Give it workบทที่ 3: มอบงานให้มัน教程 3:给它派活

🇬🇧 English

The company exists and its cashier has never run. In this tutorial you give it tasks, and you find out what Paperclip does when a task goes well, when the agent cannot do it, and where to look when you want to know what the agent did.

🇹🇭 ไทย

บริษัทมีอยู่แล้ว แต่แคชเชียร์ยังไม่เคยทำงานเลยสักครั้ง ในบทนี้คุณจะมอบงานให้มัน แล้วดูว่า Paperclip ทำอย่างไรเมื่องานไปได้ดี เมื่อเอเจนต์ทำงานนั้นไม่ได้ และต้องไปดูที่ไหนเมื่ออยากรู้ว่าเอเจนต์ทำอะไรไปบ้าง

🇨🇳 中文

公司有了,收银员却一次都还没运行过。在这个教程里,你给它派任务,并看一看:任务顺利时 Paperclip 怎么做,智能体做不了时它怎么做,以及想知道智能体到底干了什么时该去哪里看。

6 order.py: a task wakes the agentorder.py: งานปลุกเอเจนต์order.py:任务把智能体叫醒

🇬🇧 English

order.py sends one POST request. It creates an issue, which is Paperclip's word for a task, with a title, the order as its description, the status todo, and the cashier's id in assigneeAgentId.

After that the file does nothing but wait. Twice a second it asks Paperclip for the task again and looks at its status. Nowhere in this file, or in any other file on this page, is there a line that starts cashier.py. Giving the task to the cashier is what starts it. Paperclip sees a todo task with an agent's name on it, looks up that agent's adapter, and runs the command you stored in step 5.

The run on this page took 1.1 seconds from creating the task to reading done. To see how steady that is, a check gave the cashier 20 orders one after another: the fastest took 1.12 seconds, the middle one 1.37 and the slowest 2.34. Most of that second is Python starting up and importing requests.

🇹🇭 ไทย

order.py ส่งคำขอ POST หนึ่งครั้ง มันสร้าง issue ซึ่งเป็นคำที่ Paperclip ใช้เรียกงาน โดยมีชื่อเรื่อง มีออร์เดอร์เป็นคำอธิบาย มีสถานะ todo และมี id ของแคชเชียร์อยู่ใน assigneeAgentId

หลังจากนั้นไฟล์นี้ไม่ทำอะไรนอกจากรอ วินาทีละสองครั้ง มันขอดูงานนั้นจาก Paperclip อีกรอบแล้วดูสถานะ ไม่มีตรงไหนในไฟล์นี้ หรือในไฟล์อื่นใดของหน้านี้ ที่มีบรรทัดสั่งรัน cashier.py การยกงานให้แคชเชียร์นั่นเองที่ทำให้มันเริ่มทำงาน Paperclip เห็นงานสถานะ todo ที่มีชื่อเอเจนต์กำกับอยู่ ก็เปิดดูอะแดปเตอร์ของเอเจนต์ตัวนั้น แล้วรันคำสั่งที่คุณเก็บไว้ในขั้นที่ 5

การรันในหน้านี้ใช้เวลา 1.1 วินาที นับจากสร้างงานจนอ่านได้ว่า done เพื่อดูว่าเวลานี้คงที่แค่ไหน สคริปต์ตรวจสอบตัวหนึ่งส่งออร์เดอร์ให้แคชเชียร์ 20 ออร์เดอร์ต่อกัน ครั้งที่เร็วที่สุดใช้ 1.12 วินาที ครั้งที่อยู่ตรงกลางใช้ 1.37 และครั้งที่ช้าที่สุดใช้ 2.34 เวลาส่วนใหญ่ของวินาทีนั้นหมดไปกับการที่ Python เริ่มทำงานและ import requests

🇨🇳 中文

order.py 只发一个 POST 请求。它新建一个 issue,也就是 Paperclip 所说的任务:有标题,订单内容作为描述,状态是 todo,assigneeAgentId 里填的是收银员的 id。

之后这个文件除了等,什么也不做。它每秒两次向 Paperclip 重新要这个任务,看它的状态。在这个文件里,在这一页的任何别的文件里,都找不到一行启动 cashier.py 的代码。把任务交给收银员,这件事本身就启动了它。Paperclip 看到一个 todo 状态、上面写着某个智能体的任务,就去查那个智能体的适配器,然后运行你在第 5 步存进去的那条命令。

页面上的这一次运行,从新建任务到读到 done,用了 1.1 秒。为了看看这个时间稳不稳,有一个检查脚本接连给了收银员 20 份订单:最快的一次 1.12 秒,中间的一次 1.37 秒,最慢的一次 2.34 秒。这一秒钟里,大部分时间花在 Python 启动和 import requests 上。

RUN IT

venv\Scripts\python order.py "2 khao soi, 1 thai iced tea"

NEW FILE · order.py

"""order.py - give the cashier a task, wait, and print what came back.

    python order.py "2 khao soi, 1 thai iced tea"
"""
import sys
import time

import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def main():
    order = sys.argv[1]
    company = find(send("GET", "/companies"), "name", NAME)
    agents = send("GET", "/companies/" + company["id"] + "/agents")
    cashier = find(agents, "name", "Cashier")

    # A task is called an issue. Giving it to an agent is what wakes the agent.
    task = send("POST", "/companies/" + company["id"] + "/issues",
                {"title": "Bill: " + order, "description": order,
                 "status": "todo", "assigneeAgentId": cashier["id"]})
    print("Created", task["identifier"] + ":", task["title"])

    began = time.time()
    while task["status"] in ("todo", "in_progress"):
        if time.time() - began > 60:
            print("Nobody finished it in 60 seconds.")
            break
        time.sleep(0.5)
        task = send("GET", "/issues/" + task["id"])
    print("Status:", task["status"], f"(after {time.time() - began:.1f} seconds)")
    comments = send("GET", "/issues/" + task["id"] + "/comments")
    if comments:
        newest = comments[0]                 # Paperclip lists the newest comment first
        print("A comment, written by the " + newest["authorType"] + ":")
        print(newest["body"])


if __name__ == "__main__":
    main()

IT PRINTS

Created THR-1: Bill: 2 khao soi, 1 thai iced tea
Status: done (after 1.1 seconds)
A comment, written by the agent:
- 2 x khao soi = 120 baht
- 1 x thai iced tea = 30 baht
- Total: 150 baht
WHAT HAPPENED IN THAT SECOND Follow it through work() in cashier.py. Paperclip started the file and handed it the five variables. The file asked for its tasks and got one, THR-1. It sent a checkout request to claim it. It read the comments, found none written by a person, and so took the order from the task's description. bill() returned True and three lines. One PATCH request then set the status to done and attached the lines as a comment. The file printed THR-1 done and ended, and that is one heartbeat. Meanwhile order.py, which knows nothing about any of this, saw the status change and printed the comment.ไล่ตามเรื่องนี้ไปใน work() ของ cashier.py Paperclip สั่งรันไฟล์และส่งตัวแปรห้าตัวมาให้ ไฟล์ขอดูงานของตัวเองและได้มาหนึ่งงาน คือ THR-1 มันส่งคำขอ checkout เพื่อจองงานนั้น มันอ่านคอมเมนต์ ไม่พบคอมเมนต์ที่คนเขียน จึงเอาออร์เดอร์จากคำอธิบายของงาน bill() คืนค่า True กับข้อความสามบรรทัด จากนั้นคำขอ PATCH ครั้งเดียวตั้งสถานะเป็น done และแนบสามบรรทัดนั้นไว้เป็นคอมเมนต์ ไฟล์พิมพ์ THR-1 done แล้วจบ นั่นคือฮาร์ตบีตหนึ่งครั้ง ระหว่างนั้น order.py ซึ่งไม่รู้เรื่องพวกนี้เลย เห็นสถานะเปลี่ยน จึงพิมพ์คอมเมนต์ออกมา顺着 cashier.py 里的 work() 走一遍。Paperclip 启动了这个文件,并把那五个变量交给它。文件要来自己的任务,拿到一个:THR-1。它发出一个 checkout 请求来认领它。它读了评论,没有找到人写的评论,于是从任务的描述里取订单。bill() 返回 True 和三行字。接着,一个 PATCH 请求把状态设成 done,并把这三行作为评论附上去。文件打印出 THR-1 done,然后结束,这就是一次心跳。与此同时,对这一切一无所知的 order.py 看到状态变了,就把评论打印了出来。

7 correct.py: a task the agent cannot docorrect.py: งานที่เอเจนต์ทำไม่ได้correct.py:智能体做不了的任务

🇬🇧 English

Order something that is not on the menu. bill() returns False and a sentence, and now the cashier has to say so in a way that Paperclip accepts. It sets the status to blocked.

Paperclip does not let an agent write blocked and walk away. A blocked task with nobody responsible for it would sit there for ever, so the request must carry an unblockDescriptor: who acts next, and what they have to do. The cashier names itself, and gives the action as reading the corrected order in the next comment. Look for the word waiting in cashier.py.

Then you correct the order, with correct.py. It writes a comment on the task. A comment from a person on a task that an agent holds wakes that agent, just as the assignment did. This is why cashier.py has the function order_of: when it is woken it looks for the newest comment that a person wrote, and uses that as the order in place of the description.

🇹🇭 ไทย

ลองสั่งของที่ไม่มีในเมนู bill() คืนค่า False กับประโยคหนึ่งประโยค และคราวนี้แคชเชียร์ต้องบอกเรื่องนี้ในแบบที่ Paperclip ยอมรับ มันตั้งสถานะเป็น blocked

Paperclip ไม่ยอมให้เอเจนต์เขียนว่า blocked แล้วเดินหนีไป งานที่ติดขัดโดยไม่มีใครรับผิดชอบจะค้างอยู่อย่างนั้นตลอดไป คำขอจึงต้องมี unblockDescriptor ติดไปด้วย คือใครจะเป็นคนทำต่อ และต้องทำอะไร แคชเชียร์ระบุตัวมันเอง และบอกว่าสิ่งที่ต้องทำคืออ่านออร์เดอร์ที่แก้แล้วในคอมเมนต์ถัดไป ลองหาคำว่า waiting ใน cashier.py

จากนั้นคุณแก้ออร์เดอร์ด้วย correct.py มันเขียนคอมเมนต์ลงในงาน คอมเมนต์จากคน บนงานที่เอเจนต์ถืออยู่ จะปลุกเอเจนต์ตัวนั้น เหมือนกับที่การมอบหมายงานเคยปลุก นี่คือเหตุผลที่ cashier.py มีฟังก์ชัน order_of คือเมื่อถูกปลุก มันมองหาคอมเมนต์ล่าสุดที่คนเขียน แล้วใช้คอมเมนต์นั้นเป็นออร์เดอร์แทนคำอธิบาย

🇨🇳 中文

点一样菜单上没有的东西。bill() 返回 False 和一句话,这时收银员得用 Paperclip 肯接受的方式把这件事说出来。它把状态设成 blocked。

Paperclip 不允许智能体写下 blocked 就一走了之。一个卡住了却没人负责的任务,会永远搁在那里,所以这个请求必须带上一个 unblockDescriptor:下一步由谁来做,要做什么。收银员写的是它自己,要做的事是读下一条评论里改过的订单。在 cashier.py 里找一找 waiting 这个词。

然后由你用 correct.py 来改订单。它在任务上写一条评论。人在一个智能体手里的任务上写评论,会把那个智能体叫醒,就像当初指派任务把它叫醒一样。cashier.py 里之所以有 order_of 这个函数,原因就在这里:被叫醒之后,它去找人写的最新一条评论,用它来代替描述,当作订单。

RUN IT · AN ORDER WITH A DISH THE SHOP DOES NOT SELL

venv\Scripts\python order.py "1 pad thai, 2 pizza"

IT PRINTS

Created THR-2: Bill: 1 pad thai, 2 pizza
Status: blocked (after 1.1 seconds)
A comment, written by the agent:
I do not know the price of pizza.

THEN CORRECT IT

venv\Scripts\python correct.py THR-2 "1 pad thai, 2 water"

NEW FILE · correct.py

"""correct.py - write a comment on a task, wait, and print what came back.

    python correct.py THR-2 "1 pad thai, 2 water"

The comment is the corrected order. A comment from a person wakes the agent
that has the task.
"""
import sys
import time

import requests

API = "http://127.0.0.1:3100/api"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def main():
    number = sys.argv[1]                     # the task's short name, such as THR-2
    order = sys.argv[2]
    task = send("GET", "/issues/" + number)
    print(task["identifier"], "is", task["status"] + ".")
    send("POST", "/issues/" + task["id"] + "/comments", {"body": order})
    print("You wrote:", order)

    began = time.time()
    while task["status"] != "done":
        if time.time() - began > 30:
            print("Nobody finished it in 30 seconds.")
            break
        time.sleep(0.5)
        task = send("GET", "/issues/" + task["id"])
    print("Status:", task["status"], f"(after {time.time() - began:.1f} seconds)")
    comments = send("GET", "/issues/" + task["id"] + "/comments")
    newest = comments[0]                     # Paperclip lists the newest comment first
    print("A comment, written by the " + newest["authorType"] + ":")
    print(newest["body"])


if __name__ == "__main__":
    main()

IT PRINTS

THR-2 is blocked.
You wrote: 1 pad thai, 2 water
Status: done (after 1.1 seconds)
A comment, written by the agent:
- 1 x pad thai = 60 baht
- 2 x water = 20 baht
- Total: 80 baht
YOU GET A conversation that is kept on the task. THR-2 now holds the order, the cashier's question, your correction and the bill, in that order. A person who opens the task next month can read what happened without asking anybody.บทสนทนาที่เก็บอยู่กับตัวงาน ตอนนี้ THR-2 มีทั้งออร์เดอร์ คำถามของแคชเชียร์ การแก้ไขของคุณ และบิล เรียงตามลำดับนั้น คนที่เปิดงานนี้ในเดือนหน้าจะอ่านได้ว่าเกิดอะไรขึ้น โดยไม่ต้องถามใคร一段留在任务上的对话。THR-2 现在依次存着订单、收银员的疑问、你的更正和账单。下个月有人打开这个任务,不用问任何人,就能读出当时发生了什么。
YOUR TURN
  1. Open THR-2 in the web page at http://127.0.0.1:3100 and read its thread.
  2. Add "coconut ice cream": 35 to PRICES in cashier.py, and order one. You do not have to hire the cashier again. Why not?
  1. เปิด THR-2 ในหน้าเว็บที่ http://127.0.0.1:3100 แล้วอ่านเธรดของมัน
  2. เพิ่ม "coconut ice cream": 35 ลงใน PRICES ของ cashier.py แล้วสั่งหนึ่งถ้วย คุณไม่ต้องจ้างแคชเชียร์ใหม่ เพราะอะไร
  1. 在 http://127.0.0.1:3100 的网页里打开 THR-2,读一读它的对话。
  2. 往 cashier.py 的 PRICES 里加上 "coconut ice cream": 35,然后点一份。你不用把收银员重新雇一遍。为什么?

8 runs.py: what the agent printedruns.py: เอเจนต์พิมพ์อะไรออกมาruns.py:智能体打印了什么

🇬🇧 English

The cashier calls print, but it has no terminal of its own. Paperclip started it, so Paperclip catches what it prints and keeps it with the record of the run. runs.py reads those records.

There are 3 so far, and the words after "started by" say why Paperclip woke the agent. Two say assignment: the two orders. The newest says automation: that was your comment on THR-2.

This is the first place to look when an agent of yours does nothing. If the file had crashed, the run's status would be failed, its exit code would not be 0, and the Python error would be in the same record under stderr, beside stdout. The paths this file uses, which contain heartbeat-runs, are not in the API pages in Paperclip's repository. They are the ones its own web page uses, and the web page shows the same records under each agent.

🇹🇭 ไทย

แคชเชียร์เรียก print แต่มันไม่มีเทอร์มินัลของตัวเอง Paperclip เป็นคนสั่งรันมัน Paperclip จึงรับสิ่งที่มันพิมพ์ไว้ และเก็บไว้กับบันทึกของการรันรอบนั้น runs.py อ่านบันทึกเหล่านั้น

ถึงตอนนี้มี 3 รอบ และคำที่อยู่หลัง "started by" บอกว่าทำไม Paperclip ถึงปลุกเอเจนต์ สองรอบเขียนว่า assignment คือออร์เดอร์สองออร์เดอร์ รอบล่าสุดเขียนว่า automation นั่นคือคอมเมนต์ของคุณบน THR-2

นี่คือที่แรกที่ควรดู เมื่อเอเจนต์ของคุณไม่ทำอะไรเลย ถ้าไฟล์พัง สถานะของการรันจะเป็น failed exit code จะไม่ใช่ 0 และข้อผิดพลาดของ Python จะอยู่ในบันทึกเดียวกัน ใต้ stderr ข้าง ๆ stdout พาธที่ไฟล์นี้ใช้ ซึ่งมีคำว่า heartbeat-runs ไม่ได้อยู่ในหน้าเอกสาร API ใน repository ของ Paperclip มันเป็นพาธที่หน้าเว็บของ Paperclip เองใช้ และหน้าเว็บก็แสดงบันทึกชุดเดียวกันไว้ใต้เอเจนต์แต่ละตัว

🇨🇳 中文

收银员调用了 print,可它没有自己的终端。它是 Paperclip 启动的,所以它打印的东西由 Paperclip 接住,和那次运行的记录存在一起。runs.py 读的就是这些记录。

到现在一共有 3 次,“started by”后面的词说明 Paperclip 为什么叫醒了智能体。两次写着 assignment:那是两份订单。最新的一次写着 automation:那是你在 THR-2 上写的评论。

你的智能体毫无动静的时候,第一个该看的地方就是这里。如果文件崩溃了,这次运行的状态会是 failed,退出码不是 0,Python 的报错就在同一条记录的 stderr 里,紧挨着 stdout。这个文件用到的路径里带着 heartbeat-runs,它们不在 Paperclip 仓库里的 API 文档页面里。这是 Paperclip 自己的网页在用的路径,网页上每个智能体下面显示的也是同样的记录。

RUN IT

venv\Scripts\python runs.py

NEW FILE · runs.py

"""runs.py - list the times Paperclip started the cashier, and what the last one printed.

    python runs.py
"""
import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def main():
    company = find(send("GET", "/companies"), "name", NAME)
    agents = send("GET", "/companies/" + company["id"] + "/agents")
    cashier = find(agents, "name", "Cashier")
    runs = send("GET", "/companies/" + company["id"] + "/heartbeat-runs"
                + "?agentId=" + cashier["id"])
    print(len(runs), "runs, the newest first:")
    for run in runs:
        # invocationSource is why Paperclip started the agent.
        print("   ", run["status"], "- started by:", run["invocationSource"],
              "- exit code:", run["exitCode"])
    for run in runs:
        if run["status"] == "succeeded":     # a run that is still going has no output yet
            finished = send("GET", "/heartbeat-runs/" + run["id"])
            print("The newest finished run printed:")
            print(finished["resultJson"]["stdout"].strip())
            break


if __name__ == "__main__":
    main()

IT PRINTS

3 runs, the newest first:
    succeeded - started by: automation - exit code: 0
    succeeded - started by: assignment - exit code: 0
    succeeded - started by: assignment - exit code: 0
The newest finished run printed:
THR-2 done

Tutorial 4: The rules the company keepsบทที่ 4: กฎที่บริษัทรักษาไว้教程 4:公司守着的规矩

🇬🇧 English

So far Paperclip has been a postman: it carried a task to an agent and an answer back. The reason to use it and not a folder of scripts is in this tutorial. Paperclip enforces rules that you would otherwise have to write, test and trust yourself.

🇹🇭 ไทย

จนถึงตรงนี้ Paperclip เป็นแค่บุรุษไปรษณีย์ คือพางานไปส่งให้เอเจนต์ แล้วพาคำตอบกลับมา เหตุผลที่ควรใช้มันแทนโฟลเดอร์ที่เต็มไปด้วยสคริปต์อยู่ในบทนี้ Paperclip บังคับใช้กฎต่าง ๆ ซึ่งถ้าไม่มีมัน คุณต้องเขียนเอง ทดสอบเอง และเชื่อใจเอง

🇨🇳 中文

到目前为止,Paperclip 只是个邮递员:把任务送到智能体手里,再把答复带回来。为什么要用它,而不是用一文件夹的脚本,答案在这个教程里。Paperclip 会强制执行一些规矩;没有它,这些规矩你得自己写、自己测、自己去信任。

9 race.py: two agents, one taskrace.py: เอเจนต์สองตัว งานหนึ่งงานrace.py:两个智能体,一个任务

🇬🇧 English

With more than one agent, the first thing that can go wrong is two of them doing the same job. race.py sets that up. It hires a second cashier that runs the same cashier.py, and creates a task that belongs to nobody. A task with no agent on it wakes nobody, so it waits.

Then each cashier in turn asks for the task with a checkout request. The first gets the answer 200, and the task is now its own. The second gets 409, the status code for a conflict, and Paperclip's sentence Issue checkout conflict. Claiming the task also woke the winner, which did the work.

This file does not use send for those two requests, and the comment in it says why. send stops the program at any refusal. Here the refusal is the result we came to see.

Two requests one after the other are an easy test. A check on this page makes a harder one: 20 agents ask for the same task at the same instant, from 20 threads, and it does that five times. Every time, exactly one request was answered 200 and the other 19 were answered 409. Paperclip's API pages say what an agent should do with a 409: do not try again, and pick another task. That is what the line continue does in cashier.py.

🇹🇭 ไทย

เมื่อมีเอเจนต์มากกว่าหนึ่งตัว สิ่งแรกที่ผิดพลาดได้คือสองตัวทำงานเดียวกัน race.py จัดฉากนั้นขึ้นมา มันจ้างแคชเชียร์ตัวที่สองซึ่งรัน cashier.py ไฟล์เดียวกัน แล้วสร้างงานที่ไม่ได้เป็นของใคร งานที่ไม่มีเอเจนต์กำกับไม่ปลุกใคร มันจึงรออยู่

จากนั้นแคชเชียร์แต่ละตัวขอรับงานนั้นทีละตัวด้วยคำขอ checkout ตัวแรกได้คำตอบ 200 และงานเป็นของมันแล้ว ตัวที่สองได้ 409 ซึ่งเป็นรหัสสถานะของการชนกัน พร้อมประโยคของ Paperclip ว่า Issue checkout conflict การจองงานยังปลุกผู้ชนะด้วย และผู้ชนะก็ทำงานนั้นจนเสร็จ

ไฟล์นี้ไม่ได้ใช้ send กับคำขอสองครั้งนั้น และคอมเมนต์ในไฟล์บอกเหตุผลไว้ send หยุดโปรแกรมทุกครั้งที่ถูกปฏิเสธ แต่ตรงนี้การถูกปฏิเสธคือผลลัพธ์ที่เราตั้งใจมาดู

คำขอสองครั้งที่ส่งต่อกันเป็นการทดสอบที่ง่าย สคริปต์ตรวจสอบของหน้านี้ทำแบบที่ยากกว่า คือให้เอเจนต์ 20 ตัวขอรับงานเดียวกันในเสี้ยววินาทีเดียวกัน จาก 20 เธรด และทำอย่างนั้นห้ารอบ ทุกรอบมีคำขอเพียงครั้งเดียวที่ได้คำตอบ 200 และอีก 19 ครั้งได้ 409 หน้าเอกสาร API ของ Paperclip บอกไว้ว่าเอเจนต์ควรทำอย่างไรเมื่อเจอ 409 คืออย่าลองใหม่ และให้เลือกงานอื่น นั่นคือสิ่งที่บรรทัด continue ใน cashier.py ทำ

🇨🇳 中文

智能体不止一个的时候,最先可能出错的事,就是两个智能体做了同一件活。race.py 把这个场面摆了出来。它再雇一个收银员,运行的是同一个 cashier.py,然后新建一个不属于任何人的任务。上面没写智能体的任务不会叫醒谁,所以它就在那里等着。

接着两个收银员轮流用 checkout 请求来要这个任务。第一个得到的答复是 200,任务归它了。第二个得到 409,这是表示冲突的状态码,还有 Paperclip 的一句话:Issue checkout conflict。认领任务的同时也把赢的那个叫醒了,它把活干完了。

这个文件没有用 send 来发那两个请求,文件里的注释说了原因。send 一遇到拒绝就让程序停下。而在这里,被拒绝正是我们想看到的结果。

一前一后的两个请求,是容易的测试。这一页的一个检查脚本做了更难的:20 个智能体在同一瞬间、从 20 个线程里要同一个任务,这样做五轮。每一轮都恰好只有一个请求得到 200,另外 19 个得到 409。对于 409,Paperclip 的 API 文档写明了智能体该怎么办:不要重试,换一个任务。cashier.py 里的 continue 那一行做的就是这件事。

RUN IT

venv\Scripts\python race.py "3 nam ngiao"

NEW FILE · race.py

"""race.py - two cashiers reach for the same task. Only one of them gets it.

    python race.py "3 nam ngiao"
"""
import sys
import time
from pathlib import Path

import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def main():
    order = sys.argv[1]
    company = find(send("GET", "/companies"), "name", NAME)
    agents_path = "/companies/" + company["id"] + "/agents"
    first = find(send("GET", agents_path), "name", "Cashier")
    second = find(send("GET", agents_path), "name", "Cashier 2")
    if second is None:                       # the same file, hired a second time
        script = Path(__file__).resolve().parent / "cashier.py"
        start = {"command": sys.executable, "args": [str(script)]}
        second = send("POST", agents_path,
                      {"name": "Cashier 2", "role": "general", "title": "Cashier",
                       "adapterType": "process", "adapterConfig": start,
                       "reportsTo": first["reportsTo"]})
        print("Hired Cashier 2.")

    # A task that belongs to nobody. Nobody is woken, so it waits.
    task = send("POST", "/companies/" + company["id"] + "/issues",
                {"title": "Bill: " + order, "description": order, "status": "todo"})
    print("Created", task["identifier"] + ", and gave it to nobody.")

    for cashier in (first, second):
        # No send() here: this time we want to see the answer when it is a no.
        claim = requests.post(API + "/issues/" + task["id"] + "/checkout",
                              json={"agentId": cashier["id"],
                                    "expectedStatuses": ["todo"]})
        print(cashier["name"], "asks for it. Paperclip answers", claim.status_code)
        if claim.status_code == 409:
            print("   ", claim.json()["error"])

    time.sleep(3)                            # the winner is woken, and does the work
    task = send("GET", "/issues/" + task["id"])
    winner = find(send("GET", agents_path), "id", task["assigneeAgentId"])
    print(task["identifier"], "is", task["status"] + ". It was done by",
          winner["name"] + ".")


if __name__ == "__main__":
    main()

IT PRINTS

Hired Cashier 2.
Created THR-3, and gave it to nobody.
Cashier asks for it. Paperclip answers 200
Cashier 2 asks for it. Paperclip answers 409
    Issue checkout conflict
THR-3 is done. It was done by Cashier.

THE HARDER TEST · checks/race_many.py

Round 1: 20 requests at once. Answers: {200: 1, 409: 19}
Round 2: 20 requests at once. Answers: {200: 1, 409: 19}
Round 3: 20 requests at once. Answers: {200: 1, 409: 19}
Round 4: 20 requests at once. Answers: {200: 1, 409: 19}
Round 5: 20 requests at once. Answers: {200: 1, 409: 19}
(could not delete Check race: 500; archived it instead)
THE SAME TITLE TWICE GIVES YOU THE SAME TASK We met one more rule by accident, and we did not find it in the API pages. If you create a task with the same title as a task that is still open, Paperclip does not make a second one. It answers with the first. In our check that held when the second request named another agent, and when it named no agent. A different title made a new task, and so did the same title after the first task was cancelled. So if you send the order "2 water" twice while the first is still waiting, order.py prints the same THR number both times. The check is checks/same_title.py.เราเจอกฎอีกข้อหนึ่งโดยบังเอิญ และเราหาไม่เจอในหน้าเอกสาร API ถ้าคุณสร้างงานที่มีชื่อเรื่องเหมือนกับงานที่ยังเปิดอยู่ Paperclip จะไม่สร้างงานที่สอง มันตอบกลับมาด้วยงานแรก ในการตรวจสอบของเรา เป็นแบบนี้ทั้งเมื่อคำขอครั้งที่สองระบุเอเจนต์อีกตัว และเมื่อไม่ระบุเอเจนต์เลย ชื่อเรื่องที่ต่างออกไปทำให้ได้งานใหม่ และชื่อเรื่องเดิมก็ได้งานใหม่เช่นกันหลังจากงานแรกถูกยกเลิก ดังนั้นถ้าคุณส่งออร์เดอร์ "2 water" สองครั้งขณะที่ครั้งแรกยังรออยู่ order.py จะพิมพ์หมายเลข THR เดียวกันทั้งสองครั้ง สคริปต์ตรวจสอบคือ checks/same_title.py我们还无意中碰到了一条规矩,我们在 API 文档里没有找到。如果你新建的任务,标题和一个仍然开着的任务相同,Paperclip 不会建第二个,而是把第一个作为答复还给你。在我们的检查里,第二个请求写的是另一个智能体时是这样,不写任何智能体时也是这样。标题不同,就会建出新任务;第一个任务被取消以后,同样的标题也会建出新任务。所以,如果第一份 "2 water" 的订单还在等着,你又发了一遍,order.py 两次打印的会是同一个 THR 编号。这个检查脚本是 checks/same_title.py。

checks/same_title.py PRINTS

Same title, same agent, the first still open: the same task came back
Same title, another agent: the same task came back
Same title, given to nobody: the same task came back
Another title, same agent: a new task was made
Same title, same agent, after the first was cancelled: a new task was made
(could not delete Check same title: 500; archived it instead)

10 budget.py and spend.py: a budget that stops an agentbudget.py และ spend.py: งบประมาณที่หยุดเอเจนต์ได้budget.py 和 spend.py:能让智能体停下来的预算

🇬🇧 English

An agent with an AI model inside spends money each time it thinks, and an agent caught in a loop spends it quickly. Paperclip's answer is a monthly budget for each agent. budget.py sets one: 100 cents.

Our cashier costs nothing, so there is nothing to count. spend.py stands in for a model. It sends Paperclip a cost event, which is the report that a model-backed agent's adapter sends after each run: which agent, which model, how many tokens, and how many cents. Each time you run spend.py it reports 40 cents and then prints how the cashier stands.

Run it three times. After 40 and 80 cents the cashier is still idle, which means free and waiting for work. After 120 it is paused, and the reason Paperclip gives is budget. Nobody paused it. The third cost took it past its budget, and Paperclip acted.

🇹🇭 ไทย

เอเจนต์ที่มีโมเดล AI อยู่ข้างในใช้เงินทุกครั้งที่มันคิด และเอเจนต์ที่ติดอยู่ในลูปก็ใช้เงินเร็วมาก คำตอบของ Paperclip คืองบประมาณรายเดือนของเอเจนต์แต่ละตัว budget.py ตั้งไว้หนึ่งค่า คือ 100 เซนต์

แคชเชียร์ของเราไม่มีค่าใช้จ่าย จึงไม่มีอะไรให้นับ spend.py ทำหน้าที่แทนโมเดล มันส่ง cost event ไปให้ Paperclip ซึ่งเป็นรายงานแบบเดียวกับที่อะแดปเตอร์ของเอเจนต์ที่ใช้โมเดลส่งหลังการรันทุกรอบ ว่าเป็นเอเจนต์ตัวไหน โมเดลอะไร กี่โทเคน และกี่เซนต์ ทุกครั้งที่คุณรัน spend.py มันรายงาน 40 เซนต์ แล้วพิมพ์ว่าตอนนี้แคชเชียร์อยู่ในสภาพไหน

รันมันสามครั้ง หลัง 40 และ 80 เซนต์ แคชเชียร์ยังเป็น idle ซึ่งแปลว่าว่างและรองานอยู่ หลัง 120 มันเป็น paused และเหตุผลที่ Paperclip ให้ไว้คือ budget ไม่มีใครสั่งพักมัน ค่าใช้จ่ายครั้งที่สามทำให้มันเกินงบ แล้ว Paperclip ก็ลงมือเอง

🇨🇳 中文

里面装着 AI 模型的智能体,每思考一次都要花钱;陷进死循环的智能体,花起钱来很快。Paperclip 的办法是给每个智能体定一份月度预算。budget.py 定了一份:100 美分。

我们的收银员不花钱,没有什么可统计的。spend.py 来顶替模型的角色。它向 Paperclip 发一个 cost event(花费事件),也就是带模型的智能体,它的适配器在每次运行后发出的那种报告:哪个智能体,哪个模型,多少 token,多少美分。你每运行一次 spend.py,它就上报 40 美分,然后打印收银员眼下的情况。

把它运行三次。花到 40 和 80 美分时,收银员还是 idle,意思是空闲、等着活。花到 120 时,它变成了 paused,Paperclip 给出的原因是 budget。没有人去暂停它。是第三笔花费让它超出了预算,于是 Paperclip 动了手。

RUN IT

venv\Scripts\python budget.py 100

NEW FILE · budget.py

"""budget.py - say how much the cashier may spend in a month.

    python budget.py 100

The number is in cents, so 100 is one dollar.
"""
import sys
import time

import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def main():
    cents = int(sys.argv[1])
    company = find(send("GET", "/companies"), "name", NAME)
    agents = send("GET", "/companies/" + company["id"] + "/agents")
    cashier = find(agents, "name", "Cashier")
    send("PATCH", "/agents/" + cashier["id"] + "/budgets",
         {"budgetMonthlyCents": cents})
    time.sleep(1)                            # give Paperclip a moment to act on it
    cashier = send("GET", "/agents/" + cashier["id"])
    print("The cashier may spend", cashier["budgetMonthlyCents"], "cents a month.")
    print("Spent so far this month:", cashier["spentMonthlyCents"], "cents.")
    print("The cashier is", cashier["status"] + ".")


if __name__ == "__main__":
    main()

IT PRINTS

The cashier may spend 100 cents a month.
Spent so far this month: 0 cents.
The cashier is idle.

THEN SPEND, THREE TIMES

venv\Scripts\python spend.py
venv\Scripts\python spend.py
venv\Scripts\python spend.py

NEW FILE · spend.py

"""spend.py - tell Paperclip that the cashier has just cost 40 cents.

    python spend.py

The cashier costs nothing to run. An agent that calls a paid AI model does,
and after each run Paperclip is told what the run cost. This file sends one
such report by hand, so that you can see what a budget does.
"""
import time

import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"
COST = 40                                    # in cents


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def main():
    company = find(send("GET", "/companies"), "name", NAME)
    agents = send("GET", "/companies/" + company["id"] + "/agents")
    cashier = find(agents, "name", "Cashier")
    # The time now, in the form Paperclip asks for: 2026-10-09T14:43:20Z
    # The Z says it is UTC, the clock that is the same in every country.
    now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    send("POST", "/companies/" + company["id"] + "/cost-events",
         {"agentId": cashier["id"], "provider": "pretend", "model": "pretend",
          "inputTokens": 1000, "outputTokens": 200, "costCents": COST,
          "occurredAt": now})
    print("Reported a cost of", COST, "cents.")
    time.sleep(1)                            # give Paperclip a moment to act on it
    cashier = send("GET", "/agents/" + cashier["id"])
    print("Spent this month:", cashier["spentMonthlyCents"], "of",
          cashier["budgetMonthlyCents"], "cents.")
    print("The cashier is", cashier["status"] + ".")
    if cashier["pauseReason"]:
        print("Reason:", cashier["pauseReason"])


if __name__ == "__main__":
    main()

THE FIRST TIME IT PRINTS

Reported a cost of 40 cents.
Spent this month: 40 of 100 cents.
The cashier is idle.

THE SECOND TIME

Reported a cost of 40 cents.
Spent this month: 80 of 100 cents.
The cashier is idle.

THE THIRD TIME

Reported a cost of 40 cents.
Spent this month: 120 of 100 cents.
The cashier is paused.
Reason: budget

🇬🇧 English

Now give the paused cashier an order. Paperclip does not start it. In the run on this page, after 8.0 seconds Paperclip itself marked the task blocked and wrote a comment on it to say why. Look at who wrote that comment: not the agent and not you, but the system.

The way out is a decision by the board, which is you. Raise the budget with budget.py 1000 and the cashier is idle again. The blocked task does not restart by itself. It waits for a person, so write a comment on it with correct.py, and the cashier wakes and finishes it.

🇹🇭 ไทย

ทีนี้ลองส่งออร์เดอร์ให้แคชเชียร์ที่ถูกพักอยู่ Paperclip ไม่สั่งรันมัน ในการรันของหน้านี้ ผ่านไป 8.0 วินาที Paperclip ก็ตั้งสถานะงานเป็น blocked เอง และเขียนคอมเมนต์บอกเหตุผลไว้ ดูว่าใครเขียนคอมเมนต์นั้น ไม่ใช่เอเจนต์ และไม่ใช่คุณ แต่เป็นระบบ

ทางออกคือการตัดสินใจของบอร์ด ซึ่งก็คือคุณ เพิ่มงบด้วย budget.py 1000 แล้วแคชเชียร์จะกลับเป็น idle อีกครั้ง งานที่ติดขัดไม่เริ่มใหม่เอง มันรอคน ดังนั้นให้เขียนคอมเมนต์ลงไปด้วย correct.py แล้วแคชเชียร์จะตื่นมาทำงานนั้นจนเสร็จ

🇨🇳 中文

现在给这个被暂停的收银员一份订单。Paperclip 没有启动它。在页面上的这次运行里,过了 8.0 秒,Paperclip 自己把任务标成了 blocked,还在上面写了一条评论说明原因。看看这条评论是谁写的:不是智能体,也不是你,而是系统。

出路要由董事会来决定,也就是你。用 budget.py 1000 把预算调高,收银员就又是 idle 了。被卡住的任务不会自己重新开始。它在等人,所以用 correct.py 在上面写一条评论,收银员就会醒来把它做完。

AN ORDER FOR A PAUSED CASHIER

venv\Scripts\python order.py "2 water"

IT PRINTS

Created THR-4: Bill: 2 water
Status: blocked (after 8.0 seconds)
A comment, written by the system:
Paperclip cannot safely continue automatic recovery because the original assignee is not invokable. The source assignment is unchanged and the board must choose the next action.

RAISE THE BUDGET, THEN ANSWER THE TASK

venv\Scripts\python budget.py 1000
venv\Scripts\python correct.py THR-4 "2 water"

budget.py PRINTS

The cashier may spend 1000 cents a month.
Spent so far this month: 120 cents.
The cashier is idle.

correct.py PRINTS

THR-4 is blocked.
You wrote: 2 water
Status: done (after 1.7 seconds)
A comment, written by the agent:
- 2 x water = 20 baht
- Total: 20 baht
TWO REQUESTS SET A BUDGET, AND ONLY ONE OF THEM STOPPED THE AGENT Look at the path in budget.py: it ends in /budgets. The API pages in Paperclip's repository give a shorter request for this job, a PATCH to the agent itself with budgetMonthlyCents in it. Its documentation site, docs.paperclip.ing, gives the path that ends in /budgets. We tried the shorter request first. In version 2026.1005.0 it stored the number, and that was all: the agent went to 120 cents against a budget of 100 and stayed idle. The request to /budgets stored the same number and Paperclip enforced it. Our check checks/two_budgets.py does both to two fresh agents, side by side, and its output is below. The repository's pages also show a cost event without the field occurredAt, and the documentation site lists that field as required. Paperclip refused ours until it had one, in exactly the form you see in spend.py, ending in Z.ดูพาธใน budget.py มันลงท้ายด้วย /budgets หน้าเอกสาร API ใน repository ของ Paperclip ให้คำขอที่สั้นกว่าสำหรับงานนี้ คือ PATCH ไปที่ตัวเอเจนต์โดยตรง พร้อม budgetMonthlyCents ส่วนเว็บไซต์เอกสารของมัน docs.paperclip.ing ให้พาธที่ลงท้ายด้วย /budgets เราลองคำขอที่สั้นกว่าก่อน ในเวอร์ชัน 2026.1005.0 มันเก็บตัวเลขไว้ และมีแค่นั้น เอเจนต์ใช้ไปถึง 120 เซนต์ทั้งที่งบคือ 100 และยังเป็น idle อยู่ ส่วนคำขอไปที่ /budgets เก็บตัวเลขเดียวกัน และ Paperclip บังคับใช้ตามนั้น สคริปต์ตรวจสอบ checks/two_budgets.py ของเราทำทั้งสองแบบกับเอเจนต์ใหม่สองตัวเทียบกัน และผลลัพธ์อยู่ข้างล่าง หน้าเอกสารใน repository ยังแสดง cost event ที่ไม่มีฟิลด์ occurredAt ส่วนเว็บไซต์เอกสารระบุว่าฟิลด์นี้จำเป็นต้องมี Paperclip ปฏิเสธของเราจนกว่าจะมีฟิลด์นี้ ในรูปแบบเดียวกับที่คุณเห็นใน spend.py ทุกตัวอักษร คือลงท้ายด้วย Z看 budget.py 里的路径:它以 /budgets 结尾。对这件事,Paperclip 仓库里的 API 文档给的是一个更短的请求:直接对智能体本身发 PATCH,里面带上 budgetMonthlyCents。它的文档网站 docs.paperclip.ing 给的则是以 /budgets 结尾的路径。我们先试的是更短的那一种。在 2026.1005.0 这个版本里,它把数字存了下来,仅此而已:预算是 100,智能体花到了 120 美分,状态仍然是 idle。发到 /budgets 的请求存下了同一个数字,而且 Paperclip 真的照它执行了。我们的检查脚本 checks/two_budgets.py 对两个全新的智能体各用一种办法,并排比较,它的输出就在下面。仓库里的文档给出的 cost event 也没有 occurredAt 这个字段,而文档网站把它列为必填。在我们补上它之前,Paperclip 一直拒绝我们的请求,而且格式必须和你在 spend.py 里看到的完全一样,以 Z 结尾。

checks/two_budgets.py PRINTS

PATCH /agents/{id}: budget 100, spent 120, status idle, pause reason None
PATCH /agents/{id}/budgets: budget 100, spent 120, status paused, pause reason budget
(could not delete Check two budgets: 500; archived it instead)

11 report.py: everything is written downreport.py: ทุกอย่างถูกจดไว้report.py:每件事都记了下来

🇬🇧 English

The last file asks two questions. The first goes to the dashboard, one request that returns the state of the whole company: how many agents there are and how many are paused, how many tasks stand at each status, and what the month has cost.

The second goes to the activity log. Paperclip adds a line to it for every change, whoever made it, and never takes one away. After the steps on this page it held 50 lines. The file prints the newest ten, with the time, who acted, and what they did. Read them from the bottom up and you can see the end of step 10 happen: you raise the budget, you comment on THR-4, the agent claims the task, changes it and comments.

The times are in UTC, the clock that is the same in every country. Chiang Mai is seven hours ahead of it.

🇹🇭 ไทย

ไฟล์สุดท้ายถามสองคำถาม คำถามแรกส่งไปที่แดชบอร์ด ซึ่งเป็นคำขอครั้งเดียวที่คืนสภาพของทั้งบริษัท คือมีเอเจนต์กี่ตัวและถูกพักอยู่กี่ตัว มีงานอยู่ในแต่ละสถานะกี่งาน และเดือนนี้ใช้เงินไปเท่าไร

คำถามที่สองส่งไปที่บันทึกกิจกรรม Paperclip เพิ่มหนึ่งบรรทัดลงไปทุกครั้งที่มีการเปลี่ยนแปลง ไม่ว่าใครเป็นคนทำ และไม่เคยลบบรรทัดไหนออก หลังจากทำทุกขั้นในหน้านี้ มันมี 50 บรรทัด ไฟล์นี้พิมพ์สิบบรรทัดล่าสุด พร้อมเวลา ใครเป็นคนทำ และทำอะไร อ่านจากล่างขึ้นบน แล้วคุณจะเห็นตอนท้ายของขั้นที่ 10 เกิดขึ้นทีละจังหวะ คือคุณเพิ่มงบ คุณคอมเมนต์บน THR-4 เอเจนต์จองงาน แก้งาน แล้วคอมเมนต์

เวลาเหล่านี้เป็น UTC ซึ่งเป็นนาฬิกาที่เหมือนกันในทุกประเทศ เชียงใหม่เร็วกว่านั้นเจ็ดชั่วโมง

🇨🇳 中文

最后一个文件问两个问题。第一个问的是仪表盘,一次请求就返回整家公司的现状:有多少智能体、其中多少被暂停,每种状态下各有多少任务,这个月花了多少钱。

第二个问的是活动日志。每发生一次改动,不管是谁做的,Paperclip 都往里面加一行,而且从来不删。做完这一页的所有步骤以后,里面有 50 行。这个文件打印最新的十行,包括时间、是谁做的、做了什么。从下往上读,你能看到第 10 步的结尾是怎样一步步发生的:你调高预算,你在 THR-4 上写评论,智能体认领任务、修改任务、写下评论。

这些时间是 UTC,也就是全世界通用的那个时钟。清迈比它早七个小时。

RUN IT

venv\Scripts\python report.py

NEW FILE · report.py

"""report.py - the state of the company, and the last things that happened in it.

    python report.py
"""
import requests

API = "http://127.0.0.1:3100/api"
NAME = "Three Geckos Noodles"


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def main():
    company = find(send("GET", "/companies"), "name", NAME)
    board = send("GET", "/companies/" + company["id"] + "/dashboard")
    print("Agents:", board["agents"])
    print("Tasks:", board["tasks"])
    print("Spent this month:", board["costs"]["monthSpendCents"], "cents")

    events = send("GET", "/companies/" + company["id"] + "/activity")
    print(len(events), "things are in the activity log. The newest ten:")
    for event in events[:10]:
        clock = event["createdAt"][11:19]    # the time of day, taken out of the date
        what = event["details"].get("identifier", "")
        print("   ", clock, event["actorType"], event["action"], what)


if __name__ == "__main__":
    main()

IT PRINTS

Agents: {'active': 3, 'running': 0, 'paused': 0, 'error': 0}
Tasks: {'open': 0, 'inProgress': 0, 'blocked': 0, 'done': 4}
Spent this month: 120 cents
50 things are in the activity log. The newest ten:
    14:49:51 agent environment.lease_released 
    14:49:51 agent issue.comment_added THR-4
    14:49:51 agent issue.updated THR-4
    14:49:51 agent issue.checked_out 
    14:49:50 agent environment.lease_acquired 
    14:49:49 user issue.recovery_action_resolved THR-4
    14:49:49 user issue.comment_added THR-4
    14:49:49 user issue.updated THR-4
    14:49:48 user budget.policy_upserted 
    14:49:48 user agent.budget_updated 
YOU GET Four tasks, all done, and a record of each step that got them there. The dashboard counts three agents: Noi and the two cashiers.งานสี่งาน เสร็จทั้งหมด และมีบันทึกของทุกขั้นที่พามันมาถึงตรงนี้ แดชบอร์ดนับเอเจนต์ได้สามตัว คือน้อยกับแคชเชียร์สองตัว四个任务,全部完成,而且走到这一步的每个环节都有记录。仪表盘数出三个智能体:Noi 和两个收银员。

Tutorial 5: A team that thinksบทที่ 5: ทีมที่คิดเป็น教程 5:会思考的团队

🇬🇧 English

The cashier can add, and that is all. This tutorial hires four agents that work on one research paper, the kind a software company writes before it decides what to build. A reviewer gives it a quality score from 1 to 100. A researcher searches the web for the facts it lacks. A writer writes it again. An overseer sends the paper round those three until the score is high enough, and then stops. Three of the four have an AI model inside. The model runs on your own computer, so there is still no key and no bill.

Paperclip is what makes the four a team. The overseer does not call the reviewer as a function. It creates a task for the reviewer, Paperclip wakes the reviewer, and the score comes back as a comment on that task. Every score, every note from the web and every draft stays on a task, where you can read it afterwards.

Read this tutorial for how such a team is wired together. A small model is a weak reviewer and a careless writer, and the outputs below show both. Step 15 says plainly what the result is worth.

🇹🇭 ไทย

แคชเชียร์บวกเลขได้ และทำได้แค่นั้น บทนี้จ้างเอเจนต์สี่ตัวมาทำงานกับรายงานวิจัยฉบับเดียว แบบที่บริษัทซอฟต์แวร์เขียนก่อนตัดสินใจว่าจะสร้างอะไร ผู้รีวิวให้คะแนนคุณภาพจาก 1 ถึง 100 นักค้นคว้าค้นเว็บหาข้อเท็จจริงที่รายงานยังขาด นักเขียนเขียนรายงานใหม่ ส่วนผู้คุมงานส่งรายงานวนไปหาสามตัวนั้นจนกว่าคะแนนจะสูงพอ แล้วจึงหยุด สามในสี่ตัวมีโมเดล AI อยู่ข้างใน โมเดลรันบนคอมพิวเตอร์ของคุณเอง จึงยังคงไม่ต้องมีคีย์และไม่มีบิล

Paperclip คือสิ่งที่ทำให้ทั้งสี่ตัวเป็นทีม ผู้คุมงานไม่ได้เรียกผู้รีวิวเหมือนเรียกฟังก์ชัน มันสร้างงานให้ผู้รีวิว Paperclip ปลุกผู้รีวิว แล้วคะแนนกลับมาเป็นคอมเมนต์บนงานนั้น คะแนนทุกครั้ง โน้ตจากเว็บทุกชิ้น และร่างทุกฉบับ ยังอยู่บนงาน ให้คุณเปิดอ่านทีหลังได้

อ่านบทนี้เพื่อดูว่าทีมแบบนี้ต่อเข้าด้วยกันอย่างไร โมเดลขนาดเล็กเป็นผู้รีวิวที่อ่อนและเป็นนักเขียนที่สะเพร่า ผลลัพธ์ข้างล่างแสดงให้เห็นทั้งสองอย่าง ขั้นที่ 15 บอกตรง ๆ ว่าผลที่ได้มีค่าแค่ไหน

🇨🇳 中文

收银员会做加法,也只会这个。这个教程雇四个智能体来处理同一篇研究报告,就是软件公司在决定做什么之前写的那种。审稿人给它打一个 1 到 100 的质量分。研究员上网搜索它缺少的事实。写手把它重写一遍。监督员让报告在这三者之间转,直到分数够高才停。四个里有三个里面装着 AI 模型。模型跑在你自己的电脑上,所以仍然不用密钥,也没有账单。

把这四个变成一个团队的是 Paperclip。监督员并不是把审稿人当函数来调用。它给审稿人建一个任务,Paperclip 把审稿人叫醒,分数作为那个任务上的评论回来。每一个分数、每一条来自网上的笔记、每一稿,都留在任务上,事后你可以去读。

读这个教程,是为了看这样一个团队是怎样接起来的。小模型是个糟糕的审稿人,也是个马虎的写手,下面的输出把这两点都摆了出来。第 15 步会直说这个结果值多少。

12 A model and a web searchโมเดลหนึ่งตัว กับการค้นเว็บ一个模型,和一次网络搜索

🇬🇧 English

Two things to install. The first is Ollama, a free program that runs language models on your own computer. Get it from ollama.com. Then one command downloads the model this tutorial uses, qwen2.5:3b, which is 1.9 GB. We already had it on the laptop, so we did not time that download. Its licence, which ollama show qwen2.5:3b prints, is a research licence: fine for learning, and not for selling a service.

The second is ddgs, a Python library that asks public search engines a question and returns the first results. It needs no account and no key.

🇹🇭 ไทย

มีสองอย่างที่ต้องติดตั้ง อย่างแรกคือ Ollama โปรแกรมฟรีที่รันโมเดลภาษาบนคอมพิวเตอร์ของคุณเอง ดาวน์โหลดได้จาก ollama.com จากนั้นคำสั่งเดียวจะดาวน์โหลดโมเดลที่บทนี้ใช้ คือ qwen2.5:3b ขนาด 1.9 GB เรามีโมเดลนี้อยู่ในแล็ปท็อปแล้ว จึงไม่ได้จับเวลาการดาวน์โหลด สัญญาอนุญาตของมัน ซึ่ง ollama show qwen2.5:3b พิมพ์ออกมา เป็นสัญญาอนุญาตเพื่อการวิจัย ใช้เรียนรู้ได้ แต่ใช้ขายบริการไม่ได้

อย่างที่สองคือ ddgs ไลบรารี Python ที่ส่งคำถามไปยังเสิร์ชเอนจินสาธารณะ แล้วคืนผลลัพธ์แรก ๆ กลับมา ไม่ต้องมีบัญชีและไม่ต้องมีคีย์

🇨🇳 中文

要装两样东西。第一样是 Ollama,一个免费程序,用来在你自己的电脑上运行语言模型,从 ollama.com 下载。然后用一条命令下载这个教程用的模型 qwen2.5:3b,大小是 1.9 GB。我们的笔记本上本来就有它,所以没有给这次下载计时。它的许可证可以用 ollama show qwen2.5:3b 打印出来,是一份研究许可证:用来学习没问题,拿去卖服务不行。

第二样是 ddgs,一个 Python 库,它向公开的搜索引擎提一个问题,把最前面的几条结果拿回来。不需要账号,也不需要密钥。

TERMINAL 2 · WINDOWS · on a Mac, write venv/bin/python (not run by us) · บน Mac ให้เขียน venv/bin/python (เราไม่ได้รันเอง) · 在 Mac 上写 venv/bin/python(我们没有运行过)

ollama pull qwen2.5:3b
venv\Scripts\python -m pip install ddgs

13 paper_agent.py: four employees in one filepaper_agent.py: พนักงานสี่คนในไฟล์เดียวpaper_agent.py:一个文件里的四个员工

🇬🇧 English

The file has one class, PaperAgent, and one word decides which employee it is. Paperclip starts the reviewer with python paper_agent.py reviewer and the writer with python paper_agent.py writer. main() hands that word to the class, and the class keeps it in self.kind.

Start reading at work(), at the bottom of the class. It is the cashier's work() again: list my tasks, claim one, do the job, post the answer as a comment, mark the task done. Only the middle differs, and self.kind chooses it.

think() is the only place a model is used. It sends instructions and a text to Ollama, which listens on port 11434, and returns what the model wrote. The reviewer and the researcher ask for the answer as JSON, so that Python can take the score out as a number.

The reviewer's instructions are REVIEW, at the top of the file. They say who reads these papers, a software company that is deciding what its engineers build next, and what such a reader needs from one. The reviewer answers with one quality score from 1 to 100 and three things to fix. There are no points for parts: the score is one judgment of the whole paper. The researcher asks the model for two searches, runs them, and returns what the search engine said, each note with its address. The model does not retell the pages. The writer gets the paper, the reviewer's words and those notes. The search can fail. try and except catch that: when a search finds nothing, the researcher carries on with what it has.

The overseer has no model in it. oversee() is a loop: get a score; stop if the score has reached the target or the rounds have run out; otherwise get notes, get a new paper, and go round again. It remembers the best paper, because a rewrite can be worse than what it replaced. hand_to() is how one agent gives work to another. It creates a task for the other agent, with parentId set to the overseer's own task, waits until that task is done, and returns its newest comment. If the other agent's program crashes, Paperclip marks its task blocked, and hand_to() stops there. Each title carries its round number because of the rule you met in step 9: the same title twice gives you the same task.

A job is the paper together with everything about it: the template, the target, the notes. It travels in a task's description as plain text, each part under a line such as === paper ===. pack() writes that text and unpack() reads it. Two things Paperclip did decided this. We first sent the job as JSON, and it came back as something Python could not read: Paperclip turns the two characters \n in a text into a real line break. Then the writer never saw the notes: in a list of tasks, Paperclip cuts each description to 1,200 characters. A task asked for by itself comes whole, which is why work() fetches each task again after claiming it. The check below shows both.

🇹🇭 ไทย

ไฟล์นี้มีคลาสเดียว คือ PaperAgent และมีคำหนึ่งคำที่ตัดสินว่ามันเป็นพนักงานคนไหน Paperclip เริ่มผู้รีวิวด้วย python paper_agent.py reviewer และเริ่มนักเขียนด้วย python paper_agent.py writer main() ส่งคำนั้นให้คลาส และคลาสเก็บไว้ใน self.kind

เริ่มอ่านที่ work() ซึ่งอยู่ท้ายคลาส มันคือ work() ของแคชเชียร์อีกครั้ง คือขอรายการงานของฉัน จองงานหนึ่งงาน ทำงาน โพสต์คำตอบเป็นคอมเมนต์ แล้วทำเครื่องหมายว่างานเสร็จ ต่างกันแค่ตรงกลาง และ self.kind เป็นตัวเลือก

think() เป็นที่เดียวที่ใช้โมเดล มันส่งคำสั่งกับข้อความไปให้ Ollama ซึ่งรอฟังอยู่ที่พอร์ต 11434 แล้วคืนสิ่งที่โมเดลเขียนกลับมา ผู้รีวิวกับนักค้นคว้าขอคำตอบเป็น JSON เพื่อให้ Python ดึงคะแนนออกมาเป็นตัวเลขได้

คำสั่งของผู้รีวิวคือ REVIEW ที่อยู่ต้นไฟล์ มันบอกว่าใครเป็นผู้อ่านรายงานเหล่านี้ คือบริษัทซอฟต์แวร์ที่กำลังตัดสินใจว่าวิศวกรจะสร้างอะไรต่อไป และผู้อ่านแบบนั้นต้องการอะไรจากรายงาน ผู้รีวิวตอบด้วยคะแนนคุณภาพหนึ่งค่าจาก 1 ถึง 100 กับสิ่งที่ต้องแก้สามข้อ ไม่มีการแบ่งคะแนนเป็นส่วน ๆ: คะแนนนี้คือการตัดสินรายงานทั้งฉบับในครั้งเดียว นักค้นคว้าขอให้โมเดลคิดคำค้นสองคำ ค้นจริง แล้วคืนสิ่งที่เสิร์ชเอนจินตอบ โน้ตแต่ละข้อมีที่อยู่เว็บกำกับ โมเดลไม่ได้เล่าเนื้อหาหน้าเว็บใหม่ นักเขียนได้รับรายงาน คำพูดของผู้รีวิว และโน้ตเหล่านั้น การค้นอาจล้มเหลวได้ try กับ except ดักกรณีนั้นไว้: เมื่อการค้นไม่เจออะไร นักค้นคว้าก็ทำต่อด้วยสิ่งที่มี

ผู้คุมงานไม่มีโมเดลอยู่ข้างใน oversee() เป็นลูป คือขอคะแนน หยุดถ้าคะแนนถึงเป้าหรือครบจำนวนรอบแล้ว ถ้ายังก็ขอโน้ต ขอรายงานฉบับใหม่ แล้ววนอีกรอบ มันจำรายงานฉบับที่ดีที่สุดไว้ เพราะฉบับที่เขียนใหม่อาจแย่กว่าฉบับที่มันมาแทน hand_to() คือวิธีที่เอเจนต์ตัวหนึ่งมอบงานให้อีกตัว มันสร้างงานให้เอเจนต์อีกตัว โดยตั้ง parentId เป็นงานของผู้คุมงานเอง รอจนงานนั้นเสร็จ แล้วคืนคอมเมนต์ล่าสุดของงานนั้น ถ้าโปรแกรมของเอเจนต์อีกตัวพัง Paperclip จะทำเครื่องหมายงานนั้นเป็น blocked และ hand_to() ก็หยุดตรงนั้น ชื่อเรื่องแต่ละชื่อมีเลขรอบอยู่ด้วย เพราะกฎที่คุณเจอในขั้นที่ 9 คือชื่อเรื่องเดิมสองครั้งให้งานเดิมกลับมา

งานหนึ่งชิ้นคือรายงานพร้อมทุกอย่างที่เกี่ยวกับมัน ได้แก่ รายงานตัวอย่าง เป้าคะแนน และโน้ต มันเดินทางอยู่ในคำอธิบายของงานเป็นข้อความธรรมดา แต่ละส่วนอยู่ใต้บรรทัดอย่างเช่น === paper === pack() เขียนข้อความนั้น และ unpack() อ่านมัน สองสิ่งที่ Paperclip ทำเป็นตัวตัดสินเรื่องนี้ ตอนแรกเราส่งงานเป็น JSON แล้วมันกลับมาเป็นสิ่งที่ Python อ่านไม่ได้ เพราะ Paperclip เปลี่ยนอักขระสองตัว \n ในข้อความให้เป็นการขึ้นบรรทัดใหม่จริง ๆ ต่อมานักเขียนไม่เคยเห็นโน้ตเลย เพราะในรายการงาน Paperclip ตัดคำอธิบายแต่ละอันให้เหลือ 1,200 อักขระ งานที่ขอมาทีละงานจะมาครบ work() จึงดึงงานแต่ละงานมาอีกครั้งหลังจองแล้ว สคริปต์ตรวจสอบข้างล่างแสดงทั้งสองเรื่อง

🇨🇳 中文

这个文件只有一个类 PaperAgent,由一个词决定它是哪一个员工。Paperclip 用 python paper_agent.py reviewer 启动审稿人,用 python paper_agent.py writer 启动写手。main() 把这个词交给类,类把它存在 self.kind 里。

从类的最下面的 work() 读起。它就是收银员的 work():列出我的任务,认领一个,干活,把答案作为评论发出去,把任务标成完成。不同的只有中间那一段,由 self.kind 来选。

用到模型的地方只有 think()。它把指令和一段文字发给在 11434 端口等着的 Ollama,再把模型写的东西返回。审稿人和研究员要求答案是 JSON,这样 Python 才能把分数当成数字取出来。

审稿人的指令是文件开头的 REVIEW。它说明这些报告是给谁读的:一家正在决定工程师下一步做什么的软件公司,以及这样的读者需要报告提供什么。审稿人给出一个 1 到 100 的质量分,外加三处要改的地方。没有按部分拆开的小分:这个分数是对整篇报告的一次判断。研究员请模型想出两个搜索词,真的去搜,然后返回搜索引擎的原话,每条笔记都带着网址。模型并不复述那些网页。写手拿到的是报告、审稿人的话和这些笔记。搜索可能失败,try 和 except 接住这种情况:搜不到东西时,研究员就用手头已有的继续。

监督员里面没有模型。oversee() 是一个循环:要一个分数;分数到了目标或者轮数用完就停;否则去要笔记,要一篇新报告,再转一轮。它会记住最好的那一篇,因为重写出来的可能还不如原来的。hand_to() 是一个智能体把活交给另一个的办法:它给对方建一个任务,把 parentId 设成监督员自己的任务,等到那个任务完成,返回它最新的一条评论。如果对方的程序崩溃了,Paperclip 会把那个任务标成 blocked,hand_to() 就停在那里。每个标题都带着轮数,原因是你在第 9 步见过的规矩:同一个标题发两次,拿到的是同一个任务。

一份活就是报告加上和它有关的一切:范文、目标分数、笔记。它以纯文字的形式放在任务的描述里,每一部分都在一行像 === paper === 这样的标记下面。pack() 写出这段文字,unpack() 把它读回来。之所以这样做,是因为 Paperclip 的两个做法。一开始我们把活当作 JSON 发过去,回来的却是 Python 读不了的东西:Paperclip 把文字里 \n 这两个字符变成了真正的换行。后来写手始终看不到笔记:在任务列表里,Paperclip 把每条描述截到 1,200 个字符。单独去取一个任务,拿到的才是完整的,所以 work() 在认领之后把每个任务再取一次。下面的检查把这两件事都演示了。

NEW FILE · paper_agent.py

"""paper_agent.py - four employees in one file. The word after the file name says which.

Paperclip starts this file each time one of them has work to do:

    python paper_agent.py reviewer     gives the paper a quality score from 1 to 100
    python paper_agent.py researcher   searches the web for facts the paper lacks
    python paper_agent.py writer       writes the paper again
    python paper_agent.py overseer     sends the paper round until the score is high enough

The reviewer, the researcher and the writer think with a model on your own
computer, through Ollama. The overseer has no model in it: it is a loop.
"""
import json
import os
import sys
import time

import requests
from ddgs import DDGS
from ddgs.exceptions import DDGSException

# What the reviewer is told. The score is its judgment of the whole paper.
REVIEW = """You review research papers for a software company. The people who read
them decide what the engineers build next. Give this paper a quality score from
1 to 100. A paper earns a high score when it asks one clear question, answers it
with evidence, says where each fact comes from, says what the change would cost,
answers the strongest objection, and ends with one thing to do next."""


def pack(job):
    """A job as one text: each part under a line with its name, like "=== paper ==="."""
    text = ""
    for name, value in job.items():
        text = text + f"=== {name} ===\n{value}\n"
    return text


def unpack(text):
    """The other way round: the text of a task, back into a job."""
    job = {}
    for part in ("\n" + text).split("\n=== ")[1:]:
        name, value = part.split(" ===\n", 1)
        job[name] = value.strip()
    return job


class PaperAgent:
    def __init__(self, kind):
        self.kind = kind                     # work() reads this to choose the job
        self.api = os.environ["PAPERCLIP_API_URL"] + "/api"
        self.me = os.environ["PAPERCLIP_AGENT_ID"]
        self.company = os.environ["PAPERCLIP_COMPANY_ID"]
        # The key says who is asking. The run id ties each change to this run.
        self.headers = {"Authorization": "Bearer " + os.environ["PAPERCLIP_API_KEY"],
                        "X-Paperclip-Run-Id": os.environ["PAPERCLIP_RUN_ID"]}

    def ask(self, method, path, data=None):
        """One request to Paperclip, made as this agent. Returns Python data."""
        reply = requests.request(method, self.api + path, headers=self.headers, json=data)
        reply.raise_for_status()             # stop here if Paperclip said no
        return reply.json()

    def think(self, job, instructions, text, as_json=False):
        """One question to the model. Returns what it wrote."""
        question = {"model": job["model"], "stream": False,
                    "messages": [{"role": "system", "content": instructions},
                                 {"role": "user", "content": text}],
                    # temperature 0: the same question gets the same answer.
                    # num_ctx: room for the paper, the notes and the template together.
                    "options": {"temperature": 0, "num_ctx": 8192}}
        if as_json:
            question["format"] = "json"      # the model must answer in JSON
        reply = requests.post(job["ollama"] + "/api/chat", json=question, timeout=1800)
        reply.raise_for_status()
        return reply.json()["message"]["content"].strip()

    def review(self, job):
        """The reviewer's job. Returns "Score: 72" and, under it, what to fix."""
        instructions = REVIEW
        if job["template"]:
            instructions += ("\n\nThis paper got a high score before. "
                             "Use it as your picture of a 90:\n\n" + job["template"])
        instructions += ('\n\nAnswer in JSON with two keys. "score": a whole number '
                         'from 1 to 100. "fixes": a list of three sentences. Each one '
                         "names a thing in this paper to fix.")
        answer = json.loads(self.think(job, instructions, job["paper"], as_json=True))
        lines = ["Score: " + str(int(answer["score"]))]
        for point in answer["fixes"]:
            lines.append("- " + str(point))
        return "\n".join(lines)

    def research(self, job):
        """The researcher's job. Returns notes from the web, each with its address."""
        instructions = ("A reviewer scored this paper. Write two web searches that would "
                        "find the facts the paper lacks. Answer in JSON with one key. "
                        '"searches": a list of two short web searches.')
        text = "THE PAPER\n" + job["paper"] + "\n\nTHE REVIEWER\n" + job["review"]
        answer = json.loads(self.think(job, instructions, text, as_json=True))
        notes = []
        for search in answer["searches"][:2]:
            try:
                hits = DDGS().text(str(search), max_results=3)
            except DDGSException:            # the search found nothing, or was refused
                hits = []
            for hit in hits:
                notes.append("- " + hit["body"] + " (" + hit["href"] + ")")
        if not notes:
            notes.append("- The searches found nothing. Add no new facts.")
        return "\n".join(notes)

    def rewrite(self, job):
        """The writer's job. Returns the paper, written again."""
        instructions = ("Write this paper again so that it earns a higher score. Fix what "
                        "the reviewer asks for. Take facts only from the notes, and say in "
                        "the sentence where each fact comes from. Keep what the paper "
                        "recommends. "
                        "Answer with the paper only: its title, then its paragraphs. "
                        "Write nothing about the paper after its last paragraph.")
        if job["template"]:
            instructions += ("\n\nHere is another paper, about another subject. It got a "
                             "high score. Copy how it is built. Take no fact and no "
                             "sentence from it:\n\n" + job["template"])
        text = ("THE PAPER\n" + job["paper"] + "\n\nTHE REVIEWER\n" + job["review"]
                + "\n\nNOTES FROM THE WEB\n" + job["notes"])
        return self.think(job, instructions, text)

    def hand_to(self, name, title, parent, job):
        """Give a job to another agent, wait until it is done, and return its answer."""
        worker = None
        for agent in self.ask("GET", "/companies/" + self.company + "/agents"):
            if agent["name"] == name:
                worker = agent
        # A task with a title that is still open would come back instead of a new
        # one, so every title carries its round number.
        task = self.ask("POST", "/companies/" + self.company + "/issues",
                        {"title": title, "description": pack(job), "status": "todo",
                         "assigneeAgentId": worker["id"], "parentId": parent["id"]})
        while task["status"] != "done":
            if task["status"] == "blocked":  # Paperclip blocks a task when its run fails
                raise SystemExit(name + " could not finish " + task["identifier"])
            time.sleep(2)
            task = self.ask("GET", "/issues/" + task["id"])
        comments = self.ask("GET", "/issues/" + task["id"] + "/comments")
        return comments[0]["body"]           # the newest comment is the answer

    def oversee(self, job, task):
        """The overseer's job. Returns the best score, and the paper that got it."""
        paper = job["paper"]
        target = int(job["target"])          # every part of a job arrives as text
        rounds = int(job["rounds"])
        best_score = 0                       # lower than any score, so round 1 beats it
        for turn in range(1, rounds + 1):
            job = dict(job, paper=paper)     # a copy of the job, holding the newest paper
            report = self.hand_to("Reviewer", f"Review the paper, round {turn}", task, job)
            score = int(report.splitlines()[0].split()[1])
            self.ask("POST", "/issues/" + task["id"] + "/comments",
                     {"body": f"Round {turn}. {report}"})
            if score > best_score:           # a rewrite can be worse: keep the best one
                best_score = score
                best_paper = paper
                best_turn = turn
            if score >= target or turn == rounds:
                break                        # good enough, or out of rounds: stop
            job = dict(job, review=report)
            notes = self.hand_to("Researcher", f"Find facts, round {turn}", task, job)
            job = dict(job, notes=notes)
            paper = self.hand_to("Writer", f"Rewrite the paper, round {turn}", task, job)
        return (f"Best score: {best_score}, in round {best_turn} of {turn}. "
                f"The target was {target}.\n\n{best_paper}")

    def work(self):
        """Do every task that Paperclip has given to this agent."""
        mine = (f"/companies/{self.company}/issues"
                f"?assigneeAgentId={self.me}&status=todo,in_progress")
        for task in self.ask("GET", mine):
            task_path = "/issues/" + task["id"]
            # No ask() here: a 409 is an answer we expect, and we carry on after it.
            claim = requests.post(self.api + task_path + "/checkout", headers=self.headers,
                                  json={"agentId": self.me,
                                        "expectedStatuses": ["todo", "in_progress"]})
            if claim.status_code == 409:     # another run already has this task
                continue
            claim.raise_for_status()
            # In the list, a long description is cut short. Asked for alone, it is whole.
            task = self.ask("GET", task_path)
            job = unpack(task["description"])
            if self.kind == "reviewer":
                answer = self.review(job)
            elif self.kind == "researcher":
                answer = self.research(job)
            elif self.kind == "writer":
                answer = self.rewrite(job)
            else:
                answer = self.oversee(job, task)
            self.ask("PATCH", task_path, {"status": "done", "comment": answer})
            print(task["identifier"], self.kind, "done")


def main():
    if len(sys.argv) < 2 or "PAPERCLIP_API_KEY" not in os.environ:
        print("Paperclip starts this file. You run paper.py.")
        return
    agent = PaperAgent(sys.argv[1])
    agent.work()


if __name__ == "__main__":
    main()

checks/long_text.py PRINTS

Sent: 4,700 characters
The task asked for alone: 4,700 characters
The same task in the list of tasks: 1,200 characters
JSON sent:      '{"paper": "Line one.\\nLine two."}'
JSON came back: '{"paper": "Line one.\nLine two."}'
Python cannot read it back as JSON: Invalid control character at: line 1 column 21 (char 20)

14 paper.py: hand in a paperpaper.py: ส่งรายงานเข้าไปpaper.py:交上一篇报告

🇬🇧 English

paper.py is the owner's side. It opens a company named Research Desk if there is none, and hires the four agents. Each one is the same file started with a different word. timeoutSec lets a run last an hour, because a model takes minutes where the cashier took a second. Then it puts the job on a task for the Overseer, and prints each comment that arrives on that task until the task is done.

One thing in it is new. argparse is part of Python, and it reads the words typed after the file name. paper is the file you must name. The words that begin with two dashes are options, each with a value that is used when you leave it out. python paper.py --help lists them.

The paper is paper.txt, a short research paper for a software team. It asks whether the team should write its tests before its code. It is weak on purpose: three paragraphs, an opinion, and not one number or source.

🇹🇭 ไทย

paper.py คือฝั่งเจ้าของ มันเปิดบริษัทชื่อ Research Desk ถ้ายังไม่มี แล้วจ้างเอเจนต์ทั้งสี่ตัว แต่ละตัวคือไฟล์เดียวกันที่เริ่มด้วยคำต่างกัน timeoutSec ยอมให้การรันหนึ่งครั้งนานได้หนึ่งชั่วโมง เพราะโมเดลใช้เวลาเป็นนาที ในขณะที่แคชเชียร์ใช้หนึ่งวินาที จากนั้นมันใส่งานไว้บนงานของ Overseer แล้วพิมพ์คอมเมนต์แต่ละอันที่มาถึงงานนั้น จนกว่างานจะเสร็จ

ในไฟล์นี้มีของใหม่หนึ่งอย่าง argparse เป็นส่วนหนึ่งของ Python และมันอ่านคำที่พิมพ์ต่อท้ายชื่อไฟล์ paper คือไฟล์ที่คุณต้องระบุ คำที่ขึ้นต้นด้วยขีดสองขีดคือตัวเลือก แต่ละตัวมีค่าที่จะถูกใช้เมื่อคุณไม่ได้ใส่ python paper.py --help แสดงรายการทั้งหมด

รายงานคือ paper.txt รายงานวิจัยสั้น ๆ สำหรับทีมซอฟต์แวร์ มันถามว่าทีมควรเขียนเทสต์ก่อนเขียนโค้ดหรือไม่ เราตั้งใจให้มันอ่อน คือมีสามย่อหน้า มีความเห็น และไม่มีตัวเลขหรือแหล่งที่มาเลยสักอย่าง

🇨🇳 中文

paper.py 是老板这一边。如果还没有名叫 Research Desk 的公司,它就开一家,再雇四个智能体。每一个都是同一个文件,只是启动时带的词不同。timeoutSec 允许一次运行持续一小时,因为收银员用一秒钟的事,模型要用好几分钟。然后它把这份活放到给 Overseer 的任务上,并把到达这个任务的每一条评论打印出来,直到任务完成。

里面有一样新东西。argparse 是 Python 自带的,它读取文件名后面输入的那些词。paper 是你必须给出的文件。以两个短横开头的词是选项,每个选项都有一个你不写时使用的值。python paper.py --help 会把它们列出来。

报告是 paper.txt,一篇写给软件团队的简短研究报告。它问的是:团队该不该先写测试、再写代码。它是故意写得弱的:三段话,一个观点,没有一个数字,也没有一处出处。

NEW FILE · paper.py

"""paper.py - give a research paper to a team of four agents, and get it back with a higher score.

    python paper.py paper.txt
    python paper.py paper.txt --template model_paper.txt --target 85

The team is paper_agent.py, which must be in this folder. Ollama must be running.
"""
import argparse
import sys
import time
from pathlib import Path

import requests

API = "http://127.0.0.1:3100/api"
OLLAMA = "http://127.0.0.1:11434"
NAME = "Research Desk"
# (name, role, the word that paper_agent.py is started with)
TEAM = [("Overseer", "ceo", "overseer"), ("Reviewer", "general", "reviewer"),
        ("Researcher", "general", "researcher"), ("Writer", "general", "writer")]


def send(method, path, data=None):
    """One request to Paperclip. Returns what it answered, as Python data."""
    reply = requests.request(method, API + path, json=data)
    reply.raise_for_status()                 # stop here if Paperclip said no
    return reply.json()


def find(things, key, value):
    """The first thing in a list whose key has this value, or None."""
    for thing in things:
        if thing[key] == value:
            return thing
    return None


def pack(job):
    """A job as one text: each part under a line with its name, like "=== paper ==="."""
    text = ""
    for name, value in job.items():
        text = text + f"=== {name} ===\n{value}\n"
    return text


def options():
    """Read what was typed after "python paper.py"."""
    parser = argparse.ArgumentParser(description="Improve a research paper with four agents.")
    parser.add_argument("paper", help="the paper, a text file")
    parser.add_argument("--template", help="a paper that got a high score before")
    parser.add_argument("--target", type=int, default=85, help="stop at this score")
    parser.add_argument("--rounds", type=int, default=3, help="stop after this many reviews")
    parser.add_argument("--model", default="qwen2.5:3b", help="a model Ollama has")
    return parser.parse_args()


def text_of(name):
    """What a text file holds, or "" when no file was named."""
    if name is None:
        return ""
    return Path(name).read_text(encoding="utf-8").strip()


def team(company):
    """The four agents, by name. Each one is hired first if the company lacks it."""
    agents_path = "/companies/" + company["id"] + "/agents"
    script = Path(__file__).resolve().parent / "paper_agent.py"
    hired = {}
    for name, role, word in TEAM:
        agent = find(send("GET", agents_path), "name", name)
        if agent is None:
            # A model can take minutes, so each run may last an hour.
            start = {"command": sys.executable, "args": [str(script), word],
                     "timeoutSec": 3600}
            details = {"name": name, "role": role, "adapterType": "process",
                       "adapterConfig": start}
            if name != "Overseer":
                details["reportsTo"] = hired["Overseer"]["id"]
            agent = send("POST", agents_path, details)
            print("Hired", name + ".")
        hired[name] = agent
    return hired


def follow(task):
    """Print each comment on a task as it arrives, until the task is done or stuck."""
    shown = 0
    while True:
        task = send("GET", "/issues/" + task["id"])
        comments = send("GET", "/issues/" + task["id"] + "/comments")
        comments.reverse()                   # Paperclip lists the newest first
        for comment in comments[shown:]:
            print(comment["body"])
            print()
        shown = len(comments)
        if task["status"] in ("done", "blocked"):
            print("The task is", task["status"] + ".")
            return
        time.sleep(2)


def main():
    chosen = options()
    company = find(send("GET", "/companies"), "name", NAME)
    if company is None:
        company = send("POST", "/companies", {"name": NAME})
        print("Opened", NAME + ".")
    hired = team(company)
    job = {"paper": text_of(chosen.paper),
           "template": text_of(chosen.template), "target": chosen.target,
           "rounds": chosen.rounds, "model": chosen.model, "ollama": OLLAMA}
    started = time.time()
    # The whole job travels in the task's description, as text.
    task = send("POST", "/companies/" + company["id"] + "/issues",
                {"title": "Improve " + chosen.paper + ", " + time.strftime("%H:%M:%S"),
                 "description": pack(job), "status": "todo",
                 "assigneeAgentId": hired["Overseer"]["id"]})
    print("Created", task["identifier"] + ":", task["title"])
    print()
    follow(task)
    print(f"That took {time.time() - started:.0f} seconds.")


if __name__ == "__main__":
    main()

THE PAPER · paper.txt

Should our team write the tests first?

Our team writes the code and then writes tests later, if there is time. I think we should write the tests first. This is called test-driven development. It makes the code better and there are fewer bugs.

Writing tests first is a good idea because you think about what the code should do before you write it. Many companies do it. Our last release had a lot of bugs and customers were unhappy. Tests would have found them.

Some people say writing tests first is slow. But fixing bugs is slower. So we should write the tests first on every project.

RUN IT · PAPERCLIP AND OLLAMA MUST BOTH BE RUNNING

venv\Scripts\python paper.py paper.txt

IT PRINTS

Opened Research Desk.
Hired Overseer.
Hired Reviewer.
Hired Researcher.
Hired Writer.
Created RES-1: Improve paper.txt, 10:22:29

Round 1. Score: 45
- The paper does not clearly state the main question it is trying to answer.
- The paper does not provide evidence to support the claim that writing tests first makes the code better and finds fewer bugs.
- The paper does not address the cost of writing tests first and does not mention what the change would cost.

Round 2. Score: 45
- The paper does not address the cost of writing tests first, which is a crucial point for a decision-making process.
- The paper does not explicitly state what the change would cost, making it difficult to make a fully informed decision.
- The paper does not answer the strongest objection, which is the claim that writing tests first is slow.

Round 3. Score: 55
- The paper does not address the cost of writing tests first, which is a crucial factor in deciding whether to adopt TDD.
- The paper does not explicitly state the strongest objection to writing tests first and does not address it.
- The paper does not conclude with a clear next step, instead ending with a discussion of the benefits of TDD.

Best score: 55, in round 3 of 3. The target was 85.

Should our team write the tests first?

Our team writes the code and then writes tests later, if there is time. I think we should write the tests first. This is called test-driven development. It makes the code better and there are fewer bugs. According to the notes, Test-Driven Development (TDD) is a software development practice where test cases are written before code implementation. ISM eCompany, who specializes in web solutions, is adapting their development process to become more agile. One of those adaptations is introducing TDD into their development process, to have projects with a better code quality and a regression test suite. This aligns with the idea that TDD can optimize the development process and reduce costs. The notes also mention a case study that shows how TDD can assist companies in reducing project costs and enhancing quality software. Therefore, writing tests first is a good idea because it allows us to think about what the code should do before we write it, which can lead to fewer bugs. Additionally, the cost of writing tests first is not addressed in the notes, but the benefits of TDD in terms of code quality and cost reduction suggest that the benefits outweigh any potential costs. Our last release had a lot of bugs and customers were unhappy. Tests would have found them, supporting the idea that writing tests first can improve the quality of the code. Some people say writing tests first is slow. But fixing bugs is slower. So we should write the tests first on every project.

The notes from the web provide further insights. The 2025 guide to software development costs reveals real budgets, hidden fees, and smart tactics to keep projects on time and on budget. (https://datadrivendaily.com/software-development-costs/) Test-Driven Development (TDD) is a Software development method in which you write Automation Tests before the actual development process starts, which is coding. Here we are learning TDD in detail with these important points related to the same. (https://www.geeksforgeeks.org/software-engineering/test-driven-development/) Test-driven development improves code quality but costs velocity. See what the research says and when TDD actually pays off for your team. (https://www.arielsoftwares.com/test-driven-development/) May 29, 2023 · Learn what Test-Driven Development is, how it connects to testing, and why it can optimize your development process and reduce costs. (https://accedia.com/insights/blog/test-driven-development-key-for-reducing-development-costs) Jun 24, 2023 · Test-Driven Development (TDD) is a methodology in software development that necessitates tests to be written before to the production code. This approach can be used in any software... (https://www.researchgate.net/publication/372832171_Test_Driven_Development_and_Its_Impact_on_Program_Design_and_Software_Quality_A_Systematic_Literature_Review) Jun 8, 2026 · Test-driven development improves code quality but costs velocity. See what the research says and when TDD actually pays off for your team. (https://www.arielsoftwares.com/test-driven-development/)

The task is done.
That took 190 seconds.

🇬🇧 English

That took 190 seconds on the laptop, with the model on its processor. The scores went 45, 45, 55. The target was 85, so the overseer used all three rounds and handed back the paper from round 3, the best of the three.

Now read that paper, and not its score. It has things the first one lacked, and each came from a search: a company that is bringing in test-driven development, a case study about cost. But look at how they got there. Its second paragraph begins The notes from the web provide further insights, and it is the researcher's notes copied in, web addresses and all. One of them is a guide to software budgets, which has nothing to do with tests. And the paper says of its own subject that the cost of writing tests first is not addressed in the notes, and then that the benefits outweigh the cost anyway. The model put the search results on the page. It did not weigh them.

🇹🇭 ไทย

ครั้งนั้นใช้เวลา 190 วินาทีบนแล็ปท็อป โดยโมเดลรันบนซีพียู คะแนนเป็น 45, 45, 55 เป้าคือ 85 ผู้คุมงานจึงใช้ครบทั้งสามรอบ และส่งรายงานจากรอบที่ 3 กลับมา ซึ่งดีที่สุดในสามฉบับ

ทีนี้ให้อ่านรายงานนั้น ไม่ใช่คะแนนของมัน มันมีสิ่งที่ฉบับแรกไม่มี และแต่ละอย่างมาจากการค้น: บริษัทที่กำลังนำ test-driven development มาใช้ และกรณีศึกษาเรื่องต้นทุน แต่ดูว่าสิ่งเหล่านั้นเข้าไปอยู่ตรงนั้นได้อย่างไร ย่อหน้าที่สองขึ้นต้นว่า The notes from the web provide further insights และมันคือโน้ตของนักค้นคว้าที่ถูกคัดลอกลงไป พร้อมที่อยู่เว็บทั้งหมด หนึ่งในนั้นเป็นคู่มือเรื่องงบประมาณซอฟต์แวร์ ซึ่งไม่เกี่ยวกับเทสต์เลย และรายงานพูดถึงเรื่องของตัวเองว่า the cost of writing tests first is not addressed in the notes แล้วก็บอกต่อว่าประโยชน์มีมากกว่าต้นทุนอยู่ดี โมเดลนำผลการค้นมาวางบนหน้ากระดาษ มันไม่ได้ชั่งน้ำหนักสิ่งเหล่านั้น

🇨🇳 中文

这一次在笔记本上用了 190 秒,模型跑在处理器上。分数依次是 45、45、55。目标是 85,所以监督员用完了三轮,交回的是第 3 轮的报告,三篇里最好的一篇。

现在去读那篇报告,而不是它的分数。它有了第一稿没有的东西,每一样都来自搜索:一家正在引入 test-driven development 的公司,一个关于成本的案例研究。但看看它们是怎么进去的。第二段以 The notes from the web provide further insights 开头,而它就是研究员的笔记,连网址一起照搬了进来。其中一条是一份软件预算指南,和测试毫无关系。报告还谈到自己的主题,说 the cost of writing tests first is not addressed in the notes,接着又说反正好处大于成本。模型把搜索结果摆到了纸面上,却没有掂量它们。

YOU GET A paper that went round four agents three times with nobody typing. Eight tasks are in Paperclip: yours, and seven that the overseer made.รายงานที่วนผ่านเอเจนต์สี่ตัวสามรอบ โดยไม่มีใครพิมพ์อะไร ใน Paperclip มีงานแปดงาน: ของคุณหนึ่งงาน และอีกเจ็ดงานที่ผู้คุมงานสร้าง一篇在四个智能体之间转了三轮的报告,其间没有人敲过键盘。Paperclip 里有八个任务:你的一个,加上监督员建的七个。

15 A paper to learn from, and a bigger modelรายงานตัวอย่างให้เรียนรู้ และโมเดลที่ใหญ่ขึ้น一篇可以学的范文,和一个更大的模型

🇬🇧 English

Four options change what the team does. --template names a paper that got a high score before. The reviewer is told to treat it as its picture of a 90, and the writer is told to copy how it is built and to take nothing else from it. --target is the score at which the overseer stops, 85 if you leave it out. --rounds is the most reviews it will ask for, 3 if you leave it out. --model names another model that Ollama has.

The template is model_paper.txt, a paper about a different question: whether every change should be reviewed before it is merged. It is built the way a reader who has to decide needs it: the question, the evidence and where it comes from, the cost, the strongest objection, and one next step.

🇹🇭 ไทย

มีตัวเลือกสี่ตัวที่เปลี่ยนสิ่งที่ทีมทำ --template ระบุรายงานที่เคยได้คะแนนสูง ผู้รีวิวถูกบอกให้ถือว่ามันคือภาพของคะแนน 90 และนักเขียนถูกบอกให้เลียนแบบโครงสร้างของมัน โดยไม่เอาอย่างอื่นมา --target คือคะแนนที่ผู้คุมงานจะหยุด ถ้าไม่ใส่จะเป็น 85 --rounds คือจำนวนรีวิวมากที่สุดที่มันจะขอ ถ้าไม่ใส่จะเป็น 3 --model ระบุโมเดลตัวอื่นที่ Ollama มี

รายงานตัวอย่างคือ model_paper.txt รายงานเกี่ยวกับอีกคำถามหนึ่ง: ควรมีคนรีวิวทุกการเปลี่ยนแปลงก่อน merge หรือไม่ มันเขียนในแบบที่ผู้อ่านซึ่งต้องตัดสินใจต้องการ: คำถาม หลักฐานและที่มาของหลักฐาน ต้นทุน ข้อโต้แย้งที่หนักที่สุด และขั้นถัดไปหนึ่งอย่าง

🇨🇳 中文

有四个选项会改变团队的做法。--template 指定一篇以前得过高分的报告。审稿人被告知把它当作 90 分的样子,写手被告知只学它的结构,别的一概不取。--target 是监督员停下来的分数,不写就是 85。--rounds 是它最多要求审稿的次数,不写就是 3。--model 指定 Ollama 里的另一个模型。

范文是 model_paper.txt,一篇讨论另一个问题的报告:每一处改动在合并之前,是否都该有人审查。它的写法正是需要做决定的读者想要的:问题、证据及其出处、成本、最有力的反对意见,以及下一步要做的一件事。

THE TEMPLATE · model_paper.txt

Should every change be reviewed before it is merged?

Question. Today one of our engineers can merge a change that nobody else has read. This paper asks whether a second engineer should read every change first, and it recommends yes, for a trial of one quarter.

Evidence. In the last quarter our service had 14 incidents. The incident reports say that 9 of them began with a change only its author had seen. In the same quarter the payments team, which already reviews every change, had 2 incidents for a similar number of changes. These are our own numbers, from the incident log, and one quarter is a small sample.

Cost. A review is not free. The payments team measured it: a change waits about four hours for a reviewer, and a review takes about twenty minutes of the reviewer's day. For our team of eight that is roughly one engineer-day a week.

The strongest objection is speed: an urgent fix cannot wait four hours. The answer is a rule for that case. An urgent fix may be merged at once, and it is reviewed the next morning.

Next step. Turn on required review for the service on the first day of next quarter. At the end of the quarter, count the incidents and the hours changes waited, and decide with those two numbers.

RUN IT · THE SAME PAPER, THE SAME SMALL MODEL

venv\Scripts\python paper.py paper.txt --template model_paper.txt --target 85

IT PRINTS

Created RES-9: Improve paper.txt, 10:25:49

Round 1. Score: 55
- The paper does not provide evidence for the claim that writing tests first improves code quality and reduces bugs.
- The objection about the time it takes to write tests first is not addressed in detail and lacks supporting data.
- The recommendation to write tests first on every project is too broad and lacks specific context or evidence for all projects.

Round 2. Score: 40
- The paper does not provide evidence for the claim that writing tests first improves code quality and reduces bugs.
- The objection about the time it takes to write tests first is not addressed in detail and lacks supporting data.
- The recommendation to write tests first on every project is too broad and lacks specific context or evidence for all projects.

Round 3. Score: 40
- Provide specific evidence for the claim that writing tests first improves code quality and reduces bugs, such as case studies or data from previous projects.
- Address the objection about the time it takes to write tests first with more detailed data or examples, such as the impact on development speed and overall project timelines.
- Offer more specific context or evidence for when writing tests first would be beneficial, rather than recommending it for every project.

Best score: 55, in round 1 of 3. The target was 85.

Should our team write the tests first?

Our team writes the code and then writes tests later, if there is time. I think we should write the tests first. This is called test-driven development. It makes the code better and there are fewer bugs.

Writing tests first is a good idea because you think about what the code should do before you write it. Many companies do it. Our last release had a lot of bugs and customers were unhappy. Tests would have found them.

Some people say writing tests first is slow. But fixing bugs is slower. So we should write the tests first on every project.

The task is done.
That took 174 seconds.

🇬🇧 English

This time the scores went 55, 40, 40, in 174 seconds, and the paper that came back is the one you handed in, word for word. With a 90 to compare against, the reviewer scored both rewrites lower than the draft. The best paper was the first, so that is the one the overseer kept. This is what best_paper in oversee() is for: a team that cannot improve a paper should at least not make it worse.

The third run is the same command with a model about five times the size. qwen2.5:14b is 9.0 GB, needs that much memory, and is slow without a graphics card.

🇹🇭 ไทย

คราวนี้คะแนนเป็น 55, 40, 40 ใน 174 วินาที และรายงานที่ได้กลับมาคือฉบับที่คุณส่งเข้าไป คำต่อคำ เมื่อมีฉบับ 90 ให้เทียบ ผู้รีวิวให้คะแนนฉบับที่เขียนใหม่ทั้งสองต่ำกว่าฉบับร่าง ฉบับที่ดีที่สุดคือฉบับแรก ผู้คุมงานจึงเก็บฉบับนั้นไว้ นี่คือหน้าที่ของ best_paper ใน oversee(): ทีมที่ปรับปรุงรายงานไม่ได้ อย่างน้อยก็ไม่ควรทำให้มันแย่ลง

การรันครั้งที่สามเป็นคำสั่งเดิมกับโมเดลที่ใหญ่กว่าราวห้าเท่า qwen2.5:14b มีขนาด 9.0 GB ต้องใช้หน่วยความจำเท่านั้น และช้าเมื่อไม่มีการ์ดจอ

🇨🇳 中文

这一次分数是 55、40、40,用了 174 秒,而交回来的正是你交上去的那一篇,一字未改。有了一篇 90 分的范文作对比,审稿人给两次重写的分数都比初稿低。最好的是第一篇,所以监督员留下的就是它。oversee() 里的 best_paper 就是为此而设的:一个没法把报告改好的团队,至少不该把它改坏。

第三次运行是同一条命令,只是模型大了大约五倍。qwen2.5:14b 是 9.0 GB,需要同样多的内存,没有显卡时很慢。

THE SAME AGAIN, WITH A LARGER MODEL

venv\Scripts\python paper.py paper.txt --template model_paper.txt --model qwen2.5:14b

IT PRINTS

Created RES-17: Improve paper.txt, 10:28:43

Round 1. Score: 50
- The paper lacks a clear, specific question. It should ask, 'Does our team reduce bugs and improve customer satisfaction by writing tests before code?'
- The evidence provided is insufficient and lacks a direct link to the proposed change. Include data from teams that practice test-driven development and compare their bug rates and customer satisfaction with similar teams that do not.
- The cost of implementing test-driven development is not addressed. Include an estimate of the time and resources required to train the team and the potential impact on project timelines.

Round 2. Score: 85
- The paper should specify the exact source of the ResearchGate study and the AirWatch case study to provide clear references.
- The paper needs to quantify the cost of training and the initial time investment for TDD, providing a more concrete estimate of the financial impact.
- The paper should address the strongest objection more thoroughly, such as providing specific examples of how TDD can be integrated into the current workflow without significantly increasing development time.

Best score: 85, in round 2 of 2. The target was 85.

THE PAPER
Should our team reduce bugs and improve customer satisfaction by writing tests before code?

Our team currently writes code and then writes tests later, if there is time. This paper argues that we should adopt test-driven development (TDD), where tests are written before the code. TDD is a practice where developers write unit tests before writing the code that will pass those tests. According to a study published on ResearchGate (Apr 10, 2020), TDD improves the effectiveness of unit test cases compared to incremental test-last development in an industrial context. Additionally, AirWatch, a company that adopted TDD, achieved significant improvements in test coverage, reduced bugs in production, and increased the speed of development (Sep 8, 2024).

Writing tests first is beneficial because it forces developers to think about the desired functionality before writing the code, leading to better-designed code and fewer bugs. Our last release had a high number of bugs, which resulted in customer dissatisfaction. If we had implemented TDD, these bugs could have been caught earlier, improving customer satisfaction.

Some argue that TDD is slow and increases development time. However, studies show that while TDD may initially increase development time, it ultimately reduces the time to market and decreases the number of bugs, which can be more costly to fix later (ResearchGate, Sep 8, 2024).

The cost of implementing TDD includes training the team and the initial time investment. According to a case study by Accedia (Sep 8, 2024), while TDD can lead to higher initial costs, it ultimately reduces development costs by catching bugs early and improving code quality.

Next step. We should start a trial of TDD on one project for one quarter. At the end of the quarter, we will measure the number of bugs and customer satisfaction levels. We will also track the time spent on training and the impact on project timelines. Based on these metrics, we can decide whether to continue with TDD or revert to our current practice.

The task is done.
That took 445 seconds.

🇬🇧 English

Two rounds, 50 and then 85, in 445 seconds. This paper is built like the template: a question, evidence, the objection, the cost, and a next step that is a trial of one quarter. It still opens with the stray line THE PAPER. And its sources need care. Each name in brackets is the model's short form of a line in the researcher's notes, and the notes are a few lines that a search engine showed for each page. Nobody on this team opened the studies, and neither did we. Three slips show without opening anything. The paper says that a study of 10 April 2020 found that test-driven development improves the effectiveness of unit test cases, and the note says only that the study set out to investigate that. It dates the AirWatch story 8 September 2024, which is the date on a different note. And it credits Accedia with a case study about higher initial costs, a sentence that came from a note about a set of slides.

🇹🇭 ไทย

สองรอบ ได้ 50 แล้ว 85 ใน 445 วินาที รายงานฉบับนี้มีโครงสร้างเหมือนรายงานตัวอย่าง: คำถาม หลักฐาน ข้อโต้แย้ง ต้นทุน และขั้นถัดไปซึ่งเป็นการทดลองหนึ่งไตรมาส มันยังขึ้นต้นด้วยบรรทัดที่หลงมา THE PAPER และแหล่งที่มาของมันต้องระวัง ชื่อในวงเล็บแต่ละชื่อคือรูปย่อที่โมเดลตั้งให้กับบรรทัดหนึ่งในโน้ตของนักค้นคว้า และโน้ตก็คือไม่กี่บรรทัดที่เสิร์ชเอนจินแสดงสำหรับแต่ละหน้า ไม่มีใครในทีมนี้เปิดงานวิจัยเหล่านั้น และเราก็ไม่ได้เปิดเช่นกัน มีข้อผิดพลาดสามจุดที่เห็นได้โดยไม่ต้องเปิดอะไร รายงานบอกว่างานวิจัยวันที่ 10 เมษายน 2020 พบว่า test-driven development improves the effectiveness of unit test cases แต่โน้ตบอกเพียงว่างานวิจัยนั้นตั้งใจจะศึกษาเรื่องนี้ มันใส่วันที่ 8 กันยายน 2024 ให้เรื่องของ AirWatch ซึ่งเป็นวันที่ของโน้ตอีกข้อหนึ่ง และมันยกกรณีศึกษาเรื่องต้นทุนช่วงแรกที่สูงกว่าให้ Accedia ทั้งที่ประโยคนั้นมาจากโน้ตเกี่ยวกับสไลด์ชุดหนึ่ง

🇨🇳 中文

两轮,先是 50,然后是 85,用了 445 秒。这篇报告的结构和范文一样:问题、证据、反对意见、成本,以及下一步,也就是为期一个季度的试行。它开头仍然带着那行多余的 THE PAPER。它的出处也要小心对待。括号里的每个名字,都是模型给研究员笔记里某一行起的简称,而笔记只是搜索引擎为每个网页显示的寥寥几行。这个团队里没有谁打开过那些研究,我们也没有。有三处差错不用打开任何东西就看得出来。报告说 2020 年 4 月 10 日的一项研究发现 test-driven development improves the effectiveness of unit test cases,而笔记只说那项研究打算考察这一点。它给 AirWatch 的事例标上了 2024 年 9 月 8 日,那其实是另一条笔记上的日期。它还把一个关于前期成本更高的案例研究归到 Accedia 名下,可那句话出自一条讲一套幻灯片的笔记。

Runการรัน运行Scores, round by roundคะแนนในแต่ละรอบ各轮分数Secondsวินาที秒
qwen2.5:3b45, 45, 55190
qwen2.5:3b, model_paper.txt55, 40, 40174
qwen2.5:14b, model_paper.txt50, 85445
A SCORE IS NOT A CHECK The overseer stops when the reviewer says a number. The reviewer is a language model, and in the third run it gave 85 to a paper that turned the aim of a study into its finding and put a date on the wrong source. So read the last paper yourself. Look at where each fact came from: the researcher's notes are comments on the tasks named Find facts, and each note ends with its web address. And one run is one run. A web search finds other pages on another day, and the model writes another paper from them, so your scores will differ from ours. Before a paper like this decides what a team builds, a person has to open its sources.ผู้คุมงานหยุดเมื่อผู้รีวิวบอกตัวเลขหนึ่งตัว ผู้รีวิวคือโมเดลภาษา และในการรันครั้งที่สาม มันให้ 85 กับรายงานที่เปลี่ยนจุดมุ่งหมายของงานวิจัยให้กลายเป็นข้อค้นพบ และใส่วันที่ให้ผิดแหล่ง ดังนั้นให้อ่านรายงานฉบับสุดท้ายด้วยตัวเอง ดูว่าข้อเท็จจริงแต่ละข้อมาจากไหน: โน้ตของนักค้นคว้าเป็นคอมเมนต์บนงานที่ชื่อ Find facts และโน้ตแต่ละข้อลงท้ายด้วยที่อยู่เว็บ และการรันหนึ่งครั้งก็คือการรันหนึ่งครั้ง การค้นเว็บในวันอื่นจะเจอหน้าอื่น แล้วโมเดลก็เขียนรายงานอีกแบบจากหน้าเหล่านั้น คะแนนของคุณจึงจะต่างจากของเรา ก่อนที่รายงานแบบนี้จะเป็นตัวตัดสินว่าทีมจะสร้างอะไร ต้องมีคนเปิดอ่านแหล่งที่มาของมัน审稿人一报出数字,监督员就停下。审稿人是一个语言模型,在第三次运行里,它给了一篇报告 85 分,而那篇报告把一项研究的目的写成了结论,还把日期安到了别的出处上。所以最后那篇报告要你自己读。看看每个事实是从哪里来的:研究员的笔记是名叫 Find facts 的任务上的评论,每条笔记末尾都有网址。还有,一次运行只是一次运行。换一天,网页搜索会找到别的页面,模型也会据此写出另一篇报告,所以你的分数会和我们的不同。在这样一篇报告决定一个团队做什么之前,必须有人去打开它的出处。
YOUR TURN
  1. Write notes.py. It takes the name of a task, such as RES-3, and prints its comments. Use it on the task Find facts, round 1 of your own run. Which web address did the first note come from?
  2. In our third run the paper came back with the stray first line THE PAPER. Change rewrite() so that it removes that line when it is there.
  3. Write a short research paper of your own, about a choice your team has to make, and run paper.py on it with model_paper.txt as the template.
  1. เขียน notes.py มันรับชื่อของงาน เช่น RES-3 แล้วพิมพ์คอมเมนต์ของงานนั้น ใช้มันกับงาน Find facts, round 1 ในการรันของคุณเอง โน้ตข้อแรกมาจากที่อยู่เว็บไหน
  2. ในการรันครั้งที่สามของเรา รายงานกลับมาพร้อมบรรทัดแรกที่หลงมา THE PAPER แก้ rewrite() ให้ตัดบรรทัดนั้นออกเมื่อมันอยู่
  3. เขียนรายงานวิจัยสั้น ๆ ของคุณเอง เกี่ยวกับทางเลือกที่ทีมของคุณต้องตัดสินใจ แล้วรัน paper.py กับมัน โดยใช้ model_paper.txt เป็นรายงานตัวอย่าง
  1. 写一个 notes.py。它接收一个任务的名字,比如 RES-3,然后打印这个任务的评论。把它用在你自己那次运行的 Find facts, round 1 任务上。第一条笔记来自哪个网址?
  2. 在我们的第三次运行里,报告回来时开头多了一行 THE PAPER。修改 rewrite(),让它在这一行存在时把它去掉。
  3. 自己写一篇简短的研究报告,讲你的团队要做的一个选择,然后用 model_paper.txt 作范文,对它运行 paper.py。