content.go 12 KB

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