ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
chat_reply.h
Go to the documentation of this file.
1#pragma once
2// A chat reply from its generated ids (the stop id cut), plain and streamed, shared by the server's
3// /chat and /v1/chat/completions (server.cpp), the WebAssembly ember_chat / ember_finish
4// (deploy/wasm/ember_wasm.cpp) and tools/chat_v2_test.cpp.
5//
6// A record with tool turns (paperkiln/2; ChatFormat::tool_call_id >= 0) splits the reply on ids, never
7// on decoded text (chat::parse_reply): when it parses with at least one call, the reply is the content
8// before the first <|tool_call|> plus the calls (ids call_0, call_1, ... per reply; name and arguments
9// verbatim); otherwise (no call, or a part that does not parse) it is the whole decoded reply, every
10// token as its text (a control or spare token's text included), as under v1. The content is stripped
11// of leading and trailing whitespace (the C isspace set) as generate() strips a v1 reply, and is valid
12// UTF-8 (utf8_valid); names and arguments are made valid UTF-8 and are otherwise untouched. Under
13// every other record the reply is v1's: the stripped decoded ids.
14//
15// Streamed (ReplyBuilder): content pieces go out as tokens are generated (utf8_text.h's ReplyStream:
16// a cut character held, trailing whitespace held) until the first <|tool_call|> id; after it nothing
17// is sent until the end, where the rest of the content goes out (the content before the call, or, when
18// the reply does not parse, the whole reply), and then the calls, each whole. Joined, the pieces equal
19// the plain reply's content byte for byte, and the calls equal its calls.
20#include <functional>
21#include <string>
22#include <utility>
23#include <vector>
24
25#include "chat_tokenizer.h"
26#include "utf8_text.h"
27
28namespace ember {
29
30using ChatDecode = std::function<std::string(const std::vector<int>&)>;
31
32// generate()'s strip (api.cpp): leading and trailing " \t\n\v\f\r".
33inline std::string strip_reply_ws(const std::string& s) {
34 const char* ws = " \t\n\v\f\r";
35 const auto a = s.find_first_not_of(ws), b = s.find_last_not_of(ws);
36 return a == std::string::npos ? std::string() : s.substr(a, b - a + 1);
37}
38
39struct ChatReply {
40 std::string content; // valid UTF-8, stripped
41 std::vector<chat::ToolCall> tool_calls; // empty: no call (always empty under a v1 record)
42};
43
44// The plain reply of ids. call_id: ChatFormat::tool_call_id (-1: no tool turns).
45inline ChatReply reply_of(const std::vector<int>& ids, int call_id, const ChatDecode& decode) {
47 if (call_id >= 0) {
49 if (p.parsed && !p.tool_calls.empty()) {
50 r.content = utf8_valid(strip_reply_ws(p.content));
51 for (auto& c : p.tool_calls) {
52 c.name = utf8_valid(c.name);
53 c.arguments = utf8_valid(c.arguments);
54 r.tool_calls.push_back(std::move(c));
55 }
56 return r;
57 }
58 }
59 r.content = utf8_valid(strip_reply_ws(decode(ids)));
60 return r;
61}
62
63// tool_calls in OpenAI's form, as requests carry them back:
64// [{"id": "call_0", "type": "function", "function": {"name", "arguments"}}, ...].
65template <class Json>
66Json tool_calls_json(const std::vector<chat::ToolCall>& calls) {
67 Json a = Json::array();
68 for (const auto& c : calls)
69 a.push_back(Json{{"id", c.id}, {"type", "function"}, {"function", Json{{"name", c.name}, {"arguments", c.arguments}}}});
70 return a;
71}
72
73// A streamed reply: next(id) for each generated id in order (stop ids excluded), returning the newly
74// final content text (often empty); finish(out) at the end returns the rest of the content and sets
75// out to the reply, equal to reply_of(the ids seen).
77public:
78 ReplyBuilder(int call_id, ChatDecode decode) : call_id_(call_id), decode_(std::move(decode)) {}
79
80 std::string next(int id) {
81 ids_.push_back(id);
82 if (call_id_ >= 0 && id == call_id_) in_call_ = true;
83 if (in_call_) return std::string();
84 return stream_.next(decode_(ids_), false);
85 }
86
87 std::string finish(ChatReply& out) {
88 out = reply_of(ids_, call_id_, decode_);
89 if (!in_call_) return stream_.next(decode_(ids_), true);
90 if (!out.tool_calls.empty()) { // parsed: the content before the first call
91 std::vector<int> before(ids_.begin(), ids_.begin() + first_call());
92 return stream_.next(decode_(before), true);
93 }
94 return stream_.next(decode_(ids_), true); // not parsed: the whole reply is content
95 }
96
97 const std::vector<int>& ids() const { return ids_; }
98
99private:
100 std::ptrdiff_t first_call() const {
101 for (size_t i = 0; i < ids_.size(); ++i)
102 if (ids_[i] == call_id_) return static_cast<std::ptrdiff_t>(i);
103 return static_cast<std::ptrdiff_t>(ids_.size());
104 }
105
108 std::vector<int> ids_;
109 bool in_call_ = false;
111};
112
113} // namespace ember
std::string finish(ChatReply &out)
Definition chat_reply.h:87
const std::vector< int > & ids() const
Definition chat_reply.h:97
ReplyBuilder(int call_id, ChatDecode decode)
Definition chat_reply.h:78
ReplyStream stream_
Definition chat_reply.h:110
std::string next(int id)
Definition chat_reply.h:80
std::ptrdiff_t first_call() const
Definition chat_reply.h:100
std::vector< int > ids_
Definition chat_reply.h:108
const float * p[kIrDepthSourcesMax]
float c
std::vector< E > a
node out
size_t j
std::string strip_reply_ws(const std::string &s)
Definition chat_reply.h:33
Json tool_calls_json(const std::vector< chat::ToolCall > &calls)
Definition chat_reply.h:66
std::function< std::string(const std::vector< int > &)> ChatDecode
Definition chat_reply.h:30
ChatReply reply_of(const std::vector< int > &ids, int call_id, const ChatDecode &decode)
Definition chat_reply.h:45
std::string utf8_valid(const std::string &s)
Definition utf8_text.h:74
ParsedReply parse_reply(const std::vector< int > &ids, int call_id, Decode decode)
std::string content
Definition chat_reply.h:40
std::vector< chat::ToolCall > tool_calls
Definition chat_reply.h:41