content.go 14 KB

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