blob: 6cfa1111a2202dfd17a836af286532cd31252cf9 [file] [log] [blame]
Sketch🕴️305f8172026-02-27 13:58:43 +04001package loop
2
3import (
4 _ "embed"
5 "encoding/json"
6 "fmt"
7 "sync"
8
9 "dodo.cloud/neo/tools"
10
11 "github.com/anthropics/anthropic-sdk-go"
12 "github.com/invopop/jsonschema"
13)
14
15//go:embed CONVERSATION_RULES.md
16var systemPrompt string
17
18type Message struct {
19 Author string `json:"author"`
20 Contents string `json:"contents"`
21 Done bool `json:"done"`
22}
23
24type Conversation struct {
25 Messages []Message `json:"message"`
26}
27
28var todo *ToDo
29
30func Run(pr PromptReader, client *Client, tools tools.Registry) error {
31 RegisterToDoTools(tools)
32 fmt.Printf("YOU: ")
33 prompt, err := pr.Read()
34 if err != nil {
35 return err
36 }
37 todo = &ToDo{}
38 todo.ID = "1"
39 todo.Title = prompt
40 todo.AssignedTo = "assistant"
41 agents := []Agent{
42 &UserAgent{pr},
43 &AnthropicAgent{tools, client.c},
44 }
45 var wg sync.WaitGroup
46 for _, a := range agents {
47 wg.Add(1)
48 go func() {
49 if err := a.Run(todo); err != nil {
50 panic(err)
51 }
52 wg.Done()
53 }()
54 }
55 wg.Wait()
56 return nil
57}
58
59// func Loop(todo *ToDo, item *ToDo, pr PromptReader, client *Client, reg tools.Registry) error {
60// messages := []anthropic.MessageParam{
61// anthropic.NewUserMessage(anthropic.NewTextBlock(string(k))),
62// anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Work on TODO item: %s", item.ID))),
63// }
64// for {
65// fmt.Println(todo.String())
66// if item.AssignedTo == "user" {
67// fmt.Printf("YOU %s: ", item.ID)
68// prompt, err := pr.Read()
69// if err != nil {
70// return err
71// }
72// item.Discussion = append(item.Discussion, Comment{
73// Author: "user",
74// Comment: prompt,
75// })
76// item.AssignedTo = "assistant"
77// messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)))
78// } else {
79// if len(messages) == 0 {
80// messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Work on TODO item with id"))))
81// }
82// }
83
84// }
85// }
86
87func pickToDoItem(todo *ToDo) *ToDo {
88 if todo.Done {
89 return nil
90 }
91 for _, i := range todo.Items {
92 if ret := pickToDoItem(i); ret != nil {
93 return ret
94 }
95 }
96 return todo
97}
98
99func findItemByID(todo *ToDo, id string) *ToDo {
100 if todo.ID == id {
101 return todo
102 }
103 for _, i := range todo.Items {
104 if ret := findItemByID(i, id); ret != nil {
105 return ret
106 }
107 }
108 return nil
109}
110
111type ToDoItem struct {
112 ParentID string `json:"parentId"`
113 Title string `json:"title"`
114 Description string `json:"description"`
115 AssignedTo string `json:"assignedTo"`
116}
117
118type ToDoAddItemArgs struct {
119 Items []ToDoItem `json:"items"`
120}
121
122func ToDoAddItem(args ToDoAddItemArgs) (string, error) {
123 for _, td := range args.Items {
124 item := findItemByID(todo, td.ParentID)
125 if item == nil {
126 return "error", fmt.Errorf("TODO item with given id not found: %s", td.ParentID)
127 }
128 id := fmt.Sprintf("%s.%d", item.ID, len(item.Items)+1)
129 item.Items = append(item.Items, &ToDo{
130 ID: id,
131 Title: td.Title,
132 Description: td.Description,
133 AssignedTo: td.AssignedTo,
134 })
135 }
136 return "done", nil
137}
138
139type ToDoMarkItemDoneArgs struct {
140 ID string `json:"id"`
141}
142
143func ToDoMarkItemDone(args ToDoMarkItemDoneArgs) (string, error) {
144 item := findItemByID(todo, args.ID)
145 if item == nil {
146 return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
147 }
148 item.Done = true
149 return "done", nil
150}
151
152type ToDoItemAddCommentArgs struct {
153 ID string `json:"id"`
154 Comment string `json:"comment"`
155 AssignTo string `json:"assignTo"`
156}
157
158func ToDoItemAddComment(args ToDoItemAddCommentArgs) (string, error) {
159 item := findItemByID(todo, args.ID)
160 if item == nil {
161 return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
162 }
163 if len(item.Discussion) == 0 {
164 return "error", fmt.Errorf("You shall never initiate a discussion, if you want to clarify something create a TODO item for it.")
165 }
166 item.Discussion = append(item.Discussion, Comment{
167 Author: "assistant",
168 Comment: args.Comment,
169 })
170 item.AssignedTo = args.AssignTo
171 return "done", nil
172}
173
174func RegisterToDoTools(reg tools.Registry) {
175 reg.Add(tools.NewFuncTool("todo_item_add", ToDoAddItem, "Add new ToDo item."))
176 reg.Add(tools.NewFuncTool("todo_item_mark_done", ToDoMarkItemDone, "Marks ToDo item with given ID as done."))
177 reg.Add(tools.NewFuncTool("todo_item_add_comment", ToDoItemAddComment, "Adds discussion comment to given ToDo item"))
178}
179
180func GetToolSchema(schema *jsonschema.Schema) (anthropic.ToolInputSchemaParam, error) {
181 schemaBytes, err := json.Marshal(schema)
182 if err != nil {
183 return anthropic.ToolInputSchemaParam{}, err
184 }
185 var schemaMap map[string]any
186 if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil {
187 return anthropic.ToolInputSchemaParam{}, err
188 }
189
190 inputSchema, err := parseSchemaMap(schemaMap)
191 if err != nil {
192 return anthropic.ToolInputSchemaParam{}, err
193 }
194 return inputSchema, nil
195}
196
197func parseSchemaMap(s map[string]any) (anthropic.ToolInputSchemaParam, error) {
198 bytes, err := json.Marshal(s)
199 if err != nil {
200 return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to marshal schema: %w", err)
201 }
202
203 var schema anthropic.ToolInputSchemaParam
204 if err := json.Unmarshal(bytes, &schema); err != nil {
205 return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to unmarshal schema: %w", err)
206 }
207
208 return schema, nil
209}