content.go 14 KB

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