C# 使用扣子API 实现附带文件上传的AI对话功能

ModelEngine·创作计划征文活动 10w+人浏览 1.4k人参与

需求

精力能力有限,使用 deepseek 等模型没有找到附带上传文件的 API 来实现 AI 对话,公司咨询项目组最近都在使用豆包 AI 对话,认为解答的比其它智能体都比较“精准”。为实现高效率办公业务需求,决定注册火山引擎平台来模拟实现豆包AI的调用,也没有找到文件上传的API功能。

火山引擎注册地址如下:https://console.volcengine.com/auth/login

于是与豆包对话询问是否能够提供文件上传功能的API,同样的提示词,输出了不同的,许多让人迷茫的回答:第一个回复说豆包平台API暂不支持文件上传功能,建议开发者自行解析上传文档内容并组合成提示语进行会话;第二个回复给了点儿希望,说是访问豆包开放者平台,能供文件上传功能,于是点击提供的链接,发现已无效。

再次回到火山引擎,发现在火山方舟 -> 我的应用 里有一个 coze (扣子) 平台:

跳转到 coze 平台,根据以往“经验”,先没有着急创建应用和体验,直接点击左侧菜单栏的 文档中心 -> API 和 SDK -> 文件 -> 上传文件,终于找到了实现的支持。 

文件上传实现

调用扣子 API 之前需要在 API管理  ->授权-> 个人访问令牌,创建访问令牌:

然后就可以正常调用扣子提供的 API 功能了,我们创建一个 Uploader 类,基本说明如下表:

序号成员名称成员类型类型说明
1PostUrl属性string访问的 COZE API 地址
2ApiKey属性string在COZE平台申请的访问令牌
3ErrorMessage
       
属性string错误返回信息
4ResultJson属性string正常调用返回的JOSN
5PostData属性List<PostFileItem>

PostFileItem 类表示一个上传列表项,可能包含键值或文件。项的类型枚举为:

enum PostFileItemType
    {Text = 0,File = 1}

6AddKey(string key, string value)方法void添加用于上传的一个POST键值
7AddFile(string keyname, string srcFileName, string contentType = "text/plain")方法void添加用于上传的一个文件
8coze_upload()方法string调用 COZE 上传文件API

完整示例代码如下:

    public class Uploader
    {
        public CosysJaneCommonAPI.FileEx fe = new CosysJaneCommonAPI.FileEx();
        public string PostUrl { get; set; }
        public string ApiKey { get; set; }
        public string ErrorMessage = "";
        public string ResultJson = "";

        public List<PostFileItem> PostData { get; set; }

        public Uploader()
        {
            this.PostData = new List<PostFileItem>();
        }

        public void AddKey(string key, string value)
        {
            this.PostData.Add(new PostFileItem { Name = key, Value = value });
        }

        public void AddFile(string keyname, string srcFileName, string contentType = "text/plain")
        {
            string[] srcName = Path.GetFileName(srcFileName).Split('.');
            string exName = "";
            if (srcName.Length > 1)
            {
                exName = "." + srcName[srcName.Length - 1];
            }

            ReadyFile(keyname, GetBinaryData(srcFileName), exName, contentType);
        }
        void ReadyFile(string name, byte[] fileBytes, string fileExName = "", string contentType = "text/plain")
        {
            this.PostData.Add(new PostFileItem
            {
                Type = PostFileItemType.File,
                Name = name,
                FileBytes = fileBytes,
                FileName = fileExName,
                ContentType = contentType
            });
        }

        public string coze_upload()
        {
            this.PostUrl = "https://api.coze.cn/v1/files/upload";
            var boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x");
            var request = (HttpWebRequest)WebRequest.Create(this.PostUrl);
            request.ContentType = "multipart/form-data; boundary=" + boundary;
            request.Method = "POST";
            request.KeepAlive = true;
            request.Headers.Add("Authorization:Bearer " + ApiKey + "");

            Stream memStream = new System.IO.MemoryStream();
            var boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");
            var endBoundaryBytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--");

            var formdataTemplate = "\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"{0}\";\r\n\r\n{1}";

            var formFields = this.PostData.Where(m => m.Type == PostFileItemType.Text).ToList();
            foreach (var d in formFields)
            {
                var textBytes = System.Text.Encoding.UTF8.GetBytes(string.Format(formdataTemplate, d.Name, d.Value));
                memStream.Write(textBytes, 0, textBytes.Length);
            }

            const string headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";
            var files = this.PostData.Where(m => m.Type == PostFileItemType.File).ToList();
            foreach (var fe in files)
            {
                memStream.Write(boundarybytes, 0, boundarybytes.Length);
                var header = string.Format(headerTemplate, fe.Name, fe.FileName ?? "System.Byte[]", fe.ContentType ?? "text/plain");
                var headerbytes = System.Text.Encoding.UTF8.GetBytes(header);
                memStream.Write(headerbytes, 0, headerbytes.Length);
                memStream.Write(fe.FileBytes, 0, fe.FileBytes.Length);
            }
            memStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
            request.ContentLength = memStream.Length;

            HttpWebResponse response;

            try
            {
                using (var requestStream = request.GetRequestStream())
                {
                    memStream.Position = 0;
                    var tempBuffer = new byte[memStream.Length];
                    memStream.Read(tempBuffer, 0, tempBuffer.Length);
                    memStream.Close();
                    requestStream.Write(tempBuffer, 0, tempBuffer.Length);
                }
                response = (HttpWebResponse)request.GetResponse();
            }
            catch (WebException webException)
            {
                response = (HttpWebResponse)webException.Response;
            }

            if (response == null)
            {
                ErrorMessage = "HttpWebResponse is null";
            }

            var responseStream = response.GetResponseStream();
            if (responseStream == null)
            {
               ErrorMessage = "ResponseStream is null";
            }

            using (var streamReader = new StreamReader(responseStream))
            {

                ResultJson = streamReader.ReadToEnd();
                return ResultJson;
            }
        }
        
        byte[] GetBinaryData(string filename)
		{
			if(!File.Exists(filename))
			{
				return null;
			}
			try
			{
				FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read);
				byte[] imageData = new Byte[fs.Length];
				fs.Read( imageData, 0,Convert.ToInt32(fs.Length));
				fs.Close();
				return imageData;
			}
			catch(Exception)
			{
				return null;
			}
			finally
			{
				
			}
		}		
    }

    public class PostFileItem
    {
        public PostFileItem()
        {
            this.Type = PostFileItemType.Text;
        }

        public PostFileItemType Type { get; set; }
        public string Value { get; set; }
        public byte[] FileBytes { get; set; }
        public string Name { get; set; }
        public string FileName { get; set; }
        public string ContentType { get; set; }
    }

    public enum PostFileItemType
    {
        Text = 0,
        File = 1
    }

成功调用会返回如下JSON:

{
  "code": 0,
  "data": {
    "bytes": 152236,
    "created_at": 1715847583,
    "file_name": "x.docx",
    "id": "73694"
  },
  "msg": ""
}

其中的 id 就是上传成功后存储在COZE服务器的文件id,按文档说明是有有效期的(3个月),如果做为临时使用文件据说是24个小时,总之需要按照我们实际的业务进行考量。上传多个文件则按照上述步骤以此类推,然后就可以进行对话功能的实现了。

AI对话实现

实现AI对话前,需要在 COZE 开发平台创建项目(智能体),如下:

然后编辑智能体,为其添加链接解析插件,如下图:

另外,我们需要编辑个人访问令牌的 API 列表授权功能,如下图:

coze_chat 方法提供了 AI 对话功能,基本说明如下表:

然后就可以正常调用扣子提供的 API 功能了,我们创建一个 Uploader 类,基本说明如下表:

序号参数名称参数类型说明
1user_idstring对话的自定义用户ID 字符串,比如123456
2saystring提问关键词
3BotIDstring

申请的智能体ID,要通过编辑智能体项目,通过浏览器地址的最后部分查看,比如 https://www.coze.cn/space/75/bot/664277​​​​​​​​​​​​​​
 

那么 664277即为申请的 BotID

4file_id_liststring上传成功后获取的 file_id 列表(文件类型),多个id 以逗号分隔
5img_id_liststring上传成功后获取的 file_id 列表(图片类型),多个id 以逗号分隔

完整示例代码如下:

string  ErrorMessage = "";
string  ResultJson = "";
public void coze_chat(string user_id,string say, string BotID = "",string file_id_list="",string img_id_list="")
        {
            say = say.Replace("\r", "\\r").Replace("\n","\\n");
            ApiUrl = "https://api.coze.cn/v3/chat";
            string content_type="text";
            string[] file_list = file_id_list.Split(',');
            if (file_id_list != "" || img_id_list != "")
            {
                content_type = "object_string";
            }
            WebService ws = new WebService();
            string[] headers = new string[3];
            headers[0] = "Content-Type:application/json";
            headers[1] = "Accept:application/json";
            headers[2] = "Authorization:Bearer " + ApiKey + "";

            string jsoncontent = "{";
                jsoncontent+= "\"bot_id\":\""+BotID+"\",";
                jsoncontent += "\"user_id\":\"" + user_id + "\",";
                jsoncontent += "\"stream\":false,";
                jsoncontent += "\"auto_save_history\":true,";
                jsoncontent += "\"additional_messages\":[{";
                jsoncontent += "\"role\":\"user\",";
                jsoncontent += "\"content\":\"[";
                if (content_type == "object_string")
                {
                    jsoncontent += "{\\\"type\\\":\\\"text\\\",";
                    jsoncontent += "\\\"text\\\":\\\"" + say + "\\\"},";
                    if (file_id_list != "")
                    {
                        for (int i = 0; i < file_list.GetLength(0); i++)
                        {
                            jsoncontent += "{\\\"type\\\":\\\"file\\\",";
                            jsoncontent += "\\\"file_id\\\":\\\"" + file_list[i] + "\\\"},";
                        }
                    }
                    jsoncontent = jsoncontent.Substring(0, jsoncontent.Length - 1);
                    jsoncontent += "]\",";

                    jsoncontent += "\"content_type\":\"" + content_type + "\"";
                    jsoncontent += "}]}";
                }
            string postData = jsoncontent;
            ErrorMessage = "";
            ResultJson = "";
            string rs = ws.GetResponseResult(ApiUrl, Encoding.UTF8, "POST", postData, headers);
            ErrorMessage = ws.ErrorMessage;
            ResultJson = rs;
        }

其中 WebService 类示例代码如下:

    public sealed class WebService
    {
        #region Internal Members

        public string ErrorMessage = "";
        #endregion

        /// <summary>
        /// 构造函数,提供初始化数据的功能,打开Ftp站点
        /// </summary>
        public string GetResponseResult(string url, System.Text.Encoding encoding, string method, string postData)
        {
            return GetResponseResult(url,encoding,method,postData,null);
        }
        private static bool validSecurity(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }
        public string GetResponseResult(string url, System.Text.Encoding encoding, string method, string postData,string[] headers,string ContentType= "application/x-www-form-urlencoded",bool secValid=true)
        {
            method = method.ToUpper();
            if (secValid == false)
            {
                ServicePointManager.ServerCertificateValidationCallback = validSecurity;
            }
            System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls | System.Net.SecurityProtocolType.Tls11 | System.Net.SecurityProtocolType.Tls12;

            if (method == "GET")
            {
                try
                {
                    WebRequest request2 = WebRequest.Create(@url);

                    request2.Method = method;
                    if (headers != null)
                    {
                        for (int i = 0; i < headers.GetLength(0); i++)
                        {
                            if (headers[i].Split(':').Length < 2)
                            {
                                continue;
                            }

                            if (headers[i].Split(':').Length > 1)
                            {
                                if (headers[i].Split(':')[0] == "Content-Type")
                                {
                                    request2.ContentType = headers[i].Split(':')[1];
                                    continue;
                                }
                            }
                            request2.Headers.Add(headers[i]);
                        }
                    }
                    WebResponse response2 = request2.GetResponse();
                    try
                    {
                        Stream stream = response2.GetResponseStream();
                        StreamReader reader = new StreamReader(stream, encoding);
                        string content2 = reader.ReadToEnd();
                        return content2;
                    }
                    catch (WebException webEx)
                    {
                        if (webEx.Response is HttpWebResponse errorResponse)
                        {
                            string errorBody;
                            using (Stream stream = errorResponse.GetResponseStream())
                            using (StreamReader reader = new StreamReader(stream))
                            {
                                errorBody = reader.ReadToEnd();
                            }
                            return errorBody;
                        }
                        else
                        {
                            Console.WriteLine($"WebException: {webEx.Message}");
                            return webEx.Message;
                        }
                    }

                }
                catch (Exception ex)
                {
                    ErrorMessage = ex.Message;
                    return "";
                }

            }
            if (method == "POST")
            {

                Stream outstream = null;
                Stream instream = null;
                StreamReader sr = null;
                HttpWebResponse response = null;

                HttpWebRequest request = null;
                byte[] data = encoding.GetBytes(postData);
                // 准备请求...
                try
                {
                    // 设置参数
                    request = WebRequest.Create(url) as HttpWebRequest;
                    CookieContainer cookieContainer = new CookieContainer();
                    request.CookieContainer = cookieContainer;
                    request.AllowAutoRedirect = true;
                    request.Method = method;
                    request.Timeout = 1000000;
                    
                    request.ContentType = ContentType;
                    if (headers != null)
                    {
                        for (int i = 0; i < headers.GetLength(0); i++)
                        {
                            if (headers[i].Split(':').Length < 2)
                            {
                                continue;
                            }

                            if (headers[i].Split(':').Length > 1)
                            {
                                if (headers[i].Split(':')[0] == "Host")
                                {
                                    request.Host = headers[i].Split(':')[1];
                                    continue;
                                }
                                else if (headers[i].Split(':')[0] == "Content-Type")
                                {
                                    request.ContentType = headers[i].Split(':')[1];
                                    continue;
                                }
                                else if (headers[i].Split(':')[0] == "Connection")
                                {
                                    request.KeepAlive = headers[i].Split(':')[1] == "close" ? false : true;
                                    continue;
                                }
                                else if (headers[i].Split(':')[0] == "Accept")
                                {
                                    request.Accept = headers[i].Split(':')[1];
                                    continue;
                                }

                            }
                            request.Headers.Add(headers[i]);
                        }
                    }
                    request.ContentLength = data.Length;
                    try
                    {
                        outstream = request.GetRequestStream();
                        outstream.Write(data, 0, data.Length);
                        outstream.Close();
                        //发送请求并获取相应回应数据
                        response = request.GetResponse() as HttpWebResponse;
                        //直到request.GetResponse()程序才开始向目标网页发送Post请求
                        instream = response.GetResponseStream();
                        sr = new StreamReader(instream, encoding);
                        //返回结果网页(html)代码
                        string content = sr.ReadToEnd();
                        sr.Close();
                        sr.Dispose();

                        return content;
                    }
                    catch (WebException webEx)
                    {
                        if (webEx.Response is HttpWebResponse errorResponse)
                        {
                            string errorBody;
                            using (Stream stream = errorResponse.GetResponseStream())
                            using (StreamReader reader = new StreamReader(stream))
                            {
                                errorBody = reader.ReadToEnd();
                            }
                            return errorBody;
                        }
                        else
                        {
                            Console.WriteLine($"WebException: {webEx.Message}");
                            return webEx.Message;
                        }
                    }
                }
                catch (Exception ex)
                {
                    ErrorMessage = ex.Message;
                    return "";
                }
            }
            ErrorMessage = "不正确的方法类型。(目前仅支持GET/POST)";
            return "";

        }//get response result

一些基本说明可参考我的文章:https://blog.youkuaiyun.com/michaelline/article/details/139123272?spm=1011.2415.3001.5331

非流式响应

非流式响应调用对话API 流程如下图:

调用成功,会返回类似如下JSON:

{
// 在 chat 事件里,data 字段中的 id 为 Chat ID,即会话 ID。
    "id": "737662",
    "conversation_id": "737554",
    "bot_id": "73666",
    "completed_at": 1717508113,
    "last_error": {
        "code": 0,
        "msg": ""
    },
    "status": "completed",
    "usage": {
        "token_count": 6644,
        "output_count": 766,
        "input_count": 5878
    }
}

在对话事件里,data 字段中的 id 为 Chat ID,即对话 ID,conversation_id 为会话id,这是轮询获取对话状态中需要提供的两个重要ID 参数,轮询方法 get_coze_chat_status 的说明如下表:

序号参数名称参数类型说明
1parasstring

查询参数,请进行如下字符串样例拼接即可:

 conversation_id=737554&chat_id=737662

2ApiKeystring申请的个人访问令牌

完整示例代码如下:

string  ErrorMessage = "";
string  ResultJson = "";
public void get_coze_chat_status(string paras,string ApiKey)
{
            ApiUrl = "https://api.coze.cn/v3/chat/retrieve?" + paras;
            WebService ws = new WebService();
            string[] headers = new string[2];
            headers[0] = "Authorization:Bearer " + ApiKey + "";
            headers[1] = "Content-Type:application/json";

            ErrorMessage = "";
            ResultJson = "";
            string rs = GetResponseResult(ApiUrl, Encoding.UTF8, "GET", "", headers);
            ErrorMessage = ws.ErrorMessage;
            ResultJson = rs;
}

当 "status" 字段值为 "completed" 的时候,表示对话处理完毕。

查询对话详情

通过扣子查看对话消息详情 API,可获取对话的最终结果,获取方法 get_coze_chat_detail 的说明如下表:

序号参数名称参数类型说明
1parasstring

查询参数,请进行如下字符串样例拼接即可:

 conversation_id=737554&chat_id=737662

2ApiKeystring申请的个人访问令牌

完整示例代码如下:

string   ErrorMessage = "";
string   ResultJson = "";
public void get_coze_chat_detail(string paras,string ApiKey)
        {
            ApiUrl = "https://api.coze.cn/v3/chat/message/list?" + paras;
            WebService ws = new WebService();
            string[] headers = new string[2];
            headers[0] = "Authorization:Bearer " + ApiKey + "";
            headers[1] = "Content-Type:application/json";


            ErrorMessage = "";
            ResultJson = "";
            string rs = ws.GetResponseResult(ApiUrl, Encoding.UTF8, "GET", "", headers);
            ErrorMessage = ws.ErrorMessage;
            ResultJson = rs;

        }

正常返回的 JSON 比较 “庞大” ,以下是获得关键回答部分的示例代码:

string _answer="";
Newtonsoft.Json.Linq.JObject rs2 = Newtonsoft.Json.Linq.JObject.Parse(ds.ResultJson);
for(int i=0;i<rs2["data"].Count();i++){
    if (rs2["data"][i]["type"].ToString().ToLower() == "answer")
    {
         _answer= rs2["data"][i]["content"].ToString();
         return _answer;
    }
}

JSON对象 data 数组元素,type属性为 answer 的元素,其 content 值即为回答的内容。

小结

以上为作者初探 AI 对话的一些分享,希望与您一起探讨、交流,欢迎批评指正。

API的一些相关文档可以参考以下链接:

文档中心入口:

https://www.coze.cn/open/docs/guides

上传文件:

https://www.coze.cn/open/docs/developer_guides/upload_files

发起对话:

https://www.coze.cn/open/docs/developer_guides/chat_v3

轮询对话详情:

https://www.coze.cn/open/docs/developer_guides/retrieve_chat

查看对话消息详情:

https://www.coze.cn/open/docs/developer_guides/list_chat_messages

评论 85
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

初九之潜龙勿用

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值