blob: 1ae99f8a36b320e9686f95f8759e085d89490144 [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
Sketch🕴️305f8172026-02-27 13:58:43 +040018var todo *ToDo
19
20func Run(pr PromptReader, client *Client, tools tools.Registry) error {
21 RegisterToDoTools(tools)
22 fmt.Printf("YOU: ")
23 prompt, err := pr.Read()
24 if err != nil {
25 return err
26 }
27 todo = &ToDo{}
28 todo.ID = "1"
29 todo.Title = prompt
30 todo.AssignedTo = "assistant"
31 agents := []Agent{
32 &UserAgent{pr},
Sketch🕴️00202652026-02-28 21:10:00 +040033 &AnthropicAgent{pr, tools, client.c},
Sketch🕴️305f8172026-02-27 13:58:43 +040034 }
35 var wg sync.WaitGroup
36 for _, a := range agents {
37 wg.Add(1)
38 go func() {
39 if err := a.Run(todo); err != nil {
40 panic(err)
41 }
42 wg.Done()
43 }()
44 }
45 wg.Wait()
46 return nil
47}
48
Sketch🕴️305f8172026-02-27 13:58:43 +040049func pickToDoItem(todo *ToDo) *ToDo {
50 if todo.Done {
51 return nil
52 }
53 for _, i := range todo.Items {
54 if ret := pickToDoItem(i); ret != nil {
55 return ret
56 }
57 }
58 return todo
59}
60
61func findItemByID(todo *ToDo, id string) *ToDo {
62 if todo.ID == id {
63 return todo
64 }
65 for _, i := range todo.Items {
66 if ret := findItemByID(i, id); ret != nil {
67 return ret
68 }
69 }
70 return nil
71}
72
Sketch🕴️00202652026-02-28 21:10:00 +040073type ToDoSubItem struct {
74 Title string `json:"title" jsonschema:"title=title,description=high level title of the TODO item,required"`
75 Description string `json:"description" jsonschema:"title=description,description=detailed description what this TODO item is about"`
76 AssignedTo string `json:"assignedTo" jsonschema:"title=assigned to,description=name of the person who shall work on this utem"`
77}
78
Sketch🕴️305f8172026-02-27 13:58:43 +040079type ToDoItem struct {
Sketch🕴️00202652026-02-28 21:10:00 +040080 ParentID string `json:"parentId" jsonschema:"title=parent item id,description=ID of the parent TODO item this one shall be added to as a sub-item"`
81 Title string `json:"title" jsonschema:"title=title,description=high level title of the TODO item,required"`
82 Description string `json:"description" jsonschema:"title=description,description=detailed description what this TODO item is about"`
83 AssignedTo string `json:"assignedTo" jsonschema:"title=assigned to,description=name of the person who shall work on this utem"`
84 Items []ToDoSubItem `json:"items" jsonschema:"title=sub items,description:array of sub-items"`
85 Parallel bool `json:"parallel" jsonschema:"title=parallel,description=if true sub-items may be worked on in parallel and there is no depencency between them, otherwise they shall be worked on sequentially"`
Sketch🕴️305f8172026-02-27 13:58:43 +040086}
87
88type ToDoAddItemArgs struct {
Sketch🕴️00202652026-02-28 21:10:00 +040089 Items []ToDoItem `json:"items" jsonschema:"title=items,description="items to add to the TODO list,required""`
Sketch🕴️305f8172026-02-27 13:58:43 +040090}
91
92func ToDoAddItem(args ToDoAddItemArgs) (string, error) {
93 for _, td := range args.Items {
94 item := findItemByID(todo, td.ParentID)
95 if item == nil {
96 return "error", fmt.Errorf("TODO item with given id not found: %s", td.ParentID)
97 }
98 id := fmt.Sprintf("%s.%d", item.ID, len(item.Items)+1)
99 item.Items = append(item.Items, &ToDo{
100 ID: id,
101 Title: td.Title,
102 Description: td.Description,
103 AssignedTo: td.AssignedTo,
Sketch🕴️00202652026-02-28 21:10:00 +0400104 Parallel: td.Parallel,
Sketch🕴️305f8172026-02-27 13:58:43 +0400105 })
Sketch🕴️00202652026-02-28 21:10:00 +0400106 ti := item.Items[len(item.Items)-1]
107 for _, s := range td.Items {
108 sid := fmt.Sprintf("%s.%d", id, len(ti.Items)+1)
109 ti.Items = append(ti.Items, &ToDo{
110 ID: sid,
111 Title: s.Title,
112 Description: s.Description,
113 AssignedTo: s.AssignedTo,
114 })
115 }
Sketch🕴️305f8172026-02-27 13:58:43 +0400116 }
117 return "done", nil
118}
119
120type ToDoMarkItemDoneArgs struct {
Sketch🕴️00202652026-02-28 21:10:00 +0400121 ID string `json:"id" jsonschema:"title=id,description=id of the TODO item to mark as DONE,required"`
122 Summary string `json:"summary" jsonschem:"title=summary,description=detailed summary of current item and all of it's sub-item trees.,required"`
Sketch🕴️305f8172026-02-27 13:58:43 +0400123}
124
125func ToDoMarkItemDone(args ToDoMarkItemDoneArgs) (string, error) {
126 item := findItemByID(todo, args.ID)
127 if item == nil {
128 return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
129 }
130 item.Done = true
Sketch🕴️00202652026-02-28 21:10:00 +0400131 item.Summary = args.Summary
Sketch🕴️305f8172026-02-27 13:58:43 +0400132 return "done", nil
133}
134
135type ToDoItemAddCommentArgs struct {
Sketch🕴️00202652026-02-28 21:10:00 +0400136 ID string `json:"id" jsonschema:"title=id,description=id of the TODO item to add comment to,required"`
137 Comment string `json:"comment" jsonschema:"title=comment,description=actual comment text,required"`
138 AssignTo string `json:"assignTo" jsonschema:"title=assigned to,description=name of the person who shall be assigned to TODO item with given ID, if empty assignment does not chage"`
Sketch🕴️305f8172026-02-27 13:58:43 +0400139}
140
141func ToDoItemAddComment(args ToDoItemAddCommentArgs) (string, error) {
142 item := findItemByID(todo, args.ID)
143 if item == nil {
144 return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
145 }
146 if len(item.Discussion) == 0 {
147 return "error", fmt.Errorf("You shall never initiate a discussion, if you want to clarify something create a TODO item for it.")
148 }
149 item.Discussion = append(item.Discussion, Comment{
150 Author: "assistant",
151 Comment: args.Comment,
152 })
Sketch🕴️00202652026-02-28 21:10:00 +0400153 if args.AssignTo != "" {
154 item.AssignedTo = args.AssignTo
155 }
Sketch🕴️305f8172026-02-27 13:58:43 +0400156 return "done", nil
157}
158
159func RegisterToDoTools(reg tools.Registry) {
160 reg.Add(tools.NewFuncTool("todo_item_add", ToDoAddItem, "Add new ToDo item."))
161 reg.Add(tools.NewFuncTool("todo_item_mark_done", ToDoMarkItemDone, "Marks ToDo item with given ID as done."))
162 reg.Add(tools.NewFuncTool("todo_item_add_comment", ToDoItemAddComment, "Adds discussion comment to given ToDo item"))
163}
164
165func GetToolSchema(schema *jsonschema.Schema) (anthropic.ToolInputSchemaParam, error) {
166 schemaBytes, err := json.Marshal(schema)
167 if err != nil {
168 return anthropic.ToolInputSchemaParam{}, err
169 }
170 var schemaMap map[string]any
171 if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil {
172 return anthropic.ToolInputSchemaParam{}, err
173 }
174
175 inputSchema, err := parseSchemaMap(schemaMap)
176 if err != nil {
177 return anthropic.ToolInputSchemaParam{}, err
178 }
179 return inputSchema, nil
180}
181
182func parseSchemaMap(s map[string]any) (anthropic.ToolInputSchemaParam, error) {
183 bytes, err := json.Marshal(s)
184 if err != nil {
185 return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to marshal schema: %w", err)
186 }
187
188 var schema anthropic.ToolInputSchemaParam
189 if err := json.Unmarshal(bytes, &schema); err != nil {
190 return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to unmarshal schema: %w", err)
191 }
192
193 return schema, nil
194}