content.go 11 KB

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