1 # An unofficial Anthropic SDK for Bend, with typed requests, streaming, and tool calling. 2 3 import Base 4 import ./json.bend as J 5 import ./transport.bend as T 6 import ./docs.bend as Docs 7 8 def repository() -> String: 9 Docs.repository() 10 11 def version() -> String: 12 Docs.version() 13 14 type Client is Data: 15 Client{port: U32, token: String} 16 17 type Request is Data: 18 Request{model: String, max_tokens: U32, messages: J.Json, options: List<&2, J.Member<&2, J.Json>>} 19 20 type Error is Data: 21 Error{code: String, message: String, status: String, request_id: String, retry_after: String} 22 23 type Stream is Type: 24 Stream{connection: T.Connection, id: String, terminal: Bool} 25 26 type Step is Type: 27 Event{stream: Stream, kind: String, data: J.Json, request_id: String} 28 Finished{} 29 Failed{error: Error} 30 31 def text_request(model: String, input: String, max_tokens: U32) -> Request: 32 Request{model, max_tokens, J.Array{[J.Object{[J.Field{"role", J.Text{"user"}}, J.Field{"content", J.Text{input}}]}]}, Nil{}} 33 34 def params(request: Request) -> J.Json: 35 match request: 36 case Request{model, max_tokens, messages, options}: 37 J.Object{Con{J.Field{"model", J.Text{model}}, Con{J.Field{"max_tokens", J.Number{U32.show(max_tokens)}}, Con{J.Field{"messages", messages}, options}}}} 38 39 def envelope(client: Client, id: String, stream: Bool, body: J.Json, operation: String) -> J.Json: 40 match client: 41 case Client{port, token}: 42 J.Object{[J.Field{"version", J.Number{"1"}}, J.Field{"token", J.Text{token}}, J.Field{"id", J.Text{id}}, J.Field{"operation", J.Text{operation}}, J.Field{"stream", J.Boolean{stream}}, J.Field{"params", body}]} 43 44 def opened(result: Result<&1, &1, String, T.Connection>, id: String) -> Result<&1, &1, Error, Stream>: 45 match result: 46 case Fail{message}: Fail{Error{"transport_error", message, "0", "", ""}} 47 case Done{connection}: Done{Stream{connection, id, False{}}} 48 49 def open_raw(+client: Client, +id: String, stream: Bool, body: J.Json) -> IO(Result<&1, &1, Error, Stream>): 50 match client: 51 case Client{port, token}: 52 do IO>: 53 result : Result<&1, &1, String, T.Connection> <- T.connect(port, J.stringify(envelope(client, id, stream, body, "messages.create"))) 54 return opened(result, id) 55 56 def create(client: Client, id: String, request: Request) -> IO(Result<&1, &1, Error, Stream>): 57 open_raw(client, id, False{}, params(request)) 58 59 def stream(client: Client, id: String, request: Request) -> IO(Result<&1, &1, Error, Stream>): 60 open_raw(client, id, True{}, params(request)) 61 62 def close(stream: Stream) -> IO(Unit): 63 match stream: 64 case Stream{connection, id, terminal}: T.close(connection) 65 66 def field(+frame: J.Json, key: String) -> String: 67 J.text(J.get(frame, key)) 68 69 def value_or_null(value: Maybe<&2, J.Json>) -> J.Json: 70 match value: 71 case Some{v}: v 72 case None{}: J.Null{} 73 74 def json_field(frame: J.Json, key: String) -> J.Json: 75 value_or_null(J.get(frame, key)) 76 77 def fail(connection: T.Connection, code: String, message: String) -> IO(Step): 78 do IO: 79 T.close(connection) 80 return Failed{Error{code, message, "0", "", ""}} 81 82 def finish(ok: Bool, connection: T.Connection) -> IO(Step): 83 match ok: 84 case True{}: 85 do IO: 86 T.close(connection) 87 return Finished{} 88 case False{}: fail(connection, "truncated_stream", "No result or terminal response event") 89 90 def terminal_type(+name: String) -> Bool: 91 String.eq(name, "message_stop") || String.eq(name, "error") 92 93 def status(value: J.Json) -> String: 94 match value: 95 case J.Number{s}: s 96 case _: "0" 97 98 type FrameKind is Data: 99 EndFrame{} 100 ErrorFrame{} 101 ResultFrame{} 102 MetadataFrame{} 103 EventFrame{} 104 UnknownFrame{} 105 106 def select_kind(ok: Bool, yes: FrameKind, no: FrameKind) -> FrameKind: 107 match ok: 108 case True{}: yes 109 case False{}: no 110 111 def frame_kind(+kind: String) -> FrameKind: 112 select_kind(String.eq(kind, "end"), EndFrame{}, select_kind(String.eq(kind, "error"), ErrorFrame{}, 113 select_kind(String.eq(kind, "result"), ResultFrame{}, select_kind(String.eq(kind, "metadata"), MetadataFrame{}, 114 select_kind(String.eq(kind, "event"), EventFrame{}, UnknownFrame{}))))) 115 116 def step_kind(kind: FrameKind, connection: T.Connection, id: String, terminal: Bool, +frame: J.Json) -> IO(Step): 117 match kind: 118 case EndFrame{}: finish(terminal, connection) 119 case ErrorFrame{}: 120 do IO: 121 T.close(connection) 122 return Failed{Error{field(frame, "code"), field(frame, "message"), status(json_field(frame, "status")), field(frame, "request_id"), field(frame, "retry_after")}} 123 case ResultFrame{}: IO.pure(Step, Event{Stream{connection, id, True{}}, "result", json_field(frame, "data"), field(frame, "request_id")}) 124 case MetadataFrame{}: IO.pure(Step, Event{Stream{connection, id, terminal}, "metadata", frame, field(frame, "request_id")}) 125 case EventFrame{}: 126 +data = json_field(frame, "data") 127 +name = field(data, "type") 128 IO.pure(Step, Event{Stream{connection, id, terminal || terminal_type(name)}, name, data, ""}) 129 case _: fail(connection, "protocol", "Unknown bridge frame kind") 130 131 def step_id(ok: Bool, connection: T.Connection, id: String, terminal: Bool, +frame: J.Json) -> IO(Step): 132 match ok: 133 case True{}: step_kind(frame_kind(field(frame, "kind")), connection, id, terminal, frame) 134 case False{}: fail(connection, "protocol", "Mismatched bridge request id") 135 136 def received(result: T.Read, +id: String, terminal: Bool) -> IO(Step): 137 match result: 138 case T.Broken{message}: IO.pure(Step, Failed{Error{"transport_error", message, "0", "", ""}}) 139 case T.Frame{connection, +frame}: step_id(String.eq(field(frame, "id"), id), connection, id, terminal, frame) 140 141 def next(stream: Stream) -> IO(Step): 142 match stream: 143 case Stream{connection, id, terminal}: 144 do IO: 145 result : T.Read <- T.read(connection) 146 received(result, id, terminal) 147 148 def cancelled(kind: Bool, value: J.Json) -> Result<&1, &1, String, Bool>: 149 match kind value: 150 case True{} J.Boolean{found}: Done{found} 151 case _ _: Fail{"Cancellation rejected by bridge"} 152 153 def cancel_read(result: T.Read) -> IO(Result<&1, &1, String, Bool>): 154 match result: 155 case T.Broken{message}: IO.pure(Result<&1, &1, String, Bool>, Fail{message}) 156 case T.Frame{connection, +frame}: 157 do IO>: 158 T.close(connection) 159 return cancelled(String.eq(field(frame, "kind"), "cancelled"), json_field(frame, "found")) 160 161 def cancel_open(result: Result<&1, &1, String, T.Connection>) -> IO(Result<&1, &1, String, Bool>): 162 match result: 163 case Fail{e}: IO.pure(Result<&1, &1, String, Bool>, Fail{e}) 164 case Done{connection}: 165 do IO>: 166 result : T.Read <- T.read(connection) 167 cancel_read(result) 168 169 def cancel(+client: Client, id: String) -> IO(Result<&1, &1, String, Bool>): 170 match client: 171 case Client{port, token}: 172 do IO>: 173 result : Result<&1, &1, String, T.Connection> <- T.connect(port, J.stringify(envelope(client, id, False{}, J.Null{}, "cancel"))) 174 cancel_open(result) 175 176 def tool(name: String, description: String, schema: J.Json) -> J.Json: 177 J.Object{[J.Field{"name", J.Text{name}}, J.Field{"description", J.Text{description}}, J.Field{"input_schema", schema}, J.Field{"strict", J.Boolean{True{}}}]} 178 179 def tool_result(tool_use_id: String, content: String, is_error: Bool) -> J.Json: 180 J.Object{[J.Field{"type", J.Text{"tool_result"}}, J.Field{"tool_use_id", J.Text{tool_use_id}}, J.Field{"content", J.Text{content}}, J.Field{"is_error", J.Boolean{is_error}}]} 181 182 def json_schema(schema: J.Json) -> J.Json: 183 J.Object{[J.Field{"format", J.Object{[J.Field{"type", J.Text{"json_schema"}}, J.Field{"schema", schema}]}}]} 184 185 type Usage is Data: 186 Usage{input_tokens: String, output_tokens: String, cache_creation_input_tokens: String, cache_read_input_tokens: String} 187 188 type ToolUse is Data: 189 ToolUse{id: String, name: String, input: J.Json, raw: J.Json} 190 191 type Message is Data: 192 Message{id: String, stop_reason: String, text: String, calls: List<&2, ToolUse>, usage: Maybe<&2, Usage>, raw: J.Json} 193 194 def array(value: J.Json) -> List<&2, J.Json>: 195 match value: 196 case J.Array{items}: items 197 case _: Nil{} 198 199 def content_piece(is_text: Bool, item: J.Json) -> String: 200 match is_text: 201 case True{}: field(item, "text") 202 case False{}: "" 203 204 def content_text(items: List<&2, J.Json>) -> String: 205 match items: 206 case Nil{}: "" 207 case Con{+item, rest}: content_piece(String.eq(field(item, "type"), "text"), item) ++ content_text(rest) 208 209 def call_piece(is_tool: Bool, +item: J.Json, rest: List<&2, ToolUse>) -> List<&2, ToolUse>: 210 match is_tool: 211 case True{}: Con{ToolUse{field(item, "id"), field(item, "name"), json_field(item, "input"), item}, rest} 212 case False{}: rest 213 214 def tool_uses(items: List<&2, J.Json>) -> List<&2, ToolUse>: 215 match items: 216 case Nil{}: Nil{} 217 case Con{+item, rest}: call_piece(String.eq(field(item, "type"), "tool_use"), item, tool_uses(rest)) 218 219 def usage(value: J.Json) -> Maybe<&2, Usage>: 220 match value: 221 case J.Object{+fields}: Some{Usage{status(value_or_null(J.get_fields(fields, "input_tokens"))), status(value_or_null(J.get_fields(fields, "output_tokens"))), status(value_or_null(J.get_fields(fields, "cache_creation_input_tokens"))), status(value_or_null(J.get_fields(fields, "cache_read_input_tokens")))}} 222 case _: None{} 223 224 def message(+raw: J.Json) -> Message: 225 Message{field(raw, "id"), field(raw, "stop_reason"), content_text(array(json_field(raw, "content"))), tool_uses(array(json_field(raw, "content"))), usage(json_field(raw, "usage")), raw}