AI间对话APK制成

本文记录了如何使用API-KEY来实现“AI间相互对话”的功能。
笔者听闻优秀的从创作者而言,当角色设定丰满时,只需要思考场景,角色就会自己“动起来”。基于这样的想法,做出了一个AI与AI间对话的应用。技术上没有什么太大含量,更多的是作为实验性质的AI应用的产物。
设计如下:

一、类设计

Character类

角色类。每个角色维护自己的对话历史记录,并支持与其他角色类交互。
类属性:
name:角色名称。
prompt:角色的第一个提示词(以system角色输入的Prompt)。
initial:角色的初始发言(可选,同一个对话中只能又1个“初始发言”的角色)。
history:对话历史记录,格式为字典列表,包含以下字段:
role:发言者角色(system/user(其他Character扮演)/assistant)。
content:发言内容。
current_seq:发言的序列编号,用于后续手动修改内容或截取。
last_output:角色最近一次的输出内容,用这个作为其他Character类的输入。
used_tokens:累计使用的token数量(用于成本计算)。
类方法:
build_ipt_history:将history中的内容转化为OpenAI包可识别的格式。
chat_to:与其他角色对话,接收输入并生成回复,支持搜索、流式输出等参数;并更新自身历史与used_tokens。

class Character:
    def __init__(self,name:str,prompt:str,initial:str=""):
        self.prompt = prompt
        self.name = name
        self.initial = initial
        self.history = [{
            "role":"system",
            "content":self.prompt,
            "current_seq":-1
        }]
        self.current_seq = -1
        if len(initial)>0:
            self.current_seq += 1
            self.history.append({"role":"assistant","content":initial,"current_seq":self.current_seq})
        self.last_output = initial
        self.used_tokens = 0

    def build_ipt_history(self):
        res = []
        for i in self.history:
            res.append({"role":i["role"],"content":i["content"]})
        return res
        
    def chat_to(self,s:str,enable_search:bool=False,stream:bool=False,temperature:float=0.3):
        self.history.append({"role":"user","content":s,"current_seq":self.current_seq})
        self.current_seq += 1
        extra_body = {}
        stream_kwargs = {}
        if enable_search:
            extra_body["enable_search"]=True
        if stream:
            stream_kwargs["stream"]=True
            stream_kwargs["stream_options"]={"include_usage": True}
        else:
            extra_body["enable_thinking"]=False
        stream_kwargs["extra_body"]=extra_body
        completion = CLIENT.chat.completions.create(
            model=MODEL,
            messages=self.build_ipt_history(),
            temperature=temperature,
            **stream_kwargs
        )
        if stream:
            res = ''
            for chunk in completion:
                try:
                    tmp = json.loads(chunk.model_dump_json())
                    res+=tmp["choices"][0]['delta']['content']
                except Exception as e:
                    continue
            try:
                self.used_tokens += json.loads(chunk.model_dump_json())["usage"]["total_tokens"]
            except Exception as e:
                print(e)
                self.used_tokens = 0
        else:
            res = completion.choices[0].message.content
            try:
                self.used_tokens += completion.usage.prompt_tokens+completion.usage.completion_tokens
            except Exception as e:
                print(e)
                self.used_tokens = 0
        self.last_output = res
        self.history.append({"role":"assistant","content":res,"current_seq":self.current_seq})
        
        return res

ChatBetweenAI类

协调类。管理并维护多个Character实例(现只支持2个Character)之间的对话流程。

类属性:
npcs:存储所有角色的字典(键为角色名,值为 Character 实例)。
initial_npc:初始发言者(根据Character中的initial参数确定)。
history:全局对话历史记录。
change_log:对话变更日志(用于版本控制或回溯)。
TOKEN_FOR_SPLIT_DIFFERENT_NPC/TOKEN_FOR_SPLIT_NPC_AND_CONTENT/TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE/TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER:用于分割不同对话部分的特殊标记符。
seq_history_length_dict:记录每个角色的对话序列长度,主要方便截取

类方法:
add_system_prompt:为某个npc添加系统提示词
argue:多轮对话主控方法,指定轮次数和对话参数
change_history:修改特定位置的对话历史
change_log_2_txt:将手动修改的历史转化为文本格式用以导出
export:导出当前对话、NPC以及修改历史信息
export_change_log:导出手动修改的历史
export_dialogue_to_csv:导出当前对话到csv中
export_history:导出当前对话历史
export_npc:导出npc信息
get_used_token:查看当前消耗的token数量
history_to_txt:将对话历史导出为文本文件
import_:导入历史对话、NPC以及修改历史
import_change_log:导入手动修改历史
import_history:导入历史对话
import_npc:导入NPC信息
oneRoundArgueBetweenTwoAI:处理两个角色的单轮对话。
truncate:截断历史记录
txt_2_change_log:将保存的文本“恢复”为修改记录
txt_to_history:将保存的文本“恢复”为历史记录

class ChatBetweenAI:
    def __init__(self,*args):
        self.npcs = {}
        self.initial_npc = None #注意:initial_npc是先“发话”的那个人,他初始化时不应该有initial参数
        for i in args:
            if type(i)==Character:
                self.npcs[i.name] = i
                if len(i.initial)>0:
                    self.initial_npc = i.name
        self.history = []
        self.change_log = []
        self.TOKEN_FOR_SPLIT_DIFFERENT_NPC ="<split_for_different_npc>"
        self.TOKEN_FOR_SPLIT_NPC_AND_CONTENT = "<split_for_npc_and_content>"
        self.TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE = "<split_for_npc_and_begore_change>"
        self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER = "<split_for_change_before_and_after>"
        self.seq_history_length_dict = {}

    def get_used_token(self):
        return sum([i.used_tokens for i in self.npcs.values()])
    
    def argue(self,rnd=5,enable_search:bool=False,stream:bool=False,temperature:float=0.3):
        """
        两个AI交流
        暂时只支持2个NPC
        """
        if len(self.npcs.keys())==2: # first:第一个说话的人, second:第二个说话的人,第一个传入AI进行推理的人
            first = self.npcs[self.initial_npc]
            second_name = [i for i in list(self.npcs.keys()) if i != self.initial_npc][0]
            second = self.npcs[second_name]
            # 第一轮,first不用传入任何对话
            if len(self.history)==0:
                self.history.append({"role":self.initial_npc,"content":first.initial,"role_seq":first.current_seq})
                res = second.chat_to(first.initial)
                self.history.extend([
                    {"role":second.name,"content":res,"role_seq":second.current_seq}
                ])
                self.seq_history_length_dict[0]=len(self.history)
                rnd -= 1
            for i in range(rnd):
                self.oneRoundArgueBetweenTwoAI(first,second,enable_search,stream,temperature)
                
    def oneRoundArgueBetweenTwoAI(self,first:Character,second:Character,enable_search:bool=False,stream:bool=False,temperature:float=0.3):
        first_ipt = second.last_output
        first_output = first.chat_to(first_ipt,enable_search,stream,temperature)
        second_output = second.chat_to(first_output,enable_search,stream,temperature)
        self.history.extend([
            {"role":first.name,"content":first_output,"role_seq":first.current_seq},
            {"role":second.name,"content":second_output,"role_seq":second.current_seq}
        ])
        self.seq_history_length_dict[second.current_seq]=len(self.history)
    
    def change_history(self,idx:int,s:str):
        if self.history[idx]["role"] == "system":
            assert "不能修改历史prompt!"
        origin = self.history[idx]["content"]
        self.history[idx]["content"] = s
        origin_role = self.history[idx]["role"]
        self.change_log.append({"role":origin_role,"origin":origin,"updated":s,"role_seq":self.history[idx]["role_seq"]})
        # 修改Character对象中地历史
        role_seq = self.history[idx]["role_seq"]
        npc_idx = [i for i,j in enumerate(self.npcs[origin_role].history) if j["role"]=="assistant"][role_seq]
        self.npcs[origin_role].history[npc_idx]["content"] = s
        if len(self.npcs[origin_role].history) == npc_idx+1:
            self.npcs[origin_role].last_output = s
        # 非initial的npc需要改“user”
        if self.initial_npc == origin_role:
            for npc in self.npcs.keys():
                if npc!=self.initial_npc:
                    ipt_idx = [i for i,j in enumerate(self.npcs[npc].history) if j["role"]=="user"][role_seq]
                    self.npcs[npc].history[ipt_idx]["content"] = s
                    
                    

    def truncate(self,last_seq:int):
        """
        暂时只支持2个NPC
        最后一个部分为“initial_npc”的发言

        顺序:initial_npc(current_seq=0)-> second(current_seq=0)->(一轮开始)first(current_seq=1)->second(current_seq=1)(一轮结束)
        """
        if self.npcs[self.initial_npc].current_seq<=last_seq:
            return
        self.history = self.history[0:self.seq_history_length_dict[last_seq]]
        for _,i in self.npcs.items():
            i.history = [n for n in i.history if n["current_seq"]<=last_seq and not (n["current_seq"]==last_seq and n["role"]!="assistant")]
            i.last_output = [n for n in i.history if n["role"]=="assistant"][-1]["content"]
            i.current_seq = last_seq

    def add_system_prompt(self,prompt_dict:dict):
        for k,v in prompt_dict.items():
            if k not in self.npcs.keys():
                assert f"{k}不存在!"
        for k,v in prompt_dict.items():
            self.npcs[k].history.append({"role":"system","content":v,"current_seq":self.npcs[k].current_seq})

    def history_to_txt(self,history):
        # import和export还需加入current_seq
        content = self.TOKEN_FOR_SPLIT_DIFFERENT_NPC.join([self.TOKEN_FOR_SPLIT_NPC_AND_CONTENT.join([str(i) for i in i.values()]) for i in history]) # 我不能确定AI输出的内容是否会有特殊字符,故而使用特殊字符进行分割
        return content

    def change_log_2_txt(self):
        content = []
        for k,d in enumerate(self.change_log):
            role = d["role"]
            before = d["origin"]
            after = d["updated"]
            role_seq = d["role_seq"]
            content.append(role+self.TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE+before+self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER+after+self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER+str(role_seq)) # 我不能确定AI输出的内容是否会有特殊字符,故而使用特殊字符进行分割
        return self.TOKEN_FOR_SPLIT_DIFFERENT_NPC.join(content)
    
    def txt_2_change_log(self,s:str):
        if len(s)==0:
            return []
        content = s.split(self.TOKEN_FOR_SPLIT_DIFFERENT_NPC)
        res = []
        for i in content:
            tmp = i.split(self.TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE)
            role = tmp[0]
            chage_before_and_after = tmp[1].split(self.TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER)
            change_before = chage_before_and_after[0]
            change_after = chage_before_and_after[1]
            role_seq = chage_before_and_after[2]
            res.append({"role":role,"origin":change_before,"updated":change_after,"role_seq":role_seq})
        return res
    
    def export_history(self,name:str=""):
        export_file_name = "_".join([i for i in self.npcs.keys()])
        if len(name) > 0:
            export_file_name = name
        else:
            export_file_name += datetime.now().strftime("%Y%m%d%H%M%S")
        content = self.history_to_txt(self.history)
        with open(export_file_name+".txt","w",encoding="utf-8") as f:
            f.write(content)

    def export_npc(self,name:str=""):
        export_file_name = "_".join([i for i in self.npcs.keys()])
        if len(name) > 0:
            export_file_name = name
        else:
            export_file_name += datetime.now().strftime("%Y%m%d%H%M%S")
        res_data = pd.DataFrame(columns=["prompt","name","initial","history","last_output"])
        for _,v in self.npcs.items():
            tmp = pd.DataFrame(pd.Series({"prompt":v.prompt,"name":v.name,"initial":v.initial,"history":self.history_to_txt(v.history),"last_output":v.last_output})).T
            res_data = pd.concat([res_data,tmp]).reset_index(drop=True)
        res_data.to_csv(export_file_name+".csv",index=None,encoding="utf-8")

    def export_change_log(self,name:str=""):
        export_file_name = "_".join([i for i in self.npcs.keys()])
        if len(name) > 0:
            export_file_name = name
        else:
            export_file_name += datetime.now().strftime("%Y%m%d%H%M%S")
        content = self.change_log_2_txt()
        with open(export_file_name+".txt","w",encoding="utf-8") as f:
            f.write(content)

    def export(self,filename:str):
        self.export_npc(filename+"_NPC")
        self.export_history(filename+"_history")
        self.export_change_log(filename+"_change_log")
        
    def txt_to_history(self,s:str,tpe:str = "Chat"):
        if len(s)==0:
            return []
        res = s.split(self.TOKEN_FOR_SPLIT_DIFFERENT_NPC)
        res_list = []
        for i in res:
            tmp = i.split(self.TOKEN_FOR_SPLIT_NPC_AND_CONTENT)
            if tpe == "Chat":
                res_list.append({"role":tmp[0],"content":tmp[1],"role_seq":int(tmp[2])})
            else:
                res_list.append({"role":tmp[0],"content":tmp[1],"current_seq":int(tmp[2])})
        return res_list

    def import_history(self,filename:str):
        with open(filename,encoding="utf-8") as f:
            s = f.read()
        # history备份
        #self.export_history()
        self.history = []
        try:
            self.history=self.txt_to_history(s)
            for i,x in enumerate(self.history):
                self.seq_history_length_dict[x["role_seq"]]=i+1
        except Exception as e:
            assert f"格式错误:{e}"
        
    def import_npc(self,filename:str):
        # npc备份
        #self.export_npc()
        npc_df = pd.read_csv(filename,encoding="utf-8")
        npc_df = npc_df.fillna("")
        for i in npc_df.index:
            npc_para = dict(npc_df.iloc[i,:])
            self.npcs[npc_para["name"]] = Character(name=npc_para["name"],prompt=npc_para["prompt"],initial=npc_para["initial"])
            self.npcs[npc_para["name"]].last_output = npc_para["last_output"]
            self.npcs[npc_para["name"]].history = self.txt_to_history(npc_para["history"],tpe="NPC")
            self.npcs[npc_para["name"]].current_seq = max([i["current_seq"] for i in self.npcs[npc_para["name"]].history if i["role"]=="assistant"])
            if len(npc_para["initial"])>0:
                self.initial_npc = npc_para["name"]

    def import_change_log(self,filename:str):
        #self.export_change_log()
        with open(filename,encoding="utf-8") as f:
            data = f.read()
        self.change_log = self.txt_2_change_log(data)
    def import_(self,filename:str):
        NPC_file = filename+"_NPC.csv"
        history_file = filename+"_history.txt"
        changelog_file = filename+"_change_log.txt"
        if not (os.path.exists(NPC_file) and os.path.exists(history_file) and os.path.exists(changelog_file)):
            assert "缺失存档,请检查存档名称!"
        self.import_npc(NPC_file)
        self.import_change_log(changelog_file)
        self.import_history(history_file)

    def export_dialogue_to_csv(self,file_name:str):
        step = len(self.npcs.keys())
        res = pd.DataFrame(columns=list(self.npcs.keys()))
        idx = 0
        while idx<len(self.history):
            this_round = self.history[idx:idx+step]
            tmp = {k:None for k in self.npcs.keys()}
            for i in this_round:
                tmp[i["role"]]=i["content"]
            res = pd.concat([res,pd.DataFrame(pd.Series(tmp)).T],axis=0)
            idx+=step
        res.to_csv(file_name+".csv",index=False,encoding="utf-8")

二、示例代码

promptA = history_prompt+"\n你是一个经验丰富且技术高超的辩论赛辩手,上面是某场辩论赛的正方/反方的一辩与二辩的论点。现在请以正方辩手的身份加入自由辩论环节,回答由用户扮演的反方辩手的质疑,同时需要注意回答完成之后对反方的观点提出新的质疑,让用户回答,最好能够有发散性思维,从之前没有提到的角度质疑用户从而形成头脑风暴。限制50字以内,只要说的语言即可,不要加入动作或神态等其它描写。"
promptB = history_prompt+"\n你是一个经验丰富且技术高超的辩论赛辩手,上面是某场辩论赛的正方/反方的一辩与二辩的论点。现在请以反方辩手的身份加入自由辩论环节,回答由用户扮演的正方辩手的质疑,同时需要注意回答完成之后对正方的观点提出新的质疑,让用户回答,最好能够有发散性思维,从之前没有提到的角度质疑用户从而形成头脑风暴。限制50字以内,只要说的语言即可,不要加入动作或神态等其它描写。你的第一句话是:请问对方辩友,如何解释魏尔斯特拉斯的ε-δ语言从未经过通俗转化,却成为现代分析学基石?这是否证明专业文章的价值穿透力无需\"转\"的中介?"
A = Character(name="正方",prompt=promptA,initial="")
B = Character(name="反方",prompt=promptB,initial='请问对方辩友,如何解释魏尔斯特拉斯的ε-δ语言从未经过通俗转化,却成为现代分析学基石?这是否证明专业文章的价值穿透力无需"转"的中介?')
Chat = ChatBetweenAI(A,B)
Chat.argue(rnd=5,enable_search=True,stream=True)

完整的辩论可以参照我的B站视频

三、安卓APK制成

Java和Kotlin似乎没有OpenAI包,所以需要使用阿里云百炼官方提供的Dashscope。
Character类:

data class Msg(
    val role: String,
    val content: String,
    @SerializedName("current_seq")
    val currentSeq: Int
)

class Character(
    val name: String,
    val prompt: String,
    val initial: String = ""
) {
    public var history = mutableListOf<Msg>().apply {
        add(Msg("system", prompt, -1))
    }

    var currentSeq = -1
        public set

    var lastOutput = initial
        public set

    var usedTokens = 0
        public set

    init {
        if (initial.isNotEmpty()) {
            currentSeq += 1
            history.add(Msg("assistant", initial, currentSeq))
            lastOutput = initial
        }
    }

    fun addMessage(role: String, content: String) {
        if (role == "assistant") {
            currentSeq += 1
            lastOutput = content
        }
        history.add(Msg(role, content, currentSeq))
    }

    fun buildIptHistory(): List<Map<String, String>> {
        return history.map {
            mapOf("role" to it.role, "content" to it.content)
        }
    }

    fun getFullHistory(): List<Msg> = history.toList()

    fun reset() {
        history.clear()
        history.add(Msg("system", prompt, -1))
        currentSeq = -1
        lastOutput = ""
        usedTokens = 0

        if (initial.isNotEmpty()) {
            addMessage("assistant", initial)
        }
    }

    fun getLastMessage(): Msg? = history.lastOrNull()
    fun chatTo(key:String,modelName:String,s:String,enable_Thinking:Boolean,enable_search:Boolean,stream:Boolean,temprature:Float):String{
        addMessage("user",s)
        //currentSeq+=1
        val iptMsg = createMessageArray()
        val params = GenerationParam.builder()
            .apiKey(key)
            .model(modelName)
            .enableSearch(enable_search)
            .messages(iptMsg)
            .resultFormat(GenerationParam.ResultFormat.MESSAGE)
            .incrementalOutput(stream)
            .enableThinking(enable_Thinking)
            .temperature(temprature)
            .build()
        val gen = Generation();
        if(stream){
            val GenResult = gen.streamCall(params)
            var res = ""
            GenResult.blockingForEach { i-> res+=parse_json_to_history(JsonUtils.toJson(i))}
            addMessage("assistant",res)
            return res
        }
        val GenResult = gen.call(params)
        val res = parse_json_to_history(JsonUtils.toJson(GenResult))
        addMessage("assistant",res)
        return res
    }
    fun createMessageArray():List<Message>{
        val res = ArrayList<Message>()
        val roleDict = mapOf("assistant" to Role.ASSISTANT,"system" to Role.SYSTEM,"user" to Role.USER);
        for (msg in history){
            val current_Role:Role = roleDict[msg.role] ?: Role.USER;
            res.add(createMessage(current_Role,msg.content));
        }
        return res
    }

    fun parse_json_to_history(jsonStr:String):String{
        val root = JsonParser.parseString(jsonStr).getAsJsonObject();
        val contents = root.getAsJsonObject("output")
            .getAsJsonArray("choices").map{it.asJsonObject.getAsJsonObject("message").get("content").asString}
            .joinToString ("")
        try{
            val totalTokens = root.getAsJsonObject("usage")
            .get("total_tokens").getAsInt()
            usedTokens += totalTokens
        }catch(e: Exception){
            usedTokens = 0
        }
        
        return contents
    }

    fun createMessage(role:Role,content:String):Message{
        return Message.builder().role(role.getValue()).content(content).build();
    }

    fun truncateHistory(lastSeq: Int){
        history = history.filter { n ->
            (n.currentSeq as Int <= lastSeq) &&
                    !(n.currentSeq as Int == lastSeq && n.role != "assistant")
        }.toMutableList()
        lastOutput = history
            .last { it.role == "assistant" }
            .content
        currentSeq = lastSeq
    }

    fun addSystemPrompt(prompt: String){
        if(history.lastOrNull()?.role.equals("system")){
            history[history.size-1] = Msg("system", prompt, currentSeq)
        }else{
            history.add(Msg("system", prompt, currentSeq))
        }
    }

    fun importHistory(historyList: List<Map<String, Any>>) {
        history.clear()
        historyList.forEach { entry ->
            history.add(Msg(
                role = entry["role"] as String,
                content = entry["content"] as String,
                currentSeq = (entry["current_seq"] as? Int) ?: 0
            ))
        }

        // 更新当前状态
        lastOutput = history.lastOrNull { it.role == "assistant" }?.content ?: ""
        currentSeq = history.maxOfOrNull { it.currentSeq } ?: -1
    }
}

ChatBetweenAI类:

class ChatBetweenAI {
    val npcs = mutableMapOf<String, Character>()
    var initialNpc: String? = null
    val history = mutableListOf<Map<String, Any?>>()
    val changeLog = mutableListOf<Map<String, Any?>>()
    val seqHistoryLengthDict = mutableMapOf<Int, Int>()
    companion object {
        const val TOKEN_FOR_SPLIT_DIFFERENT_NPC = "<split_for_different_npc>"
        const val TOKEN_FOR_SPLIT_NPC_AND_CONTENT = "<split_for_npc_and_content>"
        const val TOKEN_FOR_SPLIT_NPC_BEFORE_CHANGE = "<split_for_npc_and_before_change>"
        const val TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER = "<split_for_change_before_and_after>"
        const val DELIMITER_COLUMN_FOR_CSV = "<DELIMITER_COLUMN_FOR_CSV>"
        const val DELIMITER_ROW_FOR_CSV = "<DELIMITER_ROW_FOR_CSV>"
    }

    fun getUsedToken(): Int {
        return npcs.values.sumOf { it.usedTokens }
    }

    suspend fun argue(rnd: Int = 1,key:String,modelName:String,enableThinking:Boolean,enableSearch: Boolean, stream: Boolean, temperature: Float):Flow<Int> = flow{
        var status=0
        if (npcs.size != 2) {
            throw IllegalArgumentException("目前只支持 2 个 NPC 的交流")
        }
        var rnd = rnd
        val first = npcs[initialNpc] ?: throw IllegalStateException("初始 NPC 未设置")
        val secondName = npcs.keys.first { it != initialNpc }
        val second = npcs[secondName] ?: throw IllegalStateException("找不到第二个 NPC")

        // 第一轮对话
        if (history.filter { n->n.get("role")!="system" }.isEmpty()) {
            history.add(mapOf(
                "role" to initialNpc,
                "content" to first.initial,
                "role_seq" to first.currentSeq
            ))

            val res = second.chatTo(key,modelName,first.initial, enableThinking, enableSearch, stream, temperature)
            history.add(mapOf(
                "role" to second.name,
                "content" to res,
                "role_seq" to second.currentSeq
            ))
            rnd -= 1
            seqHistoryLengthDict[0] = history.size
            emit(status)
            status+=1
            if (rnd < 1){
                emit(-999)
                return@flow
            }
        }

        // 后续轮次
        for (i in 1 .. rnd) {
            oneRoundArgueBetweenTwoAI(key,modelName,first, second,enableThinking, enableSearch, stream, temperature)
            emit(status)
            status+=1
        }
    }

    private suspend fun oneRoundArgueBetweenTwoAI(
        key:String,modelName:String,
        first: Character,
        second: Character,
        enableThinking: Boolean,
        enableSearch: Boolean,
        stream: Boolean,
        temperature: Float
    ) {
        val firstInput = second.lastOutput
        val firstOutput = first.chatTo(key,modelName,firstInput, enableThinking, enableSearch, stream, temperature)

        val secondOutput = second.chatTo(key,modelName,firstOutput, enableThinking, enableSearch, stream, temperature)

        history.add(mapOf(
            "role" to first.name,
            "content" to firstOutput,
            "role_seq" to first.currentSeq
        ))

        history.add(mapOf(
            "role" to second.name,
            "content" to secondOutput,
            "role_seq" to second.currentSeq
        ))

        seqHistoryLengthDict[second.currentSeq] = history.size
    }

//    fun changeHistory(idx: Int, s: String) {
//        if (history[idx]["role"] == "system") {
//            throw IllegalStateException("不能修改历史 prompt!")
//        }
//
//        val origin = history[idx]["content"] as String
//        val originRole = history[idx]["role"] as String
//        val roleSeq = history[idx]["role_seq"] as Int
//
//        // 更新历史记录
//        val newEntry = history[idx].toMutableMap().apply { put("content", s) }
//        history[idx] = newEntry
//
//        // 添加到变更日志
//        changeLog.add(mapOf(
//            "role" to originRole,
//            "origin" to origin,
//            "updated" to s,
//            "role_seq" to roleSeq
//        ))
//
//        // 更新对应 NPC 的历史
//        val npc = npcs[originRole] ?: return
//        val npcHistory = npc.getFullHistory()
//
//        // 找到对应的历史记录并更新
//        val npcIdx = npcHistory.indexOfFirst {
//            it.role == "assistant" && it.currentSeq == roleSeq
//        }
//
//        if (npcIdx != -1) {
//            npc.updateMessageContent(npcIdx, s)
//
//            // 如果是最后一条消息,更新 lastOutput
//            if (npcHistory.lastIndex == npcIdx) {
//                npc.lastOutput = s
//            }
//        }
//
//        // 更新其他 NPC 的输入
//        if (initialNpc == originRole) {
//            npcs.values.forEach { otherNpc ->
//                if (otherNpc.name != originRole) {
//                    val otherHistory = otherNpc.getFullHistory()
//                    val inputIdx = otherHistory.indexOfFirst {
//                        it.role == "user" && it.currentSeq == roleSeq
//                    }
//
//                    if (inputIdx != -1) {
//                        otherNpc.updateMessageContent(inputIdx, s)
//                    }
//                }
//            }
//        }
//    }
    fun truncate(lastSeq: Int) {
        if ((npcs[initialNpc]?.currentSeq ?: 0) <= lastSeq) return

        val truncateIndex = seqHistoryLengthDict[lastSeq] ?: return
        history.subList(truncateIndex, history.size).clear()

        npcs.values.forEach { npc ->
            npc.truncateHistory(lastSeq)
        }
    }

    fun addSystemPrompt(promptDict: Map<String, String>) {
        promptDict.forEach { (name, prompt) ->
            if (!npcs.containsKey(name)) {
                throw IllegalArgumentException("$name 不存在!")
            }
        }

        promptDict.forEach { (name, prompt) ->
            npcs[name]?.addSystemPrompt(prompt)
            history.add(mapOf(
                "role" to "system",
                "content" to prompt,
                "role_seq" to npcs[name]?.currentSeq
            ))
        }

    }

    fun historyToTxt(historyList: List<Map<String, Any?>>): String {
        return historyList.joinToString(TOKEN_FOR_SPLIT_DIFFERENT_NPC) { entry ->
            listOf(
                entry["role"] as String,
                entry["content"] as String,
                entry["role_seq"].toString()
            ).joinToString(TOKEN_FOR_SPLIT_NPC_AND_CONTENT)
        }
    }

    fun changeLogToTxt(): String {
        return changeLog.joinToString(TOKEN_FOR_SPLIT_DIFFERENT_NPC) { log ->
            listOf(
                log["role"] as String,
                log["origin"] as String,
                log["updated"] as String,
                log["role_seq"].toString()
            ).joinToString(TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER)
        }
    }

    private fun generateFileName(): String {
        return npcs.keys.joinToString("_") + SimpleDateFormat(
            "yyyyMMddHHmmss",
            Locale.getDefault()
        ).format(Date())
    }

    fun exportHistory(context: Context,name: String = "") {
        val fileName = if (name.isNotEmpty()) name else generateFileName()
        val dir = context.getFilesDir().toString()+"$fileName.csv"
        File(dir).writeText(historyToTxt(history))
    }

    fun exportNpc(context: Context,name: String = "") {
        val fileName = if (name.isNotEmpty()) name else generateFileName()
        val csvContent = buildString {

            npcs.values.forEach { char ->
                val row = listOf(
                    char.prompt,
                    char.name,
                    char.initial,
                    historyToTxt(char.getFullHistory().map {
                        mapOf(
                            "role" to it.role.toString(),
                            "content" to it.content.toString(),
                            "role_seq" to it.currentSeq.toString()
                        )
                    }),
                    char.lastOutput
                ).joinToString ( DELIMITER_COLUMN_FOR_CSV ) + DELIMITER_ROW_FOR_CSV
                append(row)
            }
        }
        val dir = context.getFilesDir().toString()+"$fileName.csv"
        File(dir).writeText(csvContent)
    }

    fun exportChangeLog(context: Context,name: String = "") {
        val fileName = if (name.isNotEmpty()) name else generateFileName()
        val dir = context.getFilesDir().toString()+"$fileName.csv"
        File(dir).writeText(changeLogToTxt())
    }

    fun export(context: Context,baseName: String) {
        exportNpc(context,"${baseName}_NPC")
        exportHistory(context,"${baseName}_history")
        exportChangeLog(context,"${baseName}_change_log")
    }


    fun txtToHistory(s: String, type: String = "Chat"): List<Map<String, Any>> {
        if (s.isEmpty()) return emptyList()

        return s.split(TOKEN_FOR_SPLIT_DIFFERENT_NPC).map { entry ->
            val parts = entry.split(TOKEN_FOR_SPLIT_NPC_AND_CONTENT)
            when (type) {
                "Chat" -> mapOf(
                    "role" to parts[0],
                    "content" to parts[1],
                    "role_seq" to parts[2].toInt()
                )
                else -> mapOf(
                    "role" to parts[0],
                    "content" to parts[1],
                    "current_seq" to parts[2].toInt()
                )
            }
        }
    }

    fun importHistory(context: Context,filename: String) {
        val dir = context.getFilesDir().toString()+"$filename"
        val content = File(dir).readText()
        history.clear()
        history.addAll(txtToHistory(content))

        // 重建序列字典
        seqHistoryLengthDict.clear()
        history.forEachIndexed { index, entry ->
            (entry["role_seq"] as? Int)?.let { seq ->
                seqHistoryLengthDict[seq] = index + 1
            }
        }
    }

    fun importNpc(context: Context,filename: String) {
        val dir = context.getFilesDir().toString()+"$filename"
        val content = File(dir).readText().split(DELIMITER_ROW_FOR_CSV)

        if (content.size < 2) return

        for (line in content) {
            val values = line.split(DELIMITER_COLUMN_FOR_CSV)
            if (values.size < 5) continue

            val name = values[1]
            val prompt = values[0]
            val initial = values[2]
            val historyText = values[3]
            val lastOutput = values[4]

            val char = Character(name, prompt, initial)
            char.lastOutput = lastOutput
            char.importHistory(txtToHistory(historyText, "NPC"))

            npcs[name] = char

            if (initial.isNotEmpty()) {
                initialNpc = name
            }
        }
    }

    fun importChangeLog(context: Context,filename: String) {
        val dir = context.getFilesDir().toString()+"$filename"
        val content = File(dir).readText()
        changeLog.clear()

        if (content.isEmpty()) return

        content.split(TOKEN_FOR_SPLIT_DIFFERENT_NPC).forEach { entry ->
            val parts = entry.split(TOKEN_FOR_SPLIT_CHANGE_BEFORE_AND_AFTER)
            if (parts.size >= 4) {
                changeLog.add(mapOf(
                    "role" to parts[0],
                    "origin" to parts[1],
                    "updated" to parts[2],
                    "role_seq" to parts[3].toInt()
                ))
            }
        }
    }

    fun import(context: Context,baseName: String) {
        importNpc(context,"${baseName}_NPC.csv")
        importHistory(context,"${baseName}_history.csv")
        importChangeLog(context,"${baseName}_change_log.csv")
    }

    fun exportDialogueToCsv(fileName: String) {
        val step = npcs.size
        if (step == 0) return

        val csvContent = buildString {
            // 标题行
            appendLine(npcs.keys.joinToString(","))

            // 内容行
            var idx = 0
            while (idx < history.size) {
                val round = history.subList(idx, minOf(idx + step, history.size))
                val row = npcs.keys.map { npcName ->
                    round.find { it["role"] == npcName }?.get("content")?.toString() ?: ""
                }
                appendLine(row.joinToString(","))
                idx += step
            }
        }

        File("$fileName.csv").writeText(csvContent)
    }

}

完整的项目代码可以查看:我的github链接

四、未来展望

现在2个AI对话似乎没有过多意义,但是未来作为创作辅助或许有其价值;之后或许可以让2个角色通过MCP调用函数以实现更为逼真的交互,或者加入更多角色以实现多角色对话,从而进行创作辅助或进行实验。我觉得还是可能有一定价值的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值