content.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. package message
  2. import (
  3. "encoding/base64"
  4. "errors"
  5. "slices"
  6. "strings"
  7. "time"
  8. "charm.land/fantasy"
  9. "charm.land/fantasy/providers/anthropic"
  10. "charm.land/fantasy/providers/google"
  11. "charm.land/fantasy/providers/openai"
  12. "github.com/charmbracelet/catwalk/pkg/catwalk"
  13. )
  14. type MessageRole string
  15. const (
  16. Assistant MessageRole = "assistant"
  17. User MessageRole = "user"
  18. System MessageRole = "system"
  19. Tool MessageRole = "tool"
  20. )
  21. type FinishReason string
  22. const (
  23. FinishReasonEndTurn FinishReason = "end_turn"
  24. FinishReasonMaxTokens FinishReason = "max_tokens"
  25. FinishReasonToolUse FinishReason = "tool_use"
  26. FinishReasonCanceled FinishReason = "canceled"
  27. FinishReasonError FinishReason = "error"
  28. FinishReasonPermissionDenied FinishReason = "permission_denied"
  29. // Should never happen
  30. FinishReasonUnknown FinishReason = "unknown"
  31. )
  32. type ContentPart interface {
  33. isPart()
  34. }
  35. type ReasoningContent struct {
  36. Thinking string `json:"thinking"`
  37. Signature string `json:"signature"`
  38. ThoughtSignature string `json:"thought_signature"` // Used for google
  39. ResponsesData *openai.ResponsesReasoningMetadata `json:"responses_data"`
  40. StartedAt int64 `json:"started_at,omitempty"`
  41. FinishedAt int64 `json:"finished_at,omitempty"`
  42. }
  43. func (tc ReasoningContent) String() string {
  44. return tc.Thinking
  45. }
  46. func (ReasoningContent) isPart() {}
  47. type TextContent struct {
  48. Text string `json:"text"`
  49. }
  50. func (tc TextContent) String() string {
  51. return tc.Text
  52. }
  53. func (TextContent) isPart() {}
  54. type ImageURLContent struct {
  55. URL string `json:"url"`
  56. Detail string `json:"detail,omitempty"`
  57. }
  58. func (iuc ImageURLContent) String() string {
  59. return iuc.URL
  60. }
  61. func (ImageURLContent) isPart() {}
  62. type BinaryContent struct {
  63. Path string
  64. MIMEType string
  65. Data []byte
  66. }
  67. func (bc BinaryContent) String(p catwalk.InferenceProvider) string {
  68. base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
  69. if p == catwalk.InferenceProviderOpenAI {
  70. return "data:" + bc.MIMEType + ";base64," + base64Encoded
  71. }
  72. return base64Encoded
  73. }
  74. func (BinaryContent) isPart() {}
  75. type ToolCall struct {
  76. ID string `json:"id"`
  77. Name string `json:"name"`
  78. Input string `json:"input"`
  79. ProviderExecuted bool `json:"provider_executed"`
  80. Finished bool `json:"finished"`
  81. }
  82. func (ToolCall) isPart() {}
  83. type ToolResult struct {
  84. ToolCallID string `json:"tool_call_id"`
  85. Name string `json:"name"`
  86. Content string `json:"content"`
  87. Data string `json:"data"`
  88. MIMEType string `json:"mime_type"`
  89. Metadata string `json:"metadata"`
  90. IsError bool `json:"is_error"`
  91. }
  92. func (ToolResult) isPart() {}
  93. type Finish struct {
  94. Reason FinishReason `json:"reason"`
  95. Time int64 `json:"time"`
  96. Message string `json:"message,omitempty"`
  97. Details string `json:"details,omitempty"`
  98. }
  99. func (Finish) isPart() {}
  100. type Message struct {
  101. ID string
  102. Role MessageRole
  103. SessionID string
  104. Parts []ContentPart
  105. Model string
  106. Provider string
  107. CreatedAt int64
  108. UpdatedAt int64
  109. IsSummaryMessage bool
  110. HookOutputs []HookOutput
  111. }
  112. type HookOutput struct {
  113. Stop bool `json:"stop,omitempty" description:"set to true if the execution should stop"`
  114. EventType string `json:"event_type" description:"ignore"`
  115. Error string `json:"error,omitempty"`
  116. Message string `json:"message,omitempty" description:"a message to send to show the user"`
  117. Decision string `json:"decision" description:"block, allow, deny, ask, only set if the request asks you to do so"`
  118. UpdatedInput string `json:"updated_input" description:"the updated tool input json, only set if the user requests you update a tool input"`
  119. AdditionalContext string `json:"additional_context" description:"additional context to send to the LLM, only set if the user asks to add additional context"`
  120. }
  121. func (m *Message) Content() TextContent {
  122. for _, part := range m.Parts {
  123. if c, ok := part.(TextContent); ok {
  124. return c
  125. }
  126. }
  127. return TextContent{}
  128. }
  129. func (m *Message) ContentWithHooksContext() string {
  130. text := strings.TrimSpace(m.Content().Text)
  131. var additionalContext []string
  132. for _, hookOutput := range m.HookOutputs {
  133. context := strings.TrimSpace(hookOutput.AdditionalContext)
  134. if context != "" {
  135. additionalContext = append(additionalContext, context)
  136. }
  137. }
  138. if len(additionalContext) > 0 {
  139. text += "## Additional Context\n"
  140. text += strings.Join(additionalContext, "\n")
  141. }
  142. return text
  143. }
  144. func (m *Message) ReasoningContent() ReasoningContent {
  145. for _, part := range m.Parts {
  146. if c, ok := part.(ReasoningContent); ok {
  147. return c
  148. }
  149. }
  150. return ReasoningContent{}
  151. }
  152. func (m *Message) ImageURLContent() []ImageURLContent {
  153. imageURLContents := make([]ImageURLContent, 0)
  154. for _, part := range m.Parts {
  155. if c, ok := part.(ImageURLContent); ok {
  156. imageURLContents = append(imageURLContents, c)
  157. }
  158. }
  159. return imageURLContents
  160. }
  161. func (m *Message) BinaryContent() []BinaryContent {
  162. binaryContents := make([]BinaryContent, 0)
  163. for _, part := range m.Parts {
  164. if c, ok := part.(BinaryContent); ok {
  165. binaryContents = append(binaryContents, c)
  166. }
  167. }
  168. return binaryContents
  169. }
  170. func (m *Message) ToolCalls() []ToolCall {
  171. toolCalls := make([]ToolCall, 0)
  172. for _, part := range m.Parts {
  173. if c, ok := part.(ToolCall); ok {
  174. toolCalls = append(toolCalls, c)
  175. }
  176. }
  177. return toolCalls
  178. }
  179. func (m *Message) ToolResults() []ToolResult {
  180. toolResults := make([]ToolResult, 0)
  181. for _, part := range m.Parts {
  182. if c, ok := part.(ToolResult); ok {
  183. toolResults = append(toolResults, c)
  184. }
  185. }
  186. return toolResults
  187. }
  188. func (m *Message) IsFinished() bool {
  189. for _, part := range m.Parts {
  190. if _, ok := part.(Finish); ok {
  191. return true
  192. }
  193. }
  194. return false
  195. }
  196. // AddHookOutputs appends multiple hook outputs to the message's hook outputs.
  197. func (m *Message) AddHookOutputs(outputs ...HookOutput) {
  198. m.HookOutputs = append(m.HookOutputs, outputs...)
  199. }
  200. func (m *Message) FinishPart() *Finish {
  201. for _, part := range m.Parts {
  202. if c, ok := part.(Finish); ok {
  203. return &c
  204. }
  205. }
  206. return nil
  207. }
  208. func (m *Message) FinishReason() FinishReason {
  209. for _, part := range m.Parts {
  210. if c, ok := part.(Finish); ok {
  211. return c.Reason
  212. }
  213. }
  214. return ""
  215. }
  216. func (m *Message) IsThinking() bool {
  217. if m.ReasoningContent().Thinking != "" && m.Content().Text == "" && !m.IsFinished() {
  218. return true
  219. }
  220. return false
  221. }
  222. func (m *Message) AppendContent(delta string) {
  223. found := false
  224. for i, part := range m.Parts {
  225. if c, ok := part.(TextContent); ok {
  226. m.Parts[i] = TextContent{Text: c.Text + delta}
  227. found = true
  228. }
  229. }
  230. if !found {
  231. m.Parts = append(m.Parts, TextContent{Text: delta})
  232. }
  233. }
  234. func (m *Message) AppendReasoningContent(delta string) {
  235. found := false
  236. for i, part := range m.Parts {
  237. if c, ok := part.(ReasoningContent); ok {
  238. m.Parts[i] = ReasoningContent{
  239. Thinking: c.Thinking + delta,
  240. Signature: c.Signature,
  241. StartedAt: c.StartedAt,
  242. FinishedAt: c.FinishedAt,
  243. }
  244. found = true
  245. }
  246. }
  247. if !found {
  248. m.Parts = append(m.Parts, ReasoningContent{
  249. Thinking: delta,
  250. StartedAt: time.Now().Unix(),
  251. })
  252. }
  253. }
  254. func (m *Message) AppendThoughtSignature(signature string) {
  255. for i, part := range m.Parts {
  256. if c, ok := part.(ReasoningContent); ok {
  257. m.Parts[i] = ReasoningContent{
  258. Thinking: c.Thinking,
  259. ThoughtSignature: c.ThoughtSignature + signature,
  260. Signature: c.Signature,
  261. StartedAt: c.StartedAt,
  262. FinishedAt: c.FinishedAt,
  263. }
  264. return
  265. }
  266. }
  267. m.Parts = append(m.Parts, ReasoningContent{ThoughtSignature: signature})
  268. }
  269. func (m *Message) AppendReasoningSignature(signature string) {
  270. for i, part := range m.Parts {
  271. if c, ok := part.(ReasoningContent); ok {
  272. m.Parts[i] = ReasoningContent{
  273. Thinking: c.Thinking,
  274. Signature: c.Signature + signature,
  275. StartedAt: c.StartedAt,
  276. FinishedAt: c.FinishedAt,
  277. }
  278. return
  279. }
  280. }
  281. m.Parts = append(m.Parts, ReasoningContent{Signature: signature})
  282. }
  283. func (m *Message) SetReasoningResponsesData(data *openai.ResponsesReasoningMetadata) {
  284. for i, part := range m.Parts {
  285. if c, ok := part.(ReasoningContent); ok {
  286. m.Parts[i] = ReasoningContent{
  287. Thinking: c.Thinking,
  288. ResponsesData: data,
  289. StartedAt: c.StartedAt,
  290. FinishedAt: c.FinishedAt,
  291. }
  292. return
  293. }
  294. }
  295. }
  296. func (m *Message) FinishThinking() {
  297. for i, part := range m.Parts {
  298. if c, ok := part.(ReasoningContent); ok {
  299. if c.FinishedAt == 0 {
  300. m.Parts[i] = ReasoningContent{
  301. Thinking: c.Thinking,
  302. Signature: c.Signature,
  303. StartedAt: c.StartedAt,
  304. FinishedAt: time.Now().Unix(),
  305. }
  306. }
  307. return
  308. }
  309. }
  310. }
  311. func (m *Message) ThinkingDuration() time.Duration {
  312. reasoning := m.ReasoningContent()
  313. if reasoning.StartedAt == 0 {
  314. return 0
  315. }
  316. endTime := reasoning.FinishedAt
  317. if endTime == 0 {
  318. endTime = time.Now().Unix()
  319. }
  320. return time.Duration(endTime-reasoning.StartedAt) * time.Second
  321. }
  322. func (m *Message) FinishToolCall(toolCallID string) {
  323. for i, part := range m.Parts {
  324. if c, ok := part.(ToolCall); ok {
  325. if c.ID == toolCallID {
  326. m.Parts[i] = ToolCall{
  327. ID: c.ID,
  328. Name: c.Name,
  329. Input: c.Input,
  330. Finished: true,
  331. }
  332. return
  333. }
  334. }
  335. }
  336. }
  337. func (m *Message) AppendToolCallInput(toolCallID string, inputDelta string) {
  338. for i, part := range m.Parts {
  339. if c, ok := part.(ToolCall); ok {
  340. if c.ID == toolCallID {
  341. m.Parts[i] = ToolCall{
  342. ID: c.ID,
  343. Name: c.Name,
  344. Input: c.Input + inputDelta,
  345. Finished: c.Finished,
  346. }
  347. return
  348. }
  349. }
  350. }
  351. }
  352. func (m *Message) AddToolCall(tc ToolCall) {
  353. for i, part := range m.Parts {
  354. if c, ok := part.(ToolCall); ok {
  355. if c.ID == tc.ID {
  356. m.Parts[i] = tc
  357. return
  358. }
  359. }
  360. }
  361. m.Parts = append(m.Parts, tc)
  362. }
  363. func (m *Message) SetToolCalls(tc []ToolCall) {
  364. // remove any existing tool call part it could have multiple
  365. parts := make([]ContentPart, 0)
  366. for _, part := range m.Parts {
  367. if _, ok := part.(ToolCall); ok {
  368. continue
  369. }
  370. parts = append(parts, part)
  371. }
  372. m.Parts = parts
  373. for _, toolCall := range tc {
  374. m.Parts = append(m.Parts, toolCall)
  375. }
  376. }
  377. func (m *Message) AddToolResult(tr ToolResult) {
  378. m.Parts = append(m.Parts, tr)
  379. }
  380. func (m *Message) SetToolResults(tr []ToolResult) {
  381. for _, toolResult := range tr {
  382. m.Parts = append(m.Parts, toolResult)
  383. }
  384. }
  385. func (m *Message) AddFinish(reason FinishReason, message, details string) {
  386. // remove any existing finish part
  387. for i, part := range m.Parts {
  388. if _, ok := part.(Finish); ok {
  389. m.Parts = slices.Delete(m.Parts, i, i+1)
  390. break
  391. }
  392. }
  393. m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix(), Message: message, Details: details})
  394. }
  395. func (m *Message) AddImageURL(url, detail string) {
  396. m.Parts = append(m.Parts, ImageURLContent{URL: url, Detail: detail})
  397. }
  398. func (m *Message) AddBinary(mimeType string, data []byte) {
  399. m.Parts = append(m.Parts, BinaryContent{MIMEType: mimeType, Data: data})
  400. }
  401. func (m *Message) ToAIMessage() []fantasy.Message {
  402. var messages []fantasy.Message
  403. switch m.Role {
  404. case User:
  405. var parts []fantasy.MessagePart
  406. text := strings.TrimSpace(m.ContentWithHooksContext())
  407. if text != "" {
  408. parts = append(parts, fantasy.TextPart{Text: text})
  409. }
  410. for _, content := range m.BinaryContent() {
  411. parts = append(parts, fantasy.FilePart{
  412. Filename: content.Path,
  413. Data: content.Data,
  414. MediaType: content.MIMEType,
  415. })
  416. }
  417. messages = append(messages, fantasy.Message{
  418. Role: fantasy.MessageRoleUser,
  419. Content: parts,
  420. })
  421. case Assistant:
  422. var parts []fantasy.MessagePart
  423. text := strings.TrimSpace(m.Content().Text)
  424. if text != "" {
  425. parts = append(parts, fantasy.TextPart{Text: text})
  426. }
  427. reasoning := m.ReasoningContent()
  428. if reasoning.Thinking != "" {
  429. reasoningPart := fantasy.ReasoningPart{Text: reasoning.Thinking, ProviderOptions: fantasy.ProviderOptions{}}
  430. if reasoning.Signature != "" {
  431. reasoningPart.ProviderOptions[anthropic.Name] = &anthropic.ReasoningOptionMetadata{
  432. Signature: reasoning.Signature,
  433. }
  434. }
  435. if reasoning.ResponsesData != nil {
  436. reasoningPart.ProviderOptions[openai.Name] = reasoning.ResponsesData
  437. }
  438. if reasoning.ThoughtSignature != "" {
  439. reasoningPart.ProviderOptions[google.Name] = &google.ReasoningMetadata{
  440. Signature: reasoning.ThoughtSignature,
  441. }
  442. }
  443. parts = append(parts, reasoningPart)
  444. }
  445. for _, call := range m.ToolCalls() {
  446. parts = append(parts, fantasy.ToolCallPart{
  447. ToolCallID: call.ID,
  448. ToolName: call.Name,
  449. Input: call.Input,
  450. ProviderExecuted: call.ProviderExecuted,
  451. })
  452. }
  453. messages = append(messages, fantasy.Message{
  454. Role: fantasy.MessageRoleAssistant,
  455. Content: parts,
  456. })
  457. case Tool:
  458. var parts []fantasy.MessagePart
  459. for _, result := range m.ToolResults() {
  460. var content fantasy.ToolResultOutputContent
  461. if result.IsError {
  462. content = fantasy.ToolResultOutputContentError{
  463. Error: errors.New(result.Content),
  464. }
  465. } else if result.Data != "" {
  466. content = fantasy.ToolResultOutputContentMedia{
  467. Data: result.Data,
  468. MediaType: result.MIMEType,
  469. }
  470. } else {
  471. content = fantasy.ToolResultOutputContentText{
  472. Text: result.Content,
  473. }
  474. }
  475. parts = append(parts, fantasy.ToolResultPart{
  476. ToolCallID: result.ToolCallID,
  477. Output: content,
  478. })
  479. }
  480. messages = append(messages, fantasy.Message{
  481. Role: fantasy.MessageRoleTool,
  482. Content: parts,
  483. })
  484. }
  485. return messages
  486. }