content.go 12 KB

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