content.go 14 KB

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