├── rustfmt.toml ├── t.bat ├── config.toml ├── frontend ├── r.bat ├── .env.production ├── u.bat ├── .env.development ├── packages.txt ├── public │ ├── favicon.ico │ └── assets │ │ └── DialogFlowAiSDK.min.js ├── src │ ├── assets │ │ ├── text-bot.png │ │ ├── inbound-bot.png │ │ ├── outbound-bot.png │ │ ├── lang │ │ │ ├── lang.js │ │ │ └── i18n.js │ │ ├── usedByLlmChatNode.png │ │ ├── usedBySentenceEmbedding.png │ │ ├── usedByLlmChatNode-thumbnail.png │ │ ├── usedByDialogNodeTextGeneration.png │ │ ├── usedBySentenceEmbedding-thumbnail.png │ │ ├── usedByDialogNodeTextGeneration-thumbnail.png │ │ ├── main.css │ │ ├── base.css │ │ └── flags │ │ │ ├── gb.svg │ │ │ └── cn.svg │ ├── components │ │ ├── 404.vue │ │ ├── flow │ │ │ ├── nodes │ │ │ │ ├── EleTipTap.vue │ │ │ │ └── EndNode.vue │ │ │ └── MainFlow.vue │ │ ├── LanguageSwitcher.vue │ │ ├── external │ │ │ └── HttpApiList.vue │ │ ├── Demos.vue │ │ ├── robot │ │ │ └── RobotFrame.vue │ │ └── management │ │ │ └── GlobalSettings.vue │ ├── App.vue │ └── main.js ├── jsconfig.json ├── b.bat ├── .gitignore ├── index.html ├── README.md ├── package.json └── vite.config.js ├── src ├── man │ └── mod.rs ├── test │ ├── mod.rs │ └── reqwest.rs ├── external │ ├── mod.rs │ └── http │ │ ├── mod.rs │ │ ├── dto.rs │ │ ├── crud.rs │ │ └── client.rs ├── robot │ ├── mod.rs │ ├── dto.rs │ └── crud.rs ├── variable │ ├── mod.rs │ └── crud.rs ├── flow │ ├── mainflow │ │ ├── mod.rs │ │ ├── dto.rs │ │ └── crud.rs │ ├── subflow │ │ ├── mod.rs │ │ └── crud.rs │ ├── mod.rs │ ├── rt │ │ ├── mod.rs │ │ ├── collector.rs │ │ ├── facade.rs │ │ ├── crud.rs │ │ ├── executor.rs │ │ ├── dto.rs │ │ └── node_typetag.rs │ └── demo │ │ ├── demo_notify.txt │ │ ├── mod.rs │ │ └── demo_notify_en.txt ├── kb │ ├── mod.rs │ ├── dto.rs │ └── crud.rs ├── intent │ ├── mod.rs │ ├── dto.rs │ └── detector.rs ├── resources │ └── assets │ │ ├── favicon.ico │ │ ├── assets │ │ ├── text-bot-CWb_Poym.png │ │ ├── inbound-bot-PJJg_rST.png │ │ ├── outbound-bot-EmsLuWRN.png │ │ ├── usedByLlmChatNode-Bv2Fg5P7.png │ │ ├── usedBySentenceEmbedding-Dmju1hVB.png │ │ ├── usedByDialogNodeTextGeneration-DrFqkTqi.png │ │ ├── usedBySentenceEmbedding-thumbnail-DVXz_sh0.png │ │ ├── usedByDialogNodeTextGeneration-thumbnail-C1iQCVQO.png │ │ └── DialogFlowAiSDK.min.js │ │ └── index.html ├── db │ ├── embedding_ddl.sql │ └── embedding_lance.rs ├── web │ ├── mod.rs │ ├── asset.rs │ └── asset.txt ├── ai │ ├── qwen3.rs │ ├── asr.rs │ ├── tts.rs │ ├── mod.rs │ ├── audio.rs │ ├── crud.rs │ ├── token_output_stream.rs │ ├── phi3.rs │ └── gemma.rs ├── lib.rs └── main.rs ├── r.bat ├── doc └── assets │ ├── allinone.png │ └── screenshots │ ├── demo1.gif │ ├── testing.png │ ├── condition1.gif │ ├── dialogNode.png │ ├── gotoNode.png │ ├── homepage.png │ ├── theEndNode.png │ ├── chat_demo_zh.gif │ ├── collectNode.png │ ├── llmChatNode.png │ ├── robotDetail.png │ ├── chat_demo_eng.gif │ ├── conditionNode.png │ ├── externalApiNode.png │ ├── flow-editor-en.png │ ├── flow-editor-zh.png │ ├── sendEmailNode.png │ ├── textGeneration.gif │ └── knowledgeBaseAnswerNode.png ├── Cross.toml ├── z.bat ├── sdk ├── java │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── dialogflowai │ │ │ │ ├── sdk │ │ │ │ ├── CollectData.java │ │ │ │ ├── ExtraData.java │ │ │ │ ├── VarData.java │ │ │ │ ├── Answer.java │ │ │ │ ├── Response.java │ │ │ │ ├── VarKind.java │ │ │ │ ├── NextAction.java │ │ │ │ ├── UserInputResult.java │ │ │ │ ├── ResponseData.java │ │ │ │ ├── ImportVariable.java │ │ │ │ └── RequestData.java │ │ │ │ ├── util │ │ │ │ └── HttpUtil.java │ │ │ │ ├── Main.java │ │ │ │ └── RequestHandler.java │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── github │ │ │ └── dialogflowai │ │ │ └── RequestHandlerTest.java │ ├── README.md │ ├── .gitignore │ └── pom.xml ├── python │ ├── README.md │ └── sdk.py └── javascript │ ├── README_zh-CN.md │ └── README.md ├── musl-zcc ├── musl-zcxx ├── examples └── README.md ├── .github └── dependabot.yml ├── .cargo └── config.toml ├── .gitignore ├── README_zh-CN.md ├── README.md └── Cargo.toml /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 -------------------------------------------------------------------------------- /t.bat: -------------------------------------------------------------------------------- 1 | cargo test -- --nocapture -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | [rust] 2 | debuginfo-level = 1 -------------------------------------------------------------------------------- /frontend/r.bat: -------------------------------------------------------------------------------- 1 | rem npm run dev 2 | pnpm dev -------------------------------------------------------------------------------- /src/man/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod settings; 2 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod reqwest; 2 | -------------------------------------------------------------------------------- /src/external/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod http; 2 | -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_REQ_BACKEND_PREFIX=/ -------------------------------------------------------------------------------- /r.bat: -------------------------------------------------------------------------------- 1 | set RUST_LOG=tokio=trace,worker=trace 2 | cls&&cargo r -------------------------------------------------------------------------------- /frontend/u.bat: -------------------------------------------------------------------------------- 1 | npm i -g npm-check-updates 2 | ncu -u 3 | npm install -------------------------------------------------------------------------------- /src/robot/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod crud; 2 | pub(crate) mod dto; 3 | -------------------------------------------------------------------------------- /src/variable/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod crud; 2 | pub(crate) mod dto; 3 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_REQ_BACKEND_PREFIX=http://localhost:12715/ -------------------------------------------------------------------------------- /src/flow/mainflow/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod crud; 2 | pub(crate) mod dto; 3 | -------------------------------------------------------------------------------- /src/flow/subflow/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod crud; 2 | pub(crate) mod dto; 3 | -------------------------------------------------------------------------------- /frontend/packages.txt: -------------------------------------------------------------------------------- 1 | Rich text editor 2 | https://github.com/iliyaZelenko/tiptap-vuetify -------------------------------------------------------------------------------- /doc/assets/allinone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/allinone.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /src/kb/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod crud; 2 | pub(crate) mod doc; 3 | pub(crate) mod dto; 4 | pub(crate) mod qa; 5 | -------------------------------------------------------------------------------- /doc/assets/screenshots/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/demo1.gif -------------------------------------------------------------------------------- /frontend/src/assets/text-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/text-bot.png -------------------------------------------------------------------------------- /frontend/src/components/404.vue: -------------------------------------------------------------------------------- 1 | 2 | 404 not found 3 | Go to Home 4 | -------------------------------------------------------------------------------- /src/flow/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod demo; 2 | pub(crate) mod mainflow; 3 | pub(crate) mod rt; 4 | pub(crate) mod subflow; 5 | -------------------------------------------------------------------------------- /src/intent/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod crud; 2 | pub(crate) mod detector; 3 | pub(crate) mod dto; 4 | pub(crate) mod phrase; 5 | -------------------------------------------------------------------------------- /src/resources/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/favicon.ico -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-musl] 2 | image = "ghcr.nju.edu.cn/cross-rs/x86_64-unknown-linux-musl:edge" 3 | 4 | -------------------------------------------------------------------------------- /doc/assets/screenshots/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/testing.png -------------------------------------------------------------------------------- /doc/assets/screenshots/condition1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/condition1.gif -------------------------------------------------------------------------------- /doc/assets/screenshots/dialogNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/dialogNode.png -------------------------------------------------------------------------------- /doc/assets/screenshots/gotoNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/gotoNode.png -------------------------------------------------------------------------------- /doc/assets/screenshots/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/homepage.png -------------------------------------------------------------------------------- /doc/assets/screenshots/theEndNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/theEndNode.png -------------------------------------------------------------------------------- /frontend/src/assets/inbound-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/inbound-bot.png -------------------------------------------------------------------------------- /frontend/src/assets/outbound-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/outbound-bot.png -------------------------------------------------------------------------------- /z.bat: -------------------------------------------------------------------------------- 1 | "C:\Program Files\7-Zip\7z.exe" a dialogflowai.zip -x!.git -x!data -x!doc -x!examples -x!frontend -x!sdk -x!target -x!*.exe -w. -------------------------------------------------------------------------------- /doc/assets/screenshots/chat_demo_zh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/chat_demo_zh.gif -------------------------------------------------------------------------------- /doc/assets/screenshots/collectNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/collectNode.png -------------------------------------------------------------------------------- /doc/assets/screenshots/llmChatNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/llmChatNode.png -------------------------------------------------------------------------------- /doc/assets/screenshots/robotDetail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/robotDetail.png -------------------------------------------------------------------------------- /frontend/src/assets/lang/lang.js: -------------------------------------------------------------------------------- 1 | import zh from './zh.js' 2 | import en from './en.js' 3 | export default { 4 | zh, 5 | en 6 | }; -------------------------------------------------------------------------------- /src/db/embedding_ddl.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE {intent_id} ( 2 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 3 | vectors JSON NOT NULL 4 | ); -------------------------------------------------------------------------------- /doc/assets/screenshots/chat_demo_eng.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/chat_demo_eng.gif -------------------------------------------------------------------------------- /doc/assets/screenshots/conditionNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/conditionNode.png -------------------------------------------------------------------------------- /doc/assets/screenshots/externalApiNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/externalApiNode.png -------------------------------------------------------------------------------- /doc/assets/screenshots/flow-editor-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/flow-editor-en.png -------------------------------------------------------------------------------- /doc/assets/screenshots/flow-editor-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/flow-editor-zh.png -------------------------------------------------------------------------------- /doc/assets/screenshots/sendEmailNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/sendEmailNode.png -------------------------------------------------------------------------------- /doc/assets/screenshots/textGeneration.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/textGeneration.gif -------------------------------------------------------------------------------- /frontend/src/assets/usedByLlmChatNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/usedByLlmChatNode.png -------------------------------------------------------------------------------- /src/external/http/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod client; 2 | pub(crate) mod crud; 3 | pub(crate) mod dto; 4 | 5 | pub(crate) use client::get_client; 6 | -------------------------------------------------------------------------------- /frontend/src/assets/usedBySentenceEmbedding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/usedBySentenceEmbedding.png -------------------------------------------------------------------------------- /src/resources/assets/assets/text-bot-CWb_Poym.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/text-bot-CWb_Poym.png -------------------------------------------------------------------------------- /doc/assets/screenshots/knowledgeBaseAnswerNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/doc/assets/screenshots/knowledgeBaseAnswerNode.png -------------------------------------------------------------------------------- /frontend/src/assets/usedByLlmChatNode-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/usedByLlmChatNode-thumbnail.png -------------------------------------------------------------------------------- /src/resources/assets/assets/inbound-bot-PJJg_rST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/inbound-bot-PJJg_rST.png -------------------------------------------------------------------------------- /frontend/src/assets/usedByDialogNodeTextGeneration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/usedByDialogNodeTextGeneration.png -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/CollectData.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | public class CollectData extends VarData { 4 | } 5 | -------------------------------------------------------------------------------- /src/resources/assets/assets/outbound-bot-EmsLuWRN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/outbound-bot-EmsLuWRN.png -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/assets/usedBySentenceEmbedding-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/usedBySentenceEmbedding-thumbnail.png -------------------------------------------------------------------------------- /src/resources/assets/assets/usedByLlmChatNode-Bv2Fg5P7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/usedByLlmChatNode-Bv2Fg5P7.png -------------------------------------------------------------------------------- /src/web/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod asset; 2 | pub mod server; 3 | 4 | // pub use crate::flow::rt::context::clean_expired_session; 5 | // pub use crate::flow::rt::convertor::t1; 6 | -------------------------------------------------------------------------------- /frontend/src/assets/usedByDialogNodeTextGeneration-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/frontend/src/assets/usedByDialogNodeTextGeneration-thumbnail.png -------------------------------------------------------------------------------- /src/resources/assets/assets/usedBySentenceEmbedding-Dmju1hVB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/usedBySentenceEmbedding-Dmju1hVB.png -------------------------------------------------------------------------------- /frontend/b.bat: -------------------------------------------------------------------------------- 1 | rem npm run build 2 | pnpm build 3 | copy /Y src\assets\DialogFlowAiSDK.js ..\sdk\javascript\. 4 | del /S /Q ..\src\resources\assets\* 5 | xcopy /S dist\* ..\src\resources\assets\. -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/util/HttpUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.util; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | @Slf4j 6 | public class HttpUtil { 7 | } 8 | -------------------------------------------------------------------------------- /src/resources/assets/assets/usedByDialogNodeTextGeneration-DrFqkTqi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/usedByDialogNodeTextGeneration-DrFqkTqi.png -------------------------------------------------------------------------------- /src/resources/assets/assets/usedBySentenceEmbedding-thumbnail-DVXz_sh0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/usedBySentenceEmbedding-thumbnail-DVXz_sh0.png -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/ExtraData.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ExtraData { 7 | private String externalLink; 8 | } 9 | -------------------------------------------------------------------------------- /src/resources/assets/assets/usedByDialogNodeTextGeneration-thumbnail-C1iQCVQO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialogflowai/dialogflow/HEAD/src/resources/assets/assets/usedByDialogNodeTextGeneration-thumbnail-C1iQCVQO.png -------------------------------------------------------------------------------- /sdk/python/README.md: -------------------------------------------------------------------------------- 1 | 2 | ``` 3 | git clone https://github.com/dialogflowai/dialogflow 4 | cd dialogflow 5 | cd sdk 6 | cd python 7 | # pip install requests 8 | # pip install pydantic 9 | python3 sdk.py 10 | ``` 11 | -------------------------------------------------------------------------------- /src/ai/qwen3.rs: -------------------------------------------------------------------------------- 1 | use candle::Tensor; 2 | use candle::quantized::gguf_file; 3 | use candle_transformers::generation::{LogitsProcessor, Sampling}; 4 | // use candle_transformers::models::quantized_qwen3::ModelWeights as Qwen3; 5 | -------------------------------------------------------------------------------- /musl-zcc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | p=`echo $@ | sed 's/--target=x86_64-unknown-linux-musl/-target x86_64-linux-musl/g' | sed 's/--target=x86_64-unknown-linux-gnu/-target x86_64-linux-musl/g'` 3 | echo $p 4 | zig cc -target x86_64-linux-musl $p 5 | -------------------------------------------------------------------------------- /musl-zcxx: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | p=`echo $@ | sed 's/--target=x86_64-unknown-linux-musl/-target x86_64-linux-musl/g' | sed 's/--target=x86_64-unknown-linux-gnu/-target x86_64-linux-musl/g'` 3 | echo $p 4 | zig c++ -target x86_64-linux-musl $p 5 | -------------------------------------------------------------------------------- /src/flow/mainflow/dto.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize)] 4 | pub(crate) struct MainFlowDetail { 5 | pub(crate) id: String, 6 | pub(crate) name: String, 7 | pub(crate) enabled: bool, 8 | } 9 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/VarData.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class VarData { 7 | private String varName; 8 | private String varValue; 9 | } 10 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/Answer.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Answer { 7 | private String content; 8 | private String contentType; 9 | } 10 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/Response.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Response { 7 | private int status; 8 | private ResponseData data; 9 | public String err; 10 | } 11 | -------------------------------------------------------------------------------- /src/ai/asr.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::huggingface::HuggingFaceModel; 4 | 5 | #[derive(Clone, Deserialize, Serialize)] 6 | #[serde(tag = "id", content = "model")] 7 | pub(crate) enum AsrProvider { 8 | HuggingFace(HuggingFaceModel), 9 | } 10 | -------------------------------------------------------------------------------- /src/ai/tts.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::huggingface::HuggingFaceModel; 4 | 5 | #[derive(Clone, Deserialize, Serialize)] 6 | #[serde(tag = "id", content = "model")] 7 | pub(crate) enum TtsProvider { 8 | HuggingFace(HuggingFaceModel), 9 | } 10 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/VarKind.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public enum VarKind { 6 | @JsonProperty("String") 7 | STRING, 8 | @JsonProperty("Number") 9 | NUMBER, 10 | } 11 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/NextAction.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public enum NextAction { 6 | @JsonProperty("Terminate") 7 | TERMINATE, 8 | @JsonProperty("None") 9 | NONE, 10 | } 11 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/UserInputResult.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public enum UserInputResult { 6 | @JsonProperty("Successful") 7 | SUCCESSFUL, 8 | @JsonProperty("Timeout") 9 | TIMEOUT, 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/flow/rt/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod collector; 2 | pub(crate) mod condition; 3 | pub(crate) mod context; 4 | pub(crate) mod convertor; 5 | pub(crate) mod crud; 6 | pub(crate) mod dto; 7 | pub(crate) mod executor; 8 | pub(crate) mod facade; 9 | pub(crate) mod node; 10 | // pub(crate) mod node_impl; 11 | // pub(crate) mod request; 12 | // pub(crate) mod response; 13 | -------------------------------------------------------------------------------- /src/ai/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod asr; 2 | pub(crate) mod audio; 3 | pub(crate) mod bs1770; 4 | pub(crate) mod chat; 5 | pub(crate) mod completion; 6 | pub(crate) mod crud; 7 | pub(crate) mod embedding; 8 | pub(crate) mod gemma; 9 | pub(super) mod huggingface; 10 | pub(super) mod llama; 11 | pub(super) mod phi3; 12 | pub(super) mod qwen3; 13 | mod token_output_stream; 14 | pub(crate) mod tts; 15 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/ResponseData.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class ResponseData { 9 | private String sessionId; 10 | private List answers; 11 | private List collectData; 12 | private NextAction nextAction; 13 | private ExtraData extraData; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // #[global_allocator] 2 | // static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc; 3 | 4 | pub(crate) mod ai; 5 | pub(crate) mod db; 6 | pub(crate) mod external; 7 | pub(crate) mod flow; 8 | pub(crate) mod intent; 9 | pub(crate) mod kb; 10 | pub(crate) mod man; 11 | pub(crate) mod result; 12 | pub(crate) mod robot; 13 | // #[cfg(test)] 14 | // pub mod test; 15 | pub(crate) mod variable; 16 | pub mod web; 17 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ### HTML 2 | Please check [html folder](./html) 3 | 4 | ### Java 5 | You can refer to: [Java SDK](../sdk/java), in the file: `Main.java`. 6 | ``` 7 | git clone https://github.com/dialogflowai/dialogflow 8 | cd dialogflow 9 | cd sdk 10 | cd java 11 | # It's a Maven project 12 | ``` 13 | 14 | ### Python 15 | ``` 16 | git clone https://github.com/dialogflowai/dialogflow 17 | cd dialogflow 18 | cd sdk 19 | cd python 20 | # pip install requests 21 | # pip install pydantic 22 | python3 sdk.py 23 | ``` 24 | -------------------------------------------------------------------------------- /sdk/java/README.md: -------------------------------------------------------------------------------- 1 | ### HTML 2 | Please check [html folder](./html) 3 | 4 | ### Java 5 | You can refer to: [Java SDK](../sdk/java), in the file: `Main.java`. 6 | ``` 7 | git clone https://github.com/dialogflowai/dialogflow 8 | cd dialogflow 9 | cd sdk 10 | cd java 11 | # It's a Maven project 12 | ``` 13 | 14 | ### Python 15 | ``` 16 | git clone https://github.com/dialogflowai/dialogflow 17 | cd dialogflow 18 | cd sdk 19 | cd python 20 | # pip install requests 21 | # pip install pydantic 22 | python3 sdk.py 23 | ``` 24 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | stats.html 31 | package-lock.json 32 | src/auto-imports.d.ts 33 | src/components.d.ts -------------------------------------------------------------------------------- /frontend/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | a, 4 | .green { 5 | text-decoration: none; 6 | color: hsla(160, 100%, 37%, 1); 7 | transition: 0.4s; 8 | } 9 | 10 | @media (hover: hover) { 11 | a:hover { 12 | background-color: hsla(160, 100%, 37%, 0.2); 13 | } 14 | } 15 | 16 | /* @media (min-width: 1024px) { 17 | body { 18 | display: flex; 19 | place-items: center; 20 | } 21 | 22 | #app { 23 | display: grid; 24 | grid-template-columns: 1fr 1fr; 25 | padding: 0 2rem; 26 | } 27 | } */ 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /frontend/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | body { 3 | /* min-height: 100vh; */ 4 | transition: color 0.5s, background-color 0.5s; 5 | line-height: 1.6; 6 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 7 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 8 | /* font-size: 12px; */ 9 | text-rendering: optimizeLegibility; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | margin:0; 13 | padding: 0; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Dialog flow chat bot with intuitive node-based editor. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/ImportVariable.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | @EqualsAndHashCode(callSuper = true) 7 | @Data 8 | public class ImportVariable extends VarData { 9 | private VarKind varKind; 10 | 11 | public static ImportVariable create(String varName, String varValue, VarKind varKind) { 12 | ImportVariable v = new ImportVariable(); 13 | v.setVarName(varName); 14 | v.setVarValue(varValue); 15 | v.setVarKind(varKind); 16 | return v; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # rustflags = ["-C", "target-cpu=native"] 3 | # rustdocflags = ["-C", "target-cpu=native"] 4 | 5 | [target.wasm32-unknown-unknown] 6 | # rustflags = ["-C", "target-feature=+simd128"] 7 | 8 | [http] 9 | # proxy="ip:port" 10 | 11 | [target.wasm32-wasi] 12 | # rustflags = ["-C", "target-feature=+simd128"] 13 | 14 | [target.x86_64-unknown-linux-musl] 15 | # linker = "rust-lld" 16 | linker = "./musl-zcxx" 17 | # linker = "x86_64-linux-musl-gcc" 18 | # rustflags = ["-Clinker=rust-lld"] 19 | # rustflags = ["-C", "linker-flavor=ld.lld"] 20 | rustflags = ["-C", "linker-flavor=gcc", "-C", "link-self-contained=no"] 21 | -------------------------------------------------------------------------------- /src/robot/dto.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize)] 4 | pub(crate) struct RobotQuery { 5 | #[serde(rename = "robotId")] 6 | pub(crate) robot_id: String, 7 | } 8 | 9 | #[derive(Deserialize, Serialize)] 10 | pub(crate) enum RobotType { 11 | InboundCallBot, 12 | OutboundCallBot, 13 | TextBot, 14 | } 15 | 16 | #[derive(Deserialize, Serialize)] 17 | pub(crate) struct RobotData { 18 | #[serde(rename = "robotId")] 19 | pub(crate) robot_id: String, 20 | #[serde(rename = "robotName")] 21 | pub(crate) robot_name: String, 22 | #[serde(rename = "robotType")] 23 | pub(crate) robot_type: RobotType, 24 | } 25 | -------------------------------------------------------------------------------- /sdk/java/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/libraries/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | ### Eclipse ### 16 | .apt_generated 17 | .classpath 18 | .factorypath 19 | .project 20 | .settings 21 | .springBeans 22 | .sts4-cache 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | 37 | ### Mac OS ### 38 | .DS_Store 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | src/resources/assets/*.gz 21 | src/resources/assets/assets/*.gz 22 | flow.db 23 | dialogflowai.exe 24 | TODO.txt 25 | # index.html 26 | /data 27 | .idea 28 | -------------------------------------------------------------------------------- /src/resources/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Dialog flow chat bot with intuitive node-based editor. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/flow/demo/demo_notify.txt: -------------------------------------------------------------------------------- 1 | [{"id":"103bsgvhkfdr1f386pl3inrqgi","name":"通知流程","canvas":"{\"cells\":[{\"position\":{\"x\":370,\"y\":90},\"size\":{\"width\":270,\"height\":129},\"view\":\"vue-shape-view\",\"shape\":\"EndNode\",\"ports\":{\"groups\":{\"absolute\":{\"position\":{\"name\":\"absolute\"},\"attrs\":{\"circle\":{\"r\":5,\"magnet\":true,\"stroke\":\"black\",\"strokeWidth\":1,\"fill\":\"#fff\",\"style\":{\"visibility\":\"show\"}}},\"label\":{\"position\":\"left\"}}},\"items\":[]},\"id\":\"59d771ff-2919-4df1-bc0e-2ef98656fcb0\",\"zIndex\":1,\"data\":{\"nodeType\":\"EndNode\",\"nodeCnt\":2,\"currentTime\":1716638587437,\"nodeName\":\"The end\",\"endingText\":\"您好,现在致电您,是因为您之前在我行申请的贷款,已审批。请及时登录App查看相关信息,谢谢\",\"valid\":true,\"invalidMessages\":[],\"newNode\":false,\"nodeId\":\"59d771ff-2919-4df1-bc0e-2ef98656fcb0\"},\"tools\":{\"items\":[]}}]}"}] -------------------------------------------------------------------------------- /src/web/asset.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use std::sync::LazyLock; 4 | 5 | pub(crate) static ASSETS_MAP: LazyLock> = LazyLock::new(|| { 6 | HashMap::from([ 7 | (r"/assets/DialogFlowAiSDK.min.js", 0), 8 | (r"/assets/inbound-bot-PJJg_rST.png", 1), 9 | (r"/assets/index-BgHOwAJS.js", 2), 10 | (r"/assets/index-C1HBSe1j.css", 3), 11 | (r"/assets/index-CBXDnolR.css", 4), 12 | (r"/assets/index-HTbpkmSB.js", 5), 13 | (r"/assets/outbound-bot-EmsLuWRN.png", 6), 14 | (r"/assets/text-bot-CWb_Poym.png", 7), 15 | (r"/assets/usedByDialogNodeTextGeneration-DrFqkTqi.png", 8), 16 | (r"/assets/usedByDialogNodeTextGeneration-thumbnail-C1iQCVQO.png", 9), 17 | (r"/assets/usedByLlmChatNode-Bv2Fg5P7.png", 10), 18 | (r"/assets/usedBySentenceEmbedding-Dmju1hVB.png", 11), 19 | (r"/assets/usedBySentenceEmbedding-thumbnail-DVXz_sh0.png", 12), 20 | (r"/favicon.ico", 13), 21 | ("/", 14), 22 | (r"/index.html", 14), 23 | ])}); 24 | -------------------------------------------------------------------------------- /src/flow/demo/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const DEMO_COLLECT: &str = include_str!("demo_collect.txt"); 2 | pub(crate) const DEMO_NOTIFY: &str = include_str!("demo_notify.txt"); 3 | pub(crate) const DEMO_REPAY: &str = include_str!("demo_repay.txt"); 4 | pub(crate) const DEMO_COLLECT_EN: &str = include_str!("demo_collect_en.txt"); 5 | pub(crate) const DEMO_NOTIFY_EN: &str = include_str!("demo_notify_en.txt"); 6 | pub(crate) const DEMO_REPAY_EN: &str = include_str!("demo_repay_en.txt"); 7 | 8 | pub(crate) fn get_demo<'a>(is_en: bool, name: &'a str) -> Option<&'static str> { 9 | if name.eq("demo-collect") { 10 | Some(if is_en { DEMO_COLLECT_EN } else { DEMO_COLLECT }) 11 | } else if name.eq("demo-notify") { 12 | Some(if is_en { DEMO_NOTIFY_EN } else { DEMO_NOTIFY }) 13 | } else if name.eq("demo-repay") { 14 | Some(if is_en { DEMO_REPAY_EN } else { DEMO_REPAY }) 15 | } else { 16 | None 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sdk/java/src/test/java/io/github/dialogflowai/RequestHandlerTest.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai; 2 | 3 | import io.github.dialogflowai.sdk.RequestData; 4 | import io.github.dialogflowai.sdk.Response; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.io.IOException; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; 11 | 12 | public class RequestHandlerTest { 13 | @Test 14 | void reqTest() throws IOException, InterruptedException { 15 | RequestData requestData = RequestData.create("r03dbzxp6zpk9uhkgcbw1ec604", "103dbzxp74kjwb148ubfmhgemb"); 16 | RequestHandler requestHandler = new RequestHandler("http://127.0.0.1:12715/flow/answer"); 17 | Response res = requestHandler.req(requestData, 1000); 18 | assertNotNull(res); 19 | assertNotNull(res.getData()); 20 | assertEquals(1, res.getData().getAnswers().size()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/flow/demo/demo_notify_en.txt: -------------------------------------------------------------------------------- 1 | [{"id":"103bsgvhkfdr1f386pl3inrqgi","name":"Notification","canvas":"{\"cells\":[{\"position\":{\"x\":370,\"y\":90},\"size\":{\"width\":270,\"height\":129},\"view\":\"vue-shape-view\",\"shape\":\"EndNode\",\"ports\":{\"groups\":{\"absolute\":{\"position\":{\"name\":\"absolute\"},\"attrs\":{\"circle\":{\"r\":5,\"magnet\":true,\"stroke\":\"black\",\"strokeWidth\":1,\"fill\":\"#fff\",\"style\":{\"visibility\":\"show\"}}},\"label\":{\"position\":\"left\"}}},\"items\":[]},\"id\":\"59d771ff-2919-4df1-bc0e-2ef98656fcb0\",\"zIndex\":1,\"data\":{\"nodeType\":\"EndNode\",\"nodeCnt\":2,\"currentTime\":1716638587437,\"nodeName\":\"The end\",\"endingText\":\"Hello, we are calling you now because you have previously applied for a loan with our bank and it has been approved. Please log in to the App in time to check the information, thank you!\",\"valid\":true,\"invalidMessages\":[],\"newNode\":false,\"nodeId\":\"59d771ff-2919-4df1-bc0e-2ef98656fcb0\"},\"tools\":{\"items\":[]}}]}"}] -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/sdk/RequestData.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai.sdk; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class RequestData { 7 | private String robotId; 8 | private String mainFlowId; 9 | private String sessionId; 10 | private UserInputResult userInputResult; 11 | private String userInput; 12 | private ImportVariable[] importVariables; 13 | private String userInputIntent; 14 | 15 | public static RequestData create(String robotId, String mainFlowId) { 16 | RequestData requestData = new RequestData(); 17 | requestData.setRobotId(robotId); 18 | requestData.setMainFlowId(mainFlowId); 19 | return requestData; 20 | } 21 | 22 | public static RequestData create(String robotId, String mainFlowId, String userInput) { 23 | RequestData requestData = create(robotId, mainFlowId); 24 | requestData.setUserInput(userInput); 25 | return requestData; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ai/audio.rs: -------------------------------------------------------------------------------- 1 | use candle::Tensor; 2 | 3 | use crate::result::Result; 4 | 5 | pub fn normalize_loudness( 6 | wav: &Tensor, 7 | sample_rate: u32, 8 | loudness_compressor: bool, 9 | ) -> Result { 10 | let energy = wav.sqr()?.mean_all()?.sqrt()?.to_vec0::()?; 11 | if energy < 2e-3 { 12 | return Ok(wav.clone()); 13 | } 14 | let wav_array = wav.to_vec1::()?; 15 | let mut meter = super::bs1770::ChannelLoudnessMeter::new(sample_rate); 16 | meter.push(wav_array.into_iter()); 17 | let power = meter.as_100ms_windows(); 18 | let loudness = match super::bs1770::gated_mean(power) { 19 | None => return Ok(wav.clone()), 20 | Some(gp) => gp.loudness_lkfs() as f64, 21 | }; 22 | let delta_loudness = -14. - loudness; 23 | let gain = 10f64.powf(delta_loudness / 20.); 24 | let wav = (wav * gain)?; 25 | if loudness_compressor { 26 | let r = wav.tanh()?; 27 | Ok(r) 28 | } else { 29 | Ok(wav) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/kb/dto.rs: -------------------------------------------------------------------------------- 1 | use std::vec::Vec; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // #[derive(Deserialize, Serialize)] 6 | // pub(crate) struct QuestionAnswerData { 7 | // pub(super) id: Option, 8 | // #[serde(rename = "qaData")] 9 | // pub(super) qa_data: QuestionAnswerPair, 10 | // } 11 | 12 | #[derive(Deserialize, Serialize)] 13 | pub(crate) struct QuestionAnswerPair { 14 | pub(super) id: Option, 15 | pub(super) question: QuestionData, 16 | #[serde(rename = "similarQuestions")] 17 | pub(super) similar_questions: Vec, 18 | pub(crate) answer: String, 19 | } 20 | 21 | #[derive(Deserialize, Serialize)] 22 | pub(crate) struct QuestionData { 23 | pub(super) question: String, 24 | pub(super) vec_row_id: Option, 25 | } 26 | 27 | #[derive(Deserialize, Serialize)] // , sqlx::FromRow 28 | pub(crate) struct DocData { 29 | pub(crate) id: i64, 30 | #[serde(rename = "fileName")] 31 | pub(crate) file_name: String, 32 | #[serde(rename = "fileSize")] 33 | pub(crate) file_size: i64, 34 | #[serde(rename = "docContent")] 35 | pub(crate) doc_content: String, 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/assets/lang/i18n.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from "vue-i18n"; 2 | // import messages from './lang.js' 3 | import zh from './zh.js' 4 | import en from './en.js' 5 | // const language = ((navigator.language ? navigator.language : navigator.userLanguage) || 'en').toLowerCase() 6 | function detectLocale() { 7 | const queryLang = new URLSearchParams(window.location.search).get('lang') 8 | if (queryLang && ['en', 'zh'].includes(queryLang)) return queryLang 9 | 10 | const savedLang = localStorage.getItem('lang') 11 | if (savedLang && ['en', 'zh'].includes(savedLang)) return savedLang 12 | 13 | const browserLang = ((navigator.language ? navigator.language : navigator.userLanguage) || 'en').toLowerCase() 14 | if (browserLang.includes('zh')) return 'zh' 15 | if (browserLang.includes('en')) return 'en' 16 | 17 | return 'en' // fallback 18 | } 19 | const i18n = createI18n({ 20 | fallbackLocale: 'en', 21 | globalInjection: true, 22 | legacy: false, 23 | locale: detectLocale(), 24 | // locale: language.split('-')[0] || 'en', 25 | // messages, 26 | messages: { 27 | zh, 28 | en 29 | } 30 | }) 31 | 32 | export default i18n; -------------------------------------------------------------------------------- /src/test/reqwest.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | 3 | use anyhow::Result; 4 | use futures_util::StreamExt; 5 | use tokio::fs::File; 6 | use tokio::io::AsyncWriteExt; 7 | 8 | #[tokio::test] 9 | async fn download() -> Result<()> { 10 | let u = "https://huggingface.co/GanymedeNil/text2vec-large-chinese/resolve/main/tokenizer.json?download=true"; 11 | let client = reqwest::Client::builder() 12 | .connect_timeout(Duration::from_millis(5000)) 13 | .read_timeout(Duration::from_millis(10000)) 14 | .proxy(reqwest::Proxy::https("http://127.0.0.1:7897")?) 15 | .build()?; 16 | let res = client.get(u).send().await?; 17 | let total_size = res.content_length().unwrap(); 18 | println!("Total size {total_size}"); 19 | // let b = res.bytes().await?; 20 | // fs::write("./temp.file", b.as_ref()).await?; 21 | let mut downloaded: u64 = 0; 22 | let mut stream = res.bytes_stream(); 23 | let mut file = File::create("./temp.file").await?; 24 | 25 | while let Some(item) = stream.next().await { 26 | let chunk = item?; 27 | file.write_all(&chunk).await?; 28 | let new = std::cmp::min(downloaded + (chunk.len() as u64), total_size); 29 | println!("Downloaded {new}"); 30 | downloaded = new; 31 | } 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/assets/flags/gb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/flow/rt/collector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use regex::Regex; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | static NUMBER_REGEX: LazyLock = 7 | LazyLock::new(|| Regex::new(r"[1-9]([\d]+)?(.[\d]+)?").unwrap()); 8 | 9 | #[derive(Clone, Deserialize, Serialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] 10 | #[rkyv(compare(PartialEq))] 11 | pub(crate) enum CollectType { 12 | UserInput, 13 | Number, 14 | IdCard, 15 | CustomizeRegex(String), 16 | } 17 | 18 | pub(crate) fn collect<'a>(s: &'a str, collect_type: &CollectType) -> Option<&'a str> { 19 | match collect_type { 20 | CollectType::UserInput => Some(s), 21 | CollectType::Number => { 22 | if let Some(cap) = NUMBER_REGEX.captures(s) { 23 | if let Some(m) = cap.get(0) { 24 | return Some(&s[m.start()..m.end()]); 25 | } 26 | } 27 | None 28 | } 29 | CollectType::IdCard => todo!(), 30 | CollectType::CustomizeRegex(regex) => { 31 | if let Ok(re) = Regex::new(regex) { 32 | if let Some(m) = re.find(s) { 33 | return Some(&s[m.start()..m.end()]); 34 | } 35 | } 36 | None 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/assets/flags/cn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/asset.txt: -------------------------------------------------------------------------------- 1 | [ 2 | (include_bytes!(r#"..\resources\assets/assets\DialogFlowAiSDK.min.js.gz"#), "text/javascript"), 3 | (include_bytes!(r#"..\resources\assets/assets\inbound-bot-PJJg_rST.png.gz"#), ""), 4 | (include_bytes!(r#"..\resources\assets/assets\index-BgHOwAJS.js.gz"#), "text/javascript"), 5 | (include_bytes!(r#"..\resources\assets/assets\index-C1HBSe1j.css.gz"#), "text/css"), 6 | (include_bytes!(r#"..\resources\assets/assets\index-CBXDnolR.css.gz"#), "text/css"), 7 | (include_bytes!(r#"..\resources\assets/assets\index-HTbpkmSB.js.gz"#), "text/javascript"), 8 | (include_bytes!(r#"..\resources\assets/assets\outbound-bot-EmsLuWRN.png.gz"#), ""), 9 | (include_bytes!(r#"..\resources\assets/assets\text-bot-CWb_Poym.png.gz"#), ""), 10 | (include_bytes!(r#"..\resources\assets/assets\usedByDialogNodeTextGeneration-DrFqkTqi.png.gz"#), ""), 11 | (include_bytes!(r#"..\resources\assets/assets\usedByDialogNodeTextGeneration-thumbnail-C1iQCVQO.png.gz"#), ""), 12 | (include_bytes!(r#"..\resources\assets/assets\usedByLlmChatNode-Bv2Fg5P7.png.gz"#), ""), 13 | (include_bytes!(r#"..\resources\assets/assets\usedBySentenceEmbedding-Dmju1hVB.png.gz"#), ""), 14 | (include_bytes!(r#"..\resources\assets/assets\usedBySentenceEmbedding-thumbnail-DVXz_sh0.png.gz"#), ""), 15 | (include_bytes!(r#"..\resources\assets/favicon.ico.gz"#), "image/x-icon"), 16 | (include_bytes!(r#"..\resources\assets/index.html.gz"#), "text/html; charset=utf-8"), 17 | ] 18 | -------------------------------------------------------------------------------- /src/flow/rt/facade.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{LazyLock, Mutex}; 3 | 4 | use axum::Json; 5 | use axum::response::IntoResponse; 6 | use tokio::sync::mpsc::Sender; 7 | 8 | use super::dto::Request; 9 | use super::executor; 10 | use crate::result::Result; 11 | use crate::web::server::to_res2; 12 | 13 | static ANSWER_SSE_SESSIONS: LazyLock>>> = 14 | LazyLock::new(|| Mutex::new(HashMap::with_capacity(128))); 15 | 16 | pub(crate) async fn answer(Json(mut req): Json) -> impl IntoResponse { 17 | let now = std::time::Instant::now(); 18 | let r = executor::process(&mut req).await; 19 | // println!("exec used time:{:?}", now.elapsed()); 20 | let res = to_res2(r); 21 | log::info!("Response used time:{:?}", now.elapsed()); 22 | res 23 | } 24 | 25 | pub(crate) async fn answer_sse(Json(req): Json) -> impl IntoResponse { 26 | let now = std::time::Instant::now(); 27 | let (s, r) = tokio::sync::mpsc::channel::(1); 28 | let mut l = ANSWER_SSE_SESSIONS.lock().unwrap(); 29 | l.insert(String::new(), s); 30 | log::info!("Response used time:{:?}", now.elapsed()); 31 | "" 32 | } 33 | 34 | pub(super) fn get_sender(session_id: &str) -> Result>> { 35 | let l = ANSWER_SSE_SESSIONS.lock()?; 36 | if l.contains_key(session_id) { 37 | let s = l.get(session_id).unwrap(); 38 | return Ok(Some(s.clone())); 39 | } 40 | Ok(None) 41 | } 42 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use env_logger::{Builder as LoggerBuilder, Target}; 4 | // use jieba_rs::Jieba; 5 | // use simsearch::{SearchOptions, SimSearch}; 6 | // use strsim::damerau_levenshtein as a; 7 | // use textdistance::nstr::damerau_levenshtein; 8 | use tokio::runtime::Builder; 9 | // use triple_accel::levenshtein::levenshtein_simd_k; 10 | 11 | use dialogflowai::web::server::start_app; 12 | 13 | // Avoid musl's default allocator due to lackluster performance 14 | // https://nickb.dev/blog/default-musl-allocator-considered-harmful-to-performance 15 | #[cfg(target_env = "musl")] 16 | #[global_allocator] 17 | static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; 18 | 19 | fn main() -> Result<(), std::io::Error> { 20 | // dialogflow::web::t1(); 21 | unsafe { 22 | env::set_var("RUST_LOG", "INFO"); 23 | } 24 | let mut builder = LoggerBuilder::from_default_env(); 25 | builder 26 | .target(Target::Stdout) 27 | .format_module_path(false) 28 | .format_target(false) 29 | .format_indent(None); 30 | builder.init(); 31 | 32 | let runtime = Builder::new_multi_thread() 33 | .worker_threads(std::thread::available_parallelism()?.get() + 5usize) 34 | .thread_name("dialogflowai") 35 | .thread_stack_size(3 * 1024 * 1024) 36 | .enable_io() 37 | .enable_time() 38 | .build() 39 | .unwrap(); 40 | 41 | // let (sender, recv) = tokio::sync::oneshot::channel::<()>(); 42 | // runtime.spawn(dialogflow::web::clean_expired_session(recv)); 43 | runtime.block_on(start_app()); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Dialog flow chat bot front end project 2 | 3 | 4 | 5 |  6 | 7 | Setup a condition branch 8 |  9 | 10 | --- 11 | 12 | Hi there. 13 | This is a sub-project of [Dialog Flow AI](https://github.com/dialogflow/dialogflow) 14 | It's built on Vue3 15 | 16 | ## Recommended IDE Setup 17 | 18 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 19 | 20 | ## Project Setup 21 | 22 | ```sh 23 | git clone https://github.com/dialogflowai/dialogflow 24 | cd dialogflow 25 | cd frontend 26 | npm install 27 | ``` 28 | 29 | ### Compile and Hot-Reload for Development 30 | 31 | ```sh 32 | npm run dev 33 | ``` 34 | 35 | On Windows, run the `r` command directly in the project root directory 36 | 37 | ### Compile and Minify for Production 38 | 39 | ```sh 40 | npm run build 41 | ``` 42 | 43 | On Windows, run the `b` command directly in the project root directory 44 | 45 | 48 | -------------------------------------------------------------------------------- /src/db/embedding_lance.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::vec::Vec; 3 | 4 | use arrow_array::array::ArrayRef; 5 | use arrow_array::types::Float32Type; 6 | use arrow_array::{FixedSizeListArray, Int32Array, RecordBatch, RecordBatchIterator}; 7 | use arrow_schema::{DataType, Field, Schema}; 8 | use futures::TryStreamExt; 9 | 10 | use lancedb::arrow::IntoArrow; 11 | use lancedb::connection::Connection; 12 | use lancedb::index::Index; 13 | use lancedb::query::{ExecutableQuery, QueryBase}; 14 | use lancedb::{connect, Error, Result, Table as LanceDbTable}; 15 | 16 | const DATA_ROOT_PATH: &str = "./data/intentev"; 17 | 18 | async fn add(robot_id: &str, v: Vec) -> Result<()> { 19 | let db = connect(DATA_ROOT_PATH).execute().await?; 20 | 21 | // let field_a = Field::new("id", DataType::UInt64, false); 22 | // let field_b = Field::new( 23 | // "vector", 24 | // DataType::FixedSizeList( 25 | // Arc::new(Field::new("item", DataType::Float32, false)), 26 | // v.len() as i32, 27 | // ), 28 | // false, 29 | // ); 30 | let v: ArrayRef = Arc::new(Int32Array::from(v)); 31 | 32 | let record_batch = RecordBatch::try_from_iter(vec![("a", v)]).unwrap(); 33 | 34 | let batches: Vec = vec![record_batch.clone(), record_batch.clone()]; 35 | 36 | let batches = RecordBatchIterator::new(batches.into_iter().map(Ok), record_batch.schema()); 37 | 38 | let table = match db.open_table(robot_id).execute().await { 39 | Ok(t) => t, 40 | Err(Error::TableNotFound { name }) => db.create_table(name, batches).execute().await?, 41 | Err(err) => return Err(err), 42 | }; 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/flow/nodes/EleTipTap.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sdk/javascript/README_zh-CN.md: -------------------------------------------------------------------------------- 1 | [English](./README.md) 2 | 3 | ## 引入SDK 4 | 5 | > 假设本工具的地址是:http://127.0.0.1:12715 6 | 7 | ### script 标签 8 | ```html 9 | 10 | ``` 11 | 12 | ### ES6 模块 13 | ```javascript 14 | import { DialogFlowAiSDK } from 'http://127.0.0.1:12715/assets/DialogFlowAiSDK.min.js' 15 | ``` 16 | 17 | ## 使用 18 | 19 | > 我们以 Vue3 来做示例 20 | 21 | ```javascript 22 | // 是否在等待答案 23 | const waitingResponse = ref(false) 24 | // 用户输入 25 | const userAsk = ref('') 26 | // 聊天记录 27 | const chatRecords = ref([]) 28 | // SDK 变量 29 | let dialogFlowAiSDK = null; 30 | async function dryrun() { 31 | // 如果用户没有输入,就不请求应答接口 32 | if (chatRecords.value.length > 0 && !userAsk.value) 33 | return; 34 | // 检查是否在等待应答接口返回,避免重复应答 35 | if (waitingResponse.value) 36 | return; 37 | waitingResponse.value = true; 38 | if (dialogFlowAiSDK == null) { 39 | dialogFlowAiSDK = new DialogFlowAiSDK({ 40 | // 确保下面的地址 http://127.0.0.1:12715/flow/answer 替换成您真实的地址. 41 | url: 'http://127.0.0.1:12715/flow/answer', // 应答接口地址 42 | robotId: robotId, // 机器人id 43 | mainFlowId: mainFlowId, // 主流程id 44 | chatHistory: chatRecords.value, // 保存聊天记录数据。这些数据,是在SDK里保存的 45 | }); 46 | } 47 | // 发送请求 48 | await dialogFlowAiSDK.sendMessage({ 49 | type: dialogFlowAiSDK.MessageKind.PLAIN_TEXT, 50 | content: userAsk.value, 51 | }); 52 | // 检查是否结束应答 53 | if (dialogFlowAiSDK.chatHasEnded) { 54 | dialogFlowAiSDK.addChat('会话结束', 'terminateText', dialogFlowAiSDK.MessageKind.PLAIN_TEXT, -1); 55 | // 当会话完成,重置SDK 56 | dialogFlowAiSDK = null; 57 | } 58 | waitingResponse.value = false; 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /src/flow/rt/crud.rs: -------------------------------------------------------------------------------- 1 | use redb::{ReadableDatabase, ReadableTable, TableDefinition}; 2 | 3 | use crate::db; 4 | use crate::result::Result; 5 | 6 | fn get_table_name(main_flow_id: &str) -> String { 7 | format!("RTN{main_flow_id}") 8 | } 9 | 10 | pub(crate) fn get_runtime_node( 11 | main_flow_id: &str, 12 | key: &str, 13 | ) -> Result> { 14 | let table_name = get_table_name(main_flow_id); 15 | let table: TableDefinition<&str, &[u8]> = TableDefinition::new(&table_name); 16 | let read_txn = db::DB.begin_read()?; 17 | let table = read_txn.open_table(table)?; 18 | let record = table.get(key)?; 19 | if let Some(r) = record { 20 | // let json = serde_json::from_str(r.value())?; 21 | let n = crate::flow::rt::node::deser_node(r.value())?; 22 | return Ok(Some(n)); 23 | } 24 | Ok(None) 25 | } 26 | 27 | pub(crate) fn save_runtime_nodes( 28 | main_flow_id: &str, 29 | nodes: Vec<(String, rkyv::util::AlignedVec)>, 30 | ) -> Result<()> { 31 | let table_name = get_table_name(main_flow_id); 32 | let table: TableDefinition<&str, &[u8]> = TableDefinition::new(&table_name); 33 | let write_txn = db::DB.begin_write()?; 34 | // println!("save_runtime_nodes {}", main_flow_id); 35 | { 36 | let mut table = write_txn.open_table(table)?; 37 | for j in nodes.iter() { 38 | table.insert(j.0.as_str(), j.1.as_slice())?; 39 | } 40 | } 41 | write_txn.commit()?; 42 | Ok(()) 43 | } 44 | 45 | pub(crate) fn remove_runtime_nodes(main_flow_id: &str) -> Result<()> { 46 | let table_name = get_table_name(main_flow_id); 47 | let table: TableDefinition<&str, &[u8]> = TableDefinition::new(&table_name); 48 | let write_txn = db::DB.begin_write()?; 49 | let _ = write_txn.delete_table(table)?; 50 | write_txn.commit()?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/external/http/dto.rs: -------------------------------------------------------------------------------- 1 | use std::vec::Vec; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Deserialize, Serialize)] 6 | pub(crate) enum Protocol { 7 | HTTP, 8 | HTTPS, 9 | } 10 | 11 | #[derive(Clone, Deserialize, Serialize)] 12 | pub(crate) enum Method { 13 | GET, 14 | POST, 15 | } 16 | 17 | #[derive(Clone, Deserialize, PartialEq, Eq, Serialize)] 18 | pub(crate) enum PostContentType { 19 | UrlEncoded, 20 | JSON, 21 | } 22 | 23 | #[derive(Clone, Deserialize, Serialize)] 24 | pub(crate) enum ValueSource { 25 | Val, 26 | Var, 27 | } 28 | 29 | #[derive(Clone, Deserialize, Serialize)] 30 | pub(crate) struct HttpReqParam { 31 | pub(crate) name: String, 32 | pub(crate) value: String, 33 | #[serde(rename = "valueSource")] 34 | pub(crate) value_source: ValueSource, 35 | } 36 | 37 | #[derive(Clone, Deserialize, Serialize)] 38 | pub(crate) struct HttpReqInfo { 39 | pub(crate) id: String, 40 | pub(crate) name: String, 41 | pub(crate) description: String, 42 | pub(crate) protocol: Protocol, 43 | pub(crate) method: Method, 44 | pub(crate) address: String, 45 | // #[serde(rename = "timeoutMilliseconds")] 46 | // pub(crate) timeout_milliseconds: u64, 47 | #[serde(rename = "postContentType")] 48 | pub(crate) post_content_type: PostContentType, 49 | pub(crate) headers: Vec, 50 | #[serde(rename = "queryParams")] 51 | pub(crate) query_params: Vec, 52 | #[serde(rename = "formData")] 53 | pub(crate) form_data: Vec, 54 | #[serde(rename = "requestBody")] 55 | pub(crate) request_body: String, 56 | #[serde(rename = "userAgent")] 57 | pub(crate) user_agent: String, 58 | // #[serde(rename = "asyncReq")] 59 | // pub(crate) async_req: bool, 60 | } 61 | 62 | pub(crate) enum ResponseData { 63 | Str(String), 64 | Bin(Vec), 65 | None, 66 | } 67 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dialogflowai", 3 | "version": "1.20.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "compress": "terser src/assets/DialogFlowAiSDK.js -o public/assets/DialogFlowAiSDK.min.js -c -m", 9 | "build": "npm run compress && vite build", 10 | "preview": "vite preview" 11 | }, 12 | "//": { 13 | "dependencies": { 14 | "@element-plus/icons-vue": "^2.3.1", 15 | "element-tiptap-vue3-fixed": "^1.2.3", 16 | "vue-scrollto": "^2.20.0", 17 | "vite-plugin-chunk-split": "^0.5.0", 18 | "@tinymce/tinymce-vue": "^6.0.0", 19 | "vite-plugin-imagemin": "^0.6.1" 20 | } 21 | }, 22 | "dependencies": { 23 | "@antv/x6": "^3.1.0", 24 | "@antv/x6-vue-shape": "^3.0.2", 25 | "@tiptap/extension-blockquote": "^3.11.1", 26 | "@tiptap/extension-bubble-menu": "^3.11.1", 27 | "@tiptap/extension-color": "^3.11.1", 28 | "@tiptap/extension-highlight": "^3.11.1", 29 | "@tiptap/extension-text-align": "^3.11.1", 30 | "@tiptap/extension-text-style": "^3.11.1", 31 | "@tiptap/extension-underline": "^3.11.1", 32 | "@tiptap/pm": "^3.11.1", 33 | "@tiptap/starter-kit": "^3.11.1", 34 | "@tiptap/vue-3": "^3.11.1", 35 | "element-plus": "^2.11.9", 36 | "vue": "^3.5.25", 37 | "vue-i18n": "^11.2.2", 38 | "vue-router": "^4.6.3" 39 | }, 40 | "devDependencies": { 41 | "@element-plus/icons-vue": "^2.3.2", 42 | "@iconify-json/bi": "^1.2.6", 43 | "@iconify-json/clarity": "^1.2.4", 44 | "@iconify-json/ep": "^1.2.3", 45 | "@iconify-json/ic": "^1.2.4", 46 | "@iconify-json/material-symbols": "^1.2.48", 47 | "@iconify-json/ph": "^1.2.2", 48 | "@iconify-json/ri": "^1.2.6", 49 | "@iconify-json/solar": "^1.2.5", 50 | "@vitejs/plugin-vue": "^6.0.2", 51 | "rollup-plugin-visualizer": "^6.0.5", 52 | "sass": "^1.94.2", 53 | "terser": "^5.44.1", 54 | "tslib": "^2.8.1", 55 | "unocss": "^66.5.9", 56 | "unplugin-auto-import": "^20.3.0", 57 | "unplugin-icons": "^22.5.0", 58 | "unplugin-vue-components": "^30.0.0", 59 | "vite": "^7.2.6" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sdk/javascript/README.md: -------------------------------------------------------------------------------- 1 | [中文文档](./README_zh-CN.md) 2 | 3 | ## Import SDK 4 | 5 | > Assume that the address of this tool is: http://127.0.0.1:12715 6 | 7 | ### script tag 8 | ```html 9 | 10 | ``` 11 | 12 | ### ES6 module 13 | ```javascript 14 | import { DialogFlowAiSDK } from 'http://127.0.0.1:12715/assets/DialogFlowAiSDK.min.js' 15 | ``` 16 | 17 | ## How to use 18 | 19 | > Let's take Vue3 as an example 20 | 21 | ```javascript 22 | // Is it waiting for an answer 23 | const waitingResponse = ref(false) 24 | // User input 25 | const userAsk = ref('') 26 | // An array for storing chat records 27 | const chatRecords = ref([]) 28 | // SDK variable 29 | let dialogFlowAiSDK = null; 30 | async function dryrun() { 31 | // If the user has no input, the answer API won't be requested 32 | if (chatRecords.value.length > 0 && !userAsk.value) 33 | return; 34 | // Check if it's waiting for the answer API to return, to avoid duplicate answers 35 | if (waitingResponse.value) 36 | return; 37 | waitingResponse.value = true; 38 | if (dialogFlowAiSDK == null) { 39 | dialogFlowAiSDK = new DialogFlowAiSDK({ 40 | // Make sure to replace http://127.0.0.1:12715/flow/answer with your actual API endpoint. 41 | url: 'http://127.0.0.1:12715/flow/answer', // answer API address 42 | robotId: robotId, // robot id 43 | mainFlowId: mainFlowId, // main flow id 44 | chatHistory: chatRecords.value, // Save chat data. This data, which is saved in the SDK 45 | }); 46 | } 47 | // Sending request 48 | await dialogFlowAiSDK.sendMessage({ 49 | type: dialogFlowAiSDK.MessageKind.PLAIN_TEXT, 50 | content: userAsk.value, 51 | }); 52 | // Check if the answer is finished 53 | if (dialogFlowAiSDK.chatHasEnded) { 54 | dialogFlowAiSDK.addChat('Conversation was over', 'terminateText', dialogFlowAiSDK.MessageKind.PLAIN_TEXT, -1); 55 | // When the session is complete, reset the SDK 56 | dialogFlowAiSDK = null; 57 | } 58 | waitingResponse.value = false; 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /sdk/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.dialogflowai 8 | java-sdk 9 | 1.17.6-SNAPSHOT 10 | 11 | 12 | 11 13 | 11 14 | UTF-8 15 | 2.18.2 16 | 17 | 18 | 19 | 20 | com.fasterxml.jackson.core 21 | jackson-databind 22 | ${fasterxml-jackson.version} 23 | 24 | 25 | com.fasterxml.jackson.core 26 | jackson-annotations 27 | ${fasterxml-jackson.version} 28 | 29 | 30 | com.fasterxml.jackson.core 31 | jackson-core 32 | ${fasterxml-jackson.version} 33 | 34 | 35 | org.slf4j 36 | slf4j-simple 37 | 2.0.16 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | 1.18.36 43 | provided 44 | 45 | 46 | org.junit.jupiter 47 | junit-jupiter 48 | 5.11.4 49 | test 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/ai/crud.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use std::convert::Infallible; 3 | use std::result::Result; 4 | 5 | use axum::body::Bytes; 6 | use axum::response::sse::{Event, KeepAlive, Sse}; 7 | // use crossbeam_channel::bounded; 8 | use futures::future::Either; 9 | use futures::stream::{self, Stream}; 10 | use serde::{Deserialize, Serialize}; 11 | use tokio::sync::mpsc; 12 | use tokio_stream::StreamExt as _; 13 | use tokio_stream::wrappers::ReceiverStream; 14 | 15 | use crate::ai::completion; 16 | 17 | #[derive(Deserialize, Serialize)] 18 | pub(crate) struct Request { 19 | pub(crate) robot_id: String, 20 | pub(crate) prompt: String, 21 | } 22 | 23 | // struct Guard; 24 | // impl Drop for Guard { 25 | // fn drop(&mut self) { 26 | // println!("A SSE connection was dropped!") 27 | // } 28 | // } 29 | 30 | pub(crate) async fn gen_text(bytes: Bytes) -> Sse>> { 31 | let q: Request = serde_json::from_slice(bytes.as_ref()).unwrap(); 32 | // let _guard = Guard; 33 | let stream = if q.robot_id.is_empty() || q.prompt.is_empty() { 34 | Either::Left(stream::once(futures::future::ready( 35 | Ok::(Event::default().data("Invalid robot_id or prompt")), 36 | ))) 37 | } else { 38 | // let (sender, receiver) = bounded::(1); 39 | // Either::Right(stream::once(futures::future::ready(Ok::( 40 | // Event::default().data("Invalid robot_id or prompt") 41 | // )))) 42 | let (sender, receiver) = mpsc::channel::(5); 43 | let stream = ReceiverStream::new(receiver).map(|s| { 44 | // log::info!("Sse sending {s}"); 45 | let event = Event::default().data(s.content); 46 | Ok::(event) 47 | }); 48 | tokio::spawn(async move { 49 | // let borrowed_sender = &sender; 50 | if let Err(e) = completion::completion(&q.robot_id, &q.prompt, sender).await { 51 | log::error!("{:?}", &e); 52 | } 53 | }); 54 | Either::Right(stream) 55 | }; 56 | Sse::new(stream).keep_alive( 57 | KeepAlive::new() 58 | .interval(Duration::from_secs(30)) 59 | .text("keep-alive-text"), 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/intent/dto.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Debug)] 4 | pub(crate) struct IntentFormData { 5 | #[serde(rename = "robotId")] 6 | pub(crate) robot_id: String, 7 | pub(crate) id: String, 8 | pub(crate) data: String, 9 | } 10 | 11 | // #[derive(Clone, Serialize, Deserialize, Debug)] 12 | // pub(crate) struct IntentsData { 13 | // pub(crate) phrase_vec_row_id: i64, 14 | // pub(crate) intents: Vec, 15 | // } 16 | 17 | // static VEC_ROW_ID_LOCKER: Mutex<()> = Mutex::new(()); 18 | 19 | // impl IntentsData { 20 | // pub(crate) fn inc_get_phrase_vec_id(&mut self) -> Result { 21 | // let _l = VEC_ROW_ID_LOCKER.lock()?; 22 | // self.phrase_vec_row_id = self.phrase_vec_row_id + 1; 23 | // Ok(self.phrase_vec_row_id) 24 | // } 25 | // } 26 | 27 | // #[derive(Clone, Serialize, Deserialize, Debug)] 28 | // pub(crate) struct Intent { 29 | // pub(crate) id: String, 30 | // pub(crate) name: String, 31 | // pub(crate) keyword_num: usize, 32 | // pub(crate) regex_num: usize, 33 | // pub(crate) phrase_num: usize, 34 | // } 35 | 36 | // impl Intent { 37 | // pub(crate) fn new(intent_name: &str) -> Self { 38 | // // println!("{}", scru128::new().to_u128()); 39 | // Intent { 40 | // id: scru128::new_string(), 41 | // name: String::from(intent_name), 42 | // keyword_num: 0, 43 | // regex_num: 0, 44 | // phrase_num: 0, 45 | // } 46 | // } 47 | // } 48 | 49 | #[derive(Serialize, Deserialize, Debug)] 50 | pub(crate) struct IntentPhraseData { 51 | pub(crate) id: i64, 52 | pub(crate) phrase: String, 53 | } 54 | 55 | #[derive(Serialize, Deserialize, Debug)] 56 | pub(crate) struct IntentDetail { 57 | pub(crate) intent_id: String, 58 | pub(crate) intent_name: String, 59 | pub(crate) keywords: Vec, 60 | pub(crate) regexes: Vec, 61 | phrase_vec_row_id: i64, 62 | pub(crate) phrases: Vec, 63 | } 64 | 65 | impl IntentDetail { 66 | pub(crate) fn new(intent_name: &str) -> Self { 67 | IntentDetail { 68 | intent_id: scru128::new_string(), 69 | intent_name: String::from(intent_name), 70 | keywords: vec![], 71 | regexes: vec![], 72 | phrase_vec_row_id: 0, 73 | phrases: vec![], 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/external/http/crud.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use axum::Json; 4 | use axum::extract::{Path, Query}; 5 | use axum::response::IntoResponse; 6 | 7 | use super::dto::HttpReqInfo; 8 | use crate::db; 9 | use crate::db_executor; 10 | use crate::result::{Error, Result}; 11 | use crate::web::server::to_res; 12 | 13 | // pub(crate) const TABLE: redb::TableDefinition<&str, &[u8]> = 14 | // redb::TableDefinition::new("externalHttpApis"); 15 | pub(crate) const TABLE_SUFFIX: &str = "externalHttpApis"; 16 | 17 | pub(crate) fn init(robot_id: &str) -> Result<()> { 18 | // db::init_table(TABLE) 19 | db_executor!(db::init_table, robot_id, TABLE_SUFFIX,) 20 | } 21 | 22 | pub(crate) async fn list(Query(q): Query>) -> impl IntoResponse { 23 | // let r: Result> = db::get_all(TABLE); 24 | if let Some(robot_id) = q.get("robotId") { 25 | let r: Result> = db_executor!(db::get_all, &robot_id, TABLE_SUFFIX,); 26 | to_res(r) 27 | } else { 28 | to_res(Err(Error::WithMessage(String::from( 29 | "Parameter: robotId is missing.", 30 | )))) 31 | } 32 | } 33 | 34 | pub(crate) fn get_detail(robot_id: &str, id: &str) -> Result> { 35 | // db::query(TABLE, id) 36 | db_executor!(db::query, robot_id, TABLE_SUFFIX, id) 37 | } 38 | 39 | pub(crate) async fn detail( 40 | Query(q): Query>, 41 | Path(id): Path, 42 | ) -> impl IntoResponse { 43 | if let Some(robot_id) = q.get("robotId") { 44 | let r: Result> = get_detail(robot_id, id.as_str()); 45 | to_res(r) 46 | } else { 47 | to_res(Err(Error::WithMessage(String::from( 48 | "Parameter: robotId is missing.", 49 | )))) 50 | } 51 | } 52 | 53 | pub(crate) async fn save( 54 | Query(q): Query>, 55 | Json(mut params): Json, 56 | ) -> impl IntoResponse { 57 | if let Some(robot_id) = q.get("robotId") { 58 | if params.id.is_empty() || params.id.eq("new") { 59 | params.id = scru128::new_string(); 60 | } 61 | let r = db_executor!(db::write, robot_id, TABLE_SUFFIX, ¶ms.id, ¶ms); 62 | to_res(r) 63 | } else { 64 | to_res(Err(Error::WithMessage(String::from( 65 | "Parameter: robotId is missing.", 66 | )))) 67 | } 68 | } 69 | 70 | pub(crate) async fn remove( 71 | Query(q): Query>, 72 | Path(id): Path, 73 | ) -> impl IntoResponse { 74 | if let Some(robot_id) = q.get("robotId") { 75 | let r = db_executor!(db::remove, &robot_id, TABLE_SUFFIX, id.as_str()); 76 | to_res(r) 77 | } else { 78 | to_res(Err(Error::WithMessage(String::from( 79 | "Parameter: robotId is missing.", 80 | )))) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/Main.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai; 2 | 3 | import io.github.dialogflowai.sdk.*; 4 | 5 | import java.io.IOException; 6 | import java.util.List; 7 | import java.util.Scanner; 8 | 9 | public class Main { 10 | public static void main(String[] args) { 11 | try (Scanner scanner = new Scanner(System.in)) { 12 | System.out.println("Robot id:"); 13 | String robotId = scanner.nextLine(); 14 | System.out.println("Main flow id:"); 15 | String mainFlowId = scanner.nextLine(); 16 | RequestData request = RequestData.create(robotId, mainFlowId, null); 17 | RequestHandler requestHandler = new RequestHandler("http://127.0.0.1:12715/flow/answer"); 18 | Response response; 19 | while (true) { 20 | try { 21 | response = requestHandler.req(request, 1000); 22 | } catch (IOException | InterruptedException e) { 23 | System.out.println("Request failed, err: " + e.getMessage()); 24 | response = null; 25 | } 26 | if (response == null) { 27 | System.out.println("Response failed, please try again."); 28 | } else if (response.getStatus() != 200) { 29 | System.out.println("Response failed: " + response.getErr()); 30 | } else if (response.getData() == null) { 31 | System.out.println("Request failed, please try again."); 32 | } else { 33 | ResponseData data = response.getData(); 34 | List answers = data.getAnswers(); 35 | if (answers == null || answers.isEmpty()) 36 | System.out.println("No answer."); 37 | else { 38 | System.out.println(answers.size() == 1 ? "Answer:" : "Answers:"); 39 | for (Answer answer : answers) { 40 | System.out.println(answer.getContent()); 41 | } 42 | } 43 | if (NextAction.TERMINATE.equals(data.getNextAction())) { 44 | System.out.println(); 45 | System.exit(0); 46 | } 47 | if (request.getSessionId() == null || request.getSessionId().isEmpty()) 48 | request.setSessionId(data.getSessionId()); 49 | } 50 | System.out.println("Input your question:"); 51 | request.setUserInput(scanner.nextLine()); 52 | if (request.getUserInput().isEmpty()) 53 | request.setUserInputResult(UserInputResult.TIMEOUT); 54 | else 55 | request.setUserInputResult(UserInputResult.SUCCESSFUL); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ai/token_output_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::result::{Error, Result}; 2 | 3 | pub struct TokenOutputStream { 4 | tokenizer: tokenizers::Tokenizer, 5 | tokens: Vec, 6 | prev_index: usize, 7 | current_index: usize, 8 | } 9 | 10 | impl TokenOutputStream { 11 | pub fn new(tokenizer: tokenizers::Tokenizer) -> Self { 12 | Self { 13 | tokenizer, 14 | tokens: Vec::new(), 15 | prev_index: 0, 16 | current_index: 0, 17 | } 18 | } 19 | 20 | pub fn into_inner(self) -> tokenizers::Tokenizer { 21 | self.tokenizer 22 | } 23 | 24 | fn decode(&self, tokens: &[u32]) -> Result { 25 | match self.tokenizer.decode(tokens, true) { 26 | Ok(str) => Ok(str), 27 | Err(err) => Err(Error::WithMessage(format!("cannot decode: {err}"))), 28 | } 29 | } 30 | 31 | // https://github.com/huggingface/text-generation-inference/blob/5ba53d44a18983a4de32d122f4cb46f4a17d9ef6/server/text_generation_server/models/model.py#L68 32 | pub fn next_token(&mut self, token: u32) -> Result> { 33 | let prev_text = if self.tokens.is_empty() { 34 | String::new() 35 | } else { 36 | let tokens = &self.tokens[self.prev_index..self.current_index]; 37 | self.decode(tokens)? 38 | }; 39 | self.tokens.push(token); 40 | let text = self.decode(&self.tokens[self.prev_index..])?; 41 | if text.len() > prev_text.len() && text.chars().last().unwrap().is_alphanumeric() { 42 | let text = text.split_at(prev_text.len()); 43 | self.prev_index = self.current_index; 44 | self.current_index = self.tokens.len(); 45 | Ok(Some(text.1.to_string())) 46 | } else { 47 | Ok(None) 48 | } 49 | } 50 | 51 | pub fn decode_rest(&self) -> Result> { 52 | let prev_text = if self.tokens.is_empty() { 53 | String::new() 54 | } else { 55 | let tokens = &self.tokens[self.prev_index..self.current_index]; 56 | self.decode(tokens)? 57 | }; 58 | let text = self.decode(&self.tokens[self.prev_index..])?; 59 | if text.len() > prev_text.len() { 60 | let text = text.split_at(prev_text.len()); 61 | Ok(Some(text.1.to_string())) 62 | } else { 63 | Ok(None) 64 | } 65 | } 66 | 67 | pub fn decode_all(&self) -> Result { 68 | self.decode(&self.tokens) 69 | } 70 | 71 | pub fn get_token(&self, token_s: &str) -> Option { 72 | self.tokenizer.get_vocab(true).get(token_s).copied() 73 | } 74 | 75 | pub fn tokenizer(&self) -> &tokenizers::Tokenizer { 76 | &self.tokenizer 77 | } 78 | 79 | pub fn clear(&mut self) { 80 | self.tokens.clear(); 81 | self.prev_index = 0; 82 | self.current_index = 0; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /frontend/src/components/LanguageSwitcher.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ locale === 'zh' ? 'Language' : '语言切换' }} 5 | 6 | 7 | 8 | {{ currentLangLabel }} 9 | 10 | 11 | 12 | 13 | 15 | 16 | {{ lang.label }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 70 | 71 | -------------------------------------------------------------------------------- /sdk/java/src/main/java/io/github/dialogflowai/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.github.dialogflowai; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.github.dialogflowai.sdk.ImportVariable; 6 | import io.github.dialogflowai.sdk.RequestData; 7 | import io.github.dialogflowai.sdk.Response; 8 | import io.github.dialogflowai.sdk.UserInputResult; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.net.URI; 14 | import java.net.http.HttpClient; 15 | import java.net.http.HttpRequest; 16 | import java.net.http.HttpResponse; 17 | import java.time.Duration; 18 | 19 | @Slf4j 20 | public class RequestHandler { 21 | private final HttpClient client; 22 | private final URI endpoint; 23 | private final ObjectMapper mapper; 24 | public RequestHandler(String endpoint) { 25 | this.client = HttpClient.newBuilder() 26 | .version(HttpClient.Version.HTTP_1_1) 27 | .build(); 28 | this.endpoint = URI.create(endpoint); 29 | this.mapper = new ObjectMapper(); 30 | this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 31 | System.setProperty("jdk.httpclient.keepalive.timeout", "1800"); 32 | } 33 | 34 | public Response req(String robotId, String mainFlowId, String userInput) throws IOException, InterruptedException { 35 | return req(userInput, robotId, mainFlowId, 2500); 36 | } 37 | 38 | public Response req(String robotId, String mainFlowId, String userInput, int timeoutMillis) throws IOException, InterruptedException { 39 | RequestData requestData = RequestData.create(robotId, mainFlowId, userInput); 40 | return req(requestData, timeoutMillis); 41 | } 42 | 43 | public Response req(RequestData requestData, int timeoutMillis) throws IOException, InterruptedException { 44 | // Check default value of request params 45 | if (requestData.getUserInput() == null) 46 | requestData.setUserInput(""); 47 | if (requestData.getUserInputResult() == null) 48 | requestData.setUserInputResult(UserInputResult.SUCCESSFUL); 49 | // End 50 | return post(requestData, timeoutMillis); 51 | } 52 | 53 | private Response post(RequestData requestData, int timeoutMillis) throws IOException, InterruptedException { 54 | HttpRequest request = HttpRequest.newBuilder() 55 | .uri(endpoint) 56 | .timeout(Duration.ofMillis(timeoutMillis)) 57 | .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(requestData))) 58 | .header("Content-Type", "application/json") 59 | .build(); 60 | 61 | // Send request and handle response 62 | HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); 63 | try (InputStream body = response.body()) { 64 | return mapper.readValue(body, Response.class); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /frontend/public/assets/DialogFlowAiSDK.min.js: -------------------------------------------------------------------------------- 1 | "use strict";class DialogFlowAiSDK{constructor(t){this.url=t.url,this.timeoutSec=t.timeoutSec||10,this.mainFlowId=t.mainFlowId,this.robotId=t.robotId,this.chatHistory=t.chatHistory||[],this.version=1,this.sessionId=t.sessionId||this.newSessionId(),this.importVariables=[],this.chatHasEnded=!1}VarKind=Object.freeze({STRING:"String",NUMBER:"Number"});UserInputResult=Object.freeze({SUCCESSFUL:"Successful",TIMEOUT:"Timeout"});MessageKind=Object.freeze({PLAIN_TEXT:"PlainText",RICH_TEXT:"RichText",IMAGE:"Timeout"});newSessionId(){return Date.now().toString()+Math.random().toString(16)}genRequestBody(t,e){const n=this;return{robotId:n.robotId,mainFlowId:n.mainFlowId,sessionId:this.sessionId,userInputResult:null,userInput:t||"",importVariables:n.importVariables.splice(0,n.importVariables.length),userInputIntent:e}}appendImportVariable(t,e,n){if(!this.VarKind[n])throw new Error(`Invalid variable kind: ${n}`);const s={varName:t,varType:n,varVal:e};this.importVariables.push(s)}correctData(t){if(!this.url)throw new Error("Missing parameter: url");if(!this.robotId)throw new Error("Missing parameter: robotId");if(!this.mainFlowId)throw new Error("Missing parameter: mainFlowId");if(null==t.sessionId)throw new Error("Missing parameter: sessionId");null==t.userInput&&(t.userInput=""),null==t.userInputResult&&(t.userInputResult=0==this.chatHistory.length||t.userInput.length>0?this.UserInputResult.SUCCESSFUL:this.UserInputResult.FAILED),null==t.importVariables&&(t.importVariables=[]),null!=t.userInputIntent&&""==t.userInputIntent&&(t.userInputIntent=null)}addChat(t,e,n,s){if(s&&s>-1){if(!(s>=this.chatHistory.length))return this.chatHistory[s].text+=t,s;for(let t=this.chatHistory.length;t{if(t.trim().length>0){console.log("line:",t);const n=JSON.parse("{"+t+"}");if(Object.hasOwn(n,"contentSeq")&&null!==n.contentSeq)e.appendAnswers({status:200,data:{answers:[{content:n.content}]}},n.contentSeq);else{const t=e.appendAnswers({status:200,data:JSON.parse(n.content)},l);l=t.chatIdx}}}),({value:a,done:i}=await r.read()))}else{const t=await o.json();console.log("Response data:",t),e.appendAnswers(t,-1)}}}export{DialogFlowAiSDK}; -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](./README.md) 2 | 3 | 欢迎给本项目,或者给[Github上的项目](https://github.com/dialogflowai/dialogflow) ✨**Star**🎇! 4 | 5 | # Dialog flow AI 6 | **只有一个执行文件** 的AI工具,不用安装任何依赖就可以**直接使用**, 它包含了意图识别,AI模型管理,可视化的流程编辑器,和应答逻辑. 7 | **0安装**,无需安装类似Redis、ElasticSearch等中间件. 8 | 9 | 10 |  11 | 12 |  13 | 14 |  15 | 16 | # ✨ 关键特性 17 | * 🛒 **轻量级** 只有一个执行文件, 可以在没有GPU的笔记本上平滑的执行 (数据文件会在运行期动态的生成). 18 | * 🐱🏍 **AI 驱动** 集成了 `Huggingface 本地模型 (Llama, Phi-3, Gemma, Multilingual E5, MiniLM L6v2, NomicEmbedTextV1_5 等其它模型)`, `Ollama` 和 `OpenAI`, 可以用于 `流程聊天`, `答案节点文本生成` 以及 `意图识别` 等. 19 | * 🚀 **快速** 使用`Rust`和`Vue`构建. 20 | * 😀 **简单** 通过使用可视化的流程编辑器,只需要用鼠标拖拽几个不同类型的节点, 即可创建一个简单的对话机器人. 21 | * 🔐 **安全** 100% 开源, 所有运行时的数据, 都保存在本地 (使用 `OpenAI API` 可能会暴露一些数据). 22 | 23 | # 现在就尝试一下! 24 | * 🐋 **Docker** 我们提供了一个`Docker`镜像: [dialogflowai/dialogflow](https://hub.docker.com/r/dialogflowai/dialogflow/) 25 | * 💻 **可直接执行的发布版本**, 请通过发布页: [点击这里](https://github.com/dialogflowai/dialogflow/releases) , 根据不同的平台下载(支持:Windows、Linux、macOS) 26 | 27 | > 默认情况下, 应用会监听: `127.0.0.1:12715`, 你可以使用 `-ip` 参数和 `-port` 参数, 来指定新的监听地址和端口, 例如: `dialogflow -ip 0.0.0.0 -port 8888` 28 | 29 | 33 | 34 | # 查看详细介绍, 了解更多信息 35 | [https://dialogflowai.github.io/#/?lang=zh](https://dialogflowai.github.io/#/?lang=zh) 36 | 37 | # 功能节点列表 38 | |节点|名称| 39 | |----|----| 40 | ||对话答案节点| 41 | ||大模型聊天节点| 42 | ||知识库答案节点| 43 | ||条件节点| 44 | ||跳转节点| 45 | ||信息收集节点| 46 | ||请求外部接口节点| 47 | ||邮件发送节点| 48 | ||流程结束节点| 49 | 50 | 通过使用上面不同的节点来排列和组合, 就可以创建解决不同场景问题的机器人. 51 | 52 | # 截图 53 |  54 | 55 |  56 | 57 | ### 体验演示机器人 58 |  59 | 60 | ### 创建一个条件节点的分支 61 |  62 | 63 | ### 对话答案节点的自动文本生成 64 | 65 |  66 | 67 | ### 测试一个对话机器人 68 |  69 | 70 | 71 | # 现在就开始使用 72 | 73 | ### Docker 镜像 74 | 1. docker pull dialogflowai/demo 75 | 2. docker run -dp 127.0.0.1:12715:12715 --name dialogflowdemo dialogflowai/demo 76 | 3. 打开浏览器并访问: http://127.0.0.1:12715/ 打开应用界面 77 | 78 | ### 发布版本 79 | 1. 从 [Github release page](https://github.com/dialogflowai/dialogflow/releases), 选择不同系统并下载. 80 | 1. 直接执行, 或者使用 `-ip` 和 `-port` 修改监听地址, 如: `dialogflow -ip 0.0.0.0 -port 8888`. 81 | 1. 打开浏览器并访问 http://localhost:12715 (默认) 或 http://`新的IP`:`新的端口` 打开应用界面 82 | 1. 进入一个机器人 83 | 2. 创建一个对话流程,并点击名称进入编辑器 84 | 1. 构建属于自己的机器人 85 | 1. 测试 86 | -------------------------------------------------------------------------------- /src/resources/assets/assets/DialogFlowAiSDK.min.js: -------------------------------------------------------------------------------- 1 | "use strict";class DialogFlowAiSDK{constructor(t){this.url=t.url,this.timeoutSec=t.timeoutSec||10,this.mainFlowId=t.mainFlowId,this.robotId=t.robotId,this.chatHistory=t.chatHistory||[],this.version=1,this.sessionId=t.sessionId||this.newSessionId(),this.importVariables=[],this.chatHasEnded=!1}VarKind=Object.freeze({STRING:"String",NUMBER:"Number"});UserInputResult=Object.freeze({SUCCESSFUL:"Successful",TIMEOUT:"Timeout"});MessageKind=Object.freeze({PLAIN_TEXT:"PlainText",RICH_TEXT:"RichText",IMAGE:"Timeout"});newSessionId(){return Date.now().toString()+Math.random().toString(16)}genRequestBody(t,e){const n=this;return{robotId:n.robotId,mainFlowId:n.mainFlowId,sessionId:this.sessionId,userInputResult:null,userInput:t||"",importVariables:n.importVariables.splice(0,n.importVariables.length),userInputIntent:e}}appendImportVariable(t,e,n){if(!this.VarKind[n])throw new Error(`Invalid variable kind: ${n}`);const s={varName:t,varType:n,varVal:e};this.importVariables.push(s)}correctData(t){if(!this.url)throw new Error("Missing parameter: url");if(!this.robotId)throw new Error("Missing parameter: robotId");if(!this.mainFlowId)throw new Error("Missing parameter: mainFlowId");if(null==t.sessionId)throw new Error("Missing parameter: sessionId");null==t.userInput&&(t.userInput=""),null==t.userInputResult&&(t.userInputResult=0==this.chatHistory.length||t.userInput.length>0?this.UserInputResult.SUCCESSFUL:this.UserInputResult.FAILED),null==t.importVariables&&(t.importVariables=[]),null!=t.userInputIntent&&""==t.userInputIntent&&(t.userInputIntent=null)}addChat(t,e,n,s){if(s&&s>-1){if(!(s>=this.chatHistory.length))return this.chatHistory[s].text+=t,s;for(let t=this.chatHistory.length;t{if(t.trim().length>0){console.log("line:",t);const n=JSON.parse("{"+t+"}");if(Object.hasOwn(n,"contentSeq")&&null!==n.contentSeq)e.appendAnswers({status:200,data:{answers:[{content:n.content}]}},n.contentSeq);else{const t=e.appendAnswers({status:200,data:JSON.parse(n.content)},l);l=t.chatIdx}}}),({value:a,done:i}=await r.read()))}else{const t=await o.json();console.log("Response data:",t),e.appendAnswers(t,-1)}}}export{DialogFlowAiSDK}; -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import path from 'path' 4 | import { defineConfig, loadEnv } from 'vite' 5 | import vue from '@vitejs/plugin-vue' 6 | 7 | import UnoCSS from 'unocss/vite' 8 | 9 | import Icons from 'unplugin-icons/vite' 10 | import IconsResolver from 'unplugin-icons/resolver' 11 | import AutoImport from 'unplugin-auto-import/vite' 12 | import Components from 'unplugin-vue-components/vite' 13 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 14 | import { visualizer } from "rollup-plugin-visualizer"; 15 | // import viteImagemin from 'vite-plugin-imagemin' 16 | // import { chunkSplitPlugin } from 'vite-plugin-chunk-split' 17 | 18 | const pathSrc = path.resolve(__dirname, 'src') 19 | 20 | function manualChunks(id) { 21 | console.log(id); 22 | if (id.includes('antv')) { 23 | return 'antv'; 24 | } else { 25 | return 'index'; 26 | } 27 | } 28 | 29 | // https://vitejs.dev/config/ 30 | export default ({ command, mode }) => { 31 | return defineConfig({ 32 | // resolve: { 33 | // alias: { 34 | // '@': pathSrc, 35 | // }, 36 | // }, 37 | resolve: { 38 | alias: { 39 | '@': fileURLToPath(new URL('./src', import.meta.url)) 40 | } 41 | }, 42 | plugins: [ 43 | UnoCSS(), 44 | vue(), 45 | // visualizer(), 46 | AutoImport({ 47 | resolvers: [ 48 | ElementPlusResolver(), 49 | IconsResolver({ 50 | prefix: 'Icon', 51 | }), 52 | ], 53 | dts: path.resolve(pathSrc, 'auto-imports.d.ts'), 54 | }), 55 | Components({ 56 | resolvers: [ 57 | ElementPlusResolver(), 58 | IconsResolver({ 59 | enabledCollections: ['ep'], 60 | }), 61 | ], 62 | dts: path.resolve(pathSrc, 'components.d.ts'), 63 | }), 64 | Icons({ 65 | autoInstall: true, 66 | }), 67 | // viteImagemin({ 68 | // gifsicle: { 69 | // optimizationLevel: 7, 70 | // interlaced: false, 71 | // }, 72 | // optipng: { 73 | // optimizationLevel: 7, 74 | // }, 75 | // mozjpeg: { 76 | // quality: 70, 77 | // }, 78 | // pngquant: { 79 | // quality: [0.8, 0.9], 80 | // speed: 4, 81 | // }, 82 | // svgo: { 83 | // plugins: [ 84 | // { 85 | // name: 'removeViewBox', 86 | // }, 87 | // { 88 | // name: 'removeEmptyAttrs', 89 | // active: false, 90 | // }, 91 | // ], 92 | // }, 93 | // }), 94 | // chunkSplitPlugin({ 95 | // customSplitting: { 96 | // 'canvas': [/\/node_modules\/@antv\//] 97 | // } 98 | // }), 99 | ], 100 | define: { 101 | // 'process.env': env, 102 | }, 103 | build: { 104 | rollupOptions: { 105 | output: { 106 | // dir: 'dist', 107 | // entryFileNames: 'index.js', 108 | // manualChunks, 109 | // chunkFilename: 'vendor_locale_[name].js', 110 | // manualChunks: { 111 | // 'canvas': ['@antv/x6', '@antv/x6-vue-shape'] 112 | // } 113 | }, 114 | }, 115 | minify: 'esbuild', // <-- add 116 | terserOptions: { 117 | compress: { 118 | drop_console: true, 119 | drop_debugger: true, 120 | }, 121 | }, 122 | }, 123 | esbuild: { 124 | drop: ['console', 'debugger'], 125 | }, 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /frontend/src/components/external/HttpApiList.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 73 | {{ t('eApi.title') }} 74 | {{ t('eApi.add') }} 75 | 76 | Now you can not only send data to the outside, but also get data from the outside and save it in variables 77 | by setting value source to a HTTP API. 78 | {{ t('var.add') }} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Edit 87 | | 88 | 89 | Delete 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /frontend/src/components/Demos.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | {{ $t("home.demo") }}: 47 | 48 | 49 | 50 | {{ $t("home.demo1") }} 55 | 56 | 57 | {{ $t("home.demo2") }} 67 | 68 | 69 | {{ $t("home.demo3") }} 79 | 80 | 81 | 82 | 83 | > 1. 84 | {{ $t("home.demo1") }} 89 | > 2. 90 | {{ $t("home.demo2") }} 100 | > 3. 101 | {{ $t("home.demo3") }} 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/kb/crud.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::Path; 3 | 4 | use axum::{ 5 | Json, 6 | extract::{Multipart, Query}, 7 | response::IntoResponse, 8 | }; 9 | 10 | use super::doc; 11 | use super::dto::{DocData, QuestionAnswerPair}; 12 | use crate::result::{Error, Result}; 13 | use crate::robot::dto::RobotQuery; 14 | use crate::web::server::to_res; 15 | 16 | pub(crate) async fn list_doc(Query(q): Query) -> impl IntoResponse { 17 | let r = super::doc::list(&q.robot_id).await; 18 | to_res(r) 19 | } 20 | 21 | pub(crate) async fn upload_doc( 22 | Query(q): Query, 23 | multipart: Multipart, 24 | ) -> impl IntoResponse { 25 | let r = save_doc(&q.robot_id, multipart).await; 26 | // if r.is_err() { 27 | // return to_res(r); 28 | // } 29 | to_res(r) 30 | } 31 | 32 | async fn save_doc(robot_id: &str, mut multipart: Multipart) -> Result { 33 | let p = Path::new(".") 34 | .join("data") 35 | .join(robot_id) 36 | .join("kb") 37 | .join("docs") 38 | .join("upload"); 39 | if !p.exists() { 40 | std::fs::create_dir_all(&p)?; 41 | } 42 | loop { 43 | let field = multipart.next_field().await?; 44 | if field.is_none() { 45 | return Err(Error::WithMessage(String::from("File not found."))); 46 | } 47 | let field = field.unwrap(); 48 | let Some(file_name) = field.file_name() else { 49 | return Err(Error::WithMessage(String::from("File name is missing."))); 50 | }; 51 | let file_name = file_name.to_string(); 52 | let Some(content_type) = field.content_type() else { 53 | return Err(Error::WithMessage(String::from("Content type is missing."))); 54 | }; 55 | let content_type = content_type.to_string(); 56 | let data = field.bytes().await?; 57 | 58 | log::info!( 59 | "Length of `{file_name}`: `{content_type}` is {} bytes", 60 | data.len() 61 | ); 62 | 63 | let text = match content_type.as_str() { 64 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => { 65 | doc::parse_docx(data.to_vec())? 66 | } 67 | _ => return Err(Error::WithMessage(String::from("Unsupported format"))), 68 | }; 69 | log::info!("Extract text: {text}"); 70 | super::doc::save(robot_id, &file_name, data.len(), &text).await?; 71 | return Ok(text); 72 | } 73 | } 74 | 75 | pub(crate) async fn update_doc( 76 | Query(q): Query, 77 | Json(doc): Json, 78 | ) -> impl IntoResponse { 79 | let r = super::doc::update(&q.robot_id, doc.id, &doc.doc_content).await; 80 | to_res(r) 81 | } 82 | 83 | #[axum::debug_handler] 84 | pub(crate) async fn delete_doc( 85 | Query(q): Query, 86 | Json(doc): Json, 87 | ) -> impl IntoResponse { 88 | let r = super::doc::delete(&q.robot_id, doc.id).await; 89 | to_res(r) 90 | } 91 | 92 | pub(crate) async fn list_qa(Query(q): Query) -> impl IntoResponse { 93 | let r = super::qa::list(&q.robot_id).await; 94 | to_res(r) 95 | } 96 | 97 | pub(crate) async fn save_qa( 98 | Query(q): Query, 99 | Json(d): Json, 100 | ) -> impl IntoResponse { 101 | let r = super::qa::save(&q.robot_id, d).await; 102 | // let r = sqlite_trans!(super::qa::add, &q.robot_id, d).await; 103 | to_res(r) 104 | } 105 | 106 | pub(crate) async fn delete_qa( 107 | Query(q): Query, 108 | Json(d): Json, 109 | ) -> impl IntoResponse { 110 | let r = super::qa::delete(&q.robot_id, d).await; 111 | to_res(r) 112 | } 113 | 114 | pub(crate) async fn qa_dryrun(Query(q): Query>) -> impl IntoResponse { 115 | let r = q.get("robotId"); 116 | let t = q.get("text"); 117 | if r.is_none() || t.is_none() { 118 | let res = Err(Error::WithMessage(String::from( 119 | "robotId or text was missing.", 120 | ))); 121 | return to_res(res); 122 | } 123 | let r = super::qa::retrieve_answer(r.unwrap(), t.unwrap()).await; 124 | to_res(r) 125 | } 126 | -------------------------------------------------------------------------------- /frontend/src/components/robot/RobotFrame.vue: -------------------------------------------------------------------------------- 1 | 18 | 29 | 30 | 31 | 32 | 33 | {{ isCollapse ? '>>>' : 34 | '<<<' }} 35 | 36 | 38 | 39 | 40 | 41 | 42 | {{ t('menu.home') }} 43 | 44 | 45 | 46 | 47 | 48 | {{ t('menu.thisRobot') }} 49 | 50 | 51 | 52 | 53 | 54 | {{ t('menu.dialogFlows') }} 55 | 56 | 57 | 58 | 59 | 60 | 61 | {{ t('menu.kb') }} 62 | 63 | {{ t('menu.qa') }} 64 | {{ t('menu.doc') }} 65 | 66 | 67 | 68 | 69 | 70 | {{ t('menu.intents') }} 71 | 72 | 73 | 74 | 75 | 76 | {{ t('menu.vars') }} 77 | 78 | 79 | 80 | 81 | 82 | {{ t('menu.eApi') }} 83 | 84 | 85 | 86 | 87 | 88 | {{ t('menu.rs') }} 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [简体中文](./README_zh-CN.md) | English 2 | 3 | # Dialog flow AI 4 | **only ONE executable file**, you can use it directly, including intent detection, AI management, a visual process editor and a response system. 5 | **ZERO installation**, no need to install any middleware such as Redis, Elastic Search, etc. 6 | [](https://deepwiki.com/dialogflowai/dialogflow) 7 | 8 |  9 | 10 |  11 | 12 |  13 | 14 | # ✨ Features 15 | * 🛒 **Light** Only ONE executable file, it can run smoothly on laptops without GPUs (data files will be created at runtime automatically). 16 | * 🐱🏍 **AI powered** Integrated `Huggingface local models (Llama, Phi-3, Gemma, Multilingual E5, MiniLM L6v2, NomicEmbedTextV1_5, etc.)`, `Ollama` and `OpenAI`, this can be used for `Chat`, `Text generation` and `Intent detection`. 17 | * 🚀 **Fast** Built on Rust and Vue3. 18 | * 😀 **Simple** Use the mouse to drag and drop with our intuitive node-based editor. 19 | * 🔐 **Safe** 100% open source, all runtime data is saved locally (Using `OpenAI API` may expose some data). 20 | 21 | # Give it a try! 22 | * 🐋 **Docker** We provided an image on Docker Hub at [dialogflowai/dialogflow](https://hub.docker.com/r/dialogflowai/dialogflow/) 23 | * 💻 **Binary releases**, please check [here](https://github.com/dialogflowai/dialogflow/releases) 24 | 25 | > By default application will listen to `127.0.0.1:12715`, you can use `-ip` and `-port` specify new value, e.g.: `dialogflow -ip 0.0.0.0 -port 8888` 26 | 27 | 31 | 32 | # Check out introduction page 33 | [https://dialogflowai.github.io/](https://dialogflowai.github.io/) 34 | 35 | # Function nodes 36 | |Node|Name| 37 | |----|----| 38 | ||Dialog Node| 39 | ||Large language model chat node| 40 | ||Knowledge base answer node| 41 | ||Conditions node| 42 | ||Goto node| 43 | ||Collect node| 44 | ||External HTTP node| 45 | ||Send email node| 46 | ||The end node| 47 | 48 | Using the different nodes above, to arrange and combine, you can get a conversational bot that can handle problems in different scenarios. 49 | 50 | # Screenshots 51 |  52 | 53 |  54 | 55 | ### Trying a demo dialog flow 56 |  57 | 58 | ### Setup a condition branch 59 |  60 | 61 | ### Text generation 62 | 63 |  64 | 65 | ### Testing a dialog flow 66 |  67 | 68 | 69 | # Get started 70 | 71 | ### Docker image 72 | 1. docker pull dialogflowai/demo 73 | 2. docker run -dp 127.0.0.1:12715:12715 --name dialogflowdemo dialogflowai/demo 74 | 3. Open your browser and visit: http://127.0.0.1:12715/ 75 | 76 | ### Binary release 77 | 1. From [Github release page](https://github.com/dialogflowai/dialogflow/releases), depending on the operating system, download the application. 78 | 1. Run it directly, or use the `-ip` and `-port` parameters to perform the listening IP address and port, e.g.: `dialogflow -ip 0.0.0.0 -port 8888`. 79 | 1. Open your browser and visit http://localhost:12715 (by default) or http://`new IP`:`new port` to see the application in action 80 | 1. Add a main flow and click its name into it 81 | 1. Create dialog flow by dragging and drop nodes onto canvas 82 | 1. Test it 83 | -------------------------------------------------------------------------------- /src/flow/rt/executor.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use regex::Regex; 4 | 5 | use super::context::Context; 6 | use super::dto::{Request, ResponseChannelWrapper, ResponseData}; 7 | use crate::ai::completion::Prompt; 8 | use crate::flow::rt::dto::{StreamingResponseData, UserInputResult}; 9 | use crate::flow::rt::node::RuntimeNode; 10 | use crate::intent::detector; 11 | use crate::result::{Error, Result}; 12 | 13 | pub(crate) static HTML_TAG_REGEX: LazyLock = 14 | LazyLock::new(|| Regex::new(r"<[^>]+>").unwrap()); 15 | 16 | pub(in crate::flow::rt) async fn process( 17 | req: &mut Request, 18 | ) -> Result<( 19 | ResponseData, 20 | Option>, 21 | )> { 22 | // log::info!("user input: {}", &req.user_input); 23 | // let now = std::time::Instant::now(); 24 | if req.session_id.is_none() || req.session_id.as_ref().unwrap().is_empty() { 25 | req.session_id = Some(scru128::new_string()); 26 | } 27 | let mut ctx = Context::get(&req.robot_id, req.session_id.as_ref().unwrap()); 28 | // log::info!("get ctx {:?}", now.elapsed()); 29 | // let now = std::time::Instant::now(); 30 | if ctx.no_node() { 31 | if ctx.main_flow_id.is_empty() { 32 | ctx.main_flow_id.push_str(&req.main_flow_id); 33 | } 34 | ctx.add_node(&req.main_flow_id); 35 | } 36 | // log::info!("add_node time {:?}", now.elapsed()); 37 | // let now = std::time::Instant::now(); 38 | if req.user_input_intent.is_none() 39 | && req.user_input_result == UserInputResult::Successful 40 | && !req.user_input.is_empty() 41 | { 42 | req.user_input_intent = detector::detect(&req.robot_id, &req.user_input).await?; 43 | // println!("{:?}", req.user_input_intent); 44 | } 45 | // log::info!("Intent detection took {:?}", now.elapsed()); 46 | if req.import_variables.is_some() { 47 | let import_variables = Option::take(&mut req.import_variables); 48 | let mut import_variables = import_variables.unwrap(); 49 | for v in import_variables.iter_mut() { 50 | let k = std::mem::take(&mut v.var_name); 51 | let v = crate::variable::dto::VariableValue::new(&v.var_val, &v.var_type); 52 | ctx.vars.insert(k, v); 53 | } 54 | } 55 | // println!("intent detect {:?}", now.elapsed()); 56 | // let now = std::time::Instant::now(); 57 | ctx.chat_history.push(Prompt { 58 | role: String::from("user"), 59 | content: HTML_TAG_REGEX.replace_all(&req.user_input, "").to_string(), 60 | }); 61 | let r = exec(req, &mut ctx).await; 62 | if r.is_ok() { 63 | let (res, _receiver) = r.as_ref().unwrap(); 64 | if !res.answers.is_empty() { 65 | for a in res.answers.iter() { 66 | ctx.chat_history.push(Prompt { 67 | role: String::from("assistant"), 68 | content: HTML_TAG_REGEX.replace_all(&a.content, "").to_string(), 69 | }); 70 | } 71 | } 72 | } 73 | // println!("exec {:?}", now.elapsed()); 74 | // let now = std::time::Instant::now(); 75 | ctx.save()?; 76 | // log::info!("ctx save time {:?}", now.elapsed()); 77 | r 78 | } 79 | 80 | pub(in crate::flow::rt) async fn exec( 81 | req: &Request, 82 | ctx: &mut Context, 83 | ) -> Result<( 84 | ResponseData, 85 | Option>, 86 | )> { 87 | // let now = std::time::Instant::now(); 88 | let mut response = ResponseData::new(req); 89 | let mut sender_wapper = ResponseChannelWrapper { 90 | sender: None, 91 | receiver: None, 92 | }; 93 | for _i in 0..100 { 94 | // let now = std::time::Instant::now(); 95 | if let Some(mut n) = ctx.pop_node() { 96 | // println!("pop node {:?}", now.elapsed()); 97 | let ret = n.exec(req, ctx, &mut response, &mut sender_wapper).await; 98 | // println!("node exec {:?}", now.elapsed()); 99 | if ret { 100 | // log::info!("exec time {:?}", now.elapsed()); 101 | return Ok((response, sender_wapper.receiver)); 102 | } 103 | } else { 104 | return Ok((response, sender_wapper.receiver)); 105 | } 106 | } 107 | let m = if *crate::web::server::IS_EN { 108 | "Too many executions, please check if the process configuration is correct." 109 | } else { 110 | "执行次数太多,请检查流程配置是否正确。" 111 | }; 112 | Err(Error::WithMessage(String::from(m))) 113 | } 114 | -------------------------------------------------------------------------------- /src/flow/rt/dto.rs: -------------------------------------------------------------------------------- 1 | use std::vec::Vec; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{flow::subflow::dto::NextActionType, variable::dto::SimpleVariable}; 6 | 7 | #[derive(Deserialize, PartialEq, Eq)] 8 | pub(crate) enum UserInputResult { 9 | Successful, 10 | Timeout, 11 | } 12 | 13 | #[derive(Deserialize)] 14 | pub(crate) struct Request { 15 | #[serde(rename = "robotId")] 16 | pub(crate) robot_id: String, 17 | #[serde(rename = "mainFlowId")] 18 | pub(crate) main_flow_id: String, 19 | #[serde(rename = "sessionId")] 20 | pub(crate) session_id: Option, 21 | #[serde(rename = "userInputResult")] 22 | pub(crate) user_input_result: UserInputResult, 23 | #[serde(rename = "userInput")] 24 | pub(crate) user_input: String, 25 | #[serde(rename = "importVariables")] 26 | pub(crate) import_variables: Option>, 27 | #[serde(rename = "userInputIntent")] 28 | pub(crate) user_input_intent: Option, 29 | } 30 | 31 | #[derive(Serialize)] 32 | pub(crate) struct CollectData { 33 | #[serde(rename = "varName")] 34 | pub(crate) var_name: String, 35 | pub(crate) value: String, 36 | } 37 | 38 | #[derive(Clone, Deserialize, Serialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] 39 | #[rkyv(compare(PartialEq))] 40 | pub(crate) enum AnswerContentType { 41 | TextPlain, 42 | TextHtml, 43 | } 44 | 45 | #[derive(Serialize)] 46 | pub(crate) struct AnswerData { 47 | pub(crate) content: String, 48 | #[serde(rename = "contentType")] 49 | pub(crate) content_type: AnswerContentType, 50 | } 51 | 52 | pub(crate) struct ResponseChannelWrapper { 53 | pub(crate) sender: Option>, 54 | pub(crate) receiver: Option>, 55 | } 56 | 57 | impl ResponseChannelWrapper { 58 | pub(crate) fn new(buffer: usize) -> Self { 59 | let (sender, receiver) = tokio::sync::mpsc::channel(buffer); 60 | Self { 61 | sender: Some(sender), 62 | receiver: Some(receiver), 63 | } 64 | } 65 | pub(crate) fn send_response(&self, res: &ResponseData) { 66 | let res_data = serde_json::to_string(res).unwrap(); 67 | log::info!("send response: {}", &res_data); 68 | crate::sse_send!( 69 | self.sender.as_ref().unwrap(), 70 | StreamingResponseData { 71 | content_seq: None, 72 | content: res_data, 73 | } 74 | ); 75 | } 76 | } 77 | 78 | #[derive(Serialize)] 79 | pub(crate) struct StreamingResponseData { 80 | #[serde(rename = "contentSeq")] 81 | pub(crate) content_seq: Option, 82 | pub(crate) content: String, 83 | } 84 | 85 | #[derive(Serialize)] 86 | pub(crate) struct ResponseData { 87 | #[serde(rename = "sessionId")] 88 | pub(crate) session_id: String, 89 | pub(crate) answers: Vec, 90 | #[serde(rename = "collectData")] 91 | pub(crate) collect_data: Vec, 92 | #[serde(rename = "nextAction")] 93 | pub(crate) next_action: NextActionType, 94 | #[serde(rename = "extraData")] 95 | pub(crate) extra_data: ExtraData, 96 | #[serde(rename = "sseReceiverTicket")] 97 | pub(crate) sse_receiver_ticket: String, 98 | } 99 | 100 | impl ResponseData { 101 | pub(crate) fn new(req: &Request) -> Self { 102 | Self { 103 | session_id: req.session_id.as_ref().unwrap().clone(), 104 | answers: Vec::with_capacity(5), 105 | collect_data: Vec::with_capacity(10), 106 | next_action: NextActionType::None, 107 | extra_data: ExtraData { 108 | external_link: String::new(), 109 | }, 110 | sse_receiver_ticket: String::new(), 111 | } 112 | } 113 | // pub(crate) fn new_with_plain_text_answer(a: String) -> Self { 114 | // Self { 115 | // session_id: String::new(), 116 | // answers: vec![AnswerData { 117 | // content: a, 118 | // content_type: AnswerContentType::TextPlain, 119 | // }], 120 | // collect_data: Vec::with_capacity(0), 121 | // next_action: NextActionType::None, 122 | // extra_data: ExtraData { 123 | // external_link: String::new(), 124 | // }, 125 | // sse_receiver_ticket: String::new(), 126 | // } 127 | // } 128 | } 129 | 130 | #[derive(Serialize)] 131 | pub(crate) struct ExtraData { 132 | #[serde(rename = "externalLink")] 133 | pub(crate) external_link: String, 134 | } 135 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dialogflowai" 3 | version = "1.20.0" 4 | edition = "2024" 5 | homepage = "https://dialogflowai.github.io/" 6 | authors = ["dialogflowchatbot "] 7 | 8 | [lib] 9 | name = "dialogflowai" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | # https://github.com/djkoloski/rust_serialization_benchmark 14 | 15 | [dependencies] 16 | # For lancedb 17 | # arrow-array = "52.2.0" 18 | # arrow-schema = "52.2.0" 19 | # lzma-sys = { version = "*", features = ["static"] } 20 | # End 21 | # artful = "0.1.1" 22 | anyhow = "1.0.100" 23 | axum = {version = "0.8.7", features = ["query", "tokio", "macros", "multipart"]} 24 | bigdecimal = "0.4.9" 25 | # bytes = "1.9" 26 | # candle-core = { git = "https://github.com/huggingface/candle.git", version = "0.5.1" } 27 | candle = { version = "0.9.1", package = "candle-core", default-features = false } 28 | # candle = { git = "https://github.com/huggingface/candle.git", package = "candle-core", default-features = false } 29 | candle-nn = "0.9.1" 30 | # candle-nn = { git = "https://github.com/huggingface/candle.git" } 31 | # candle-onnx = "0.8" 32 | # candle-transformers = { git = "https://github.com/huggingface/candle.git" } 33 | candle-transformers = { version = "0.9.1" } 34 | # candle-transformers = { version = "0.9.1", features = ["flash-attn"] } 35 | # crossbeam-channel = "0.5" 36 | frand = "0.10.1" 37 | tokenizers = "0.22.2" 38 | # candle_embed = "0.1" 39 | colored = "3.0.0" 40 | # dashmap = "5.5.1" 41 | # enum_dispatch = "0.3.13" 42 | # erased-serde = "0.4.6" 43 | #fastembed = "3.6" 44 | futures = "0.3.31" 45 | futures-util = "0.3.31" 46 | # hf-hub = { path = "./rslibs/hf-hub", default-features = false, features = ["tokio"] } 47 | itoa = "1.0.15" 48 | # jieba-rs = "0.6.7" 49 | # oasysdb = "0.7.3" 50 | # once_cell = "1.20" 51 | # ort = { version = "=2.0.0-rc.0", default-features = false } 52 | # paste = "1.0" 53 | redb = "3.1.0" 54 | regex = "1.12.2" 55 | reqwest = { version = "0.12.26", default-features = false, features = ["native-tls", "stream"] } 56 | rkyv = {version = "0.8.12", features = ["aligned", "alloc", "bytecheck"]} 57 | scru128 = "3.2.3" 58 | serde = { version = "1.0.228", features = ["derive"] } 59 | serde_json = "1.0.145" 60 | scraper = "0.24.0" 61 | # snmalloc-rs = "0.3.4" # 暂时不支持MUSL 62 | # simd-json = "0.10" 63 | # simsearch = "0.2" 64 | # strsim = "0.10.0" 65 | # textdistance = "1.0.2" 66 | time = { version = "0.3.44", features = ["formatting"] } 67 | tower-http = { version = "0.6.8", features = ["cors", "limit"] } 68 | # typetag = "0.2" 69 | tokio = { version = "1.48.0", features = ["fs", "io-util", "macros", "net", "rt", "rt-multi-thread", "signal", "time", "tracing"] } 70 | tokio-stream = "0.1.17" 71 | # tracing-subscriber = "0.3" 72 | log = "0.4.28" 73 | env_logger = "0.11.8" 74 | lettre = { version = "0.11.19", default-features = false, features = ["builder", "tokio1", "smtp-transport", "tokio1-native-tls", "pool"]} 75 | unicase = "2.8.1" 76 | # sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio", "sqlite", "macros"] } 77 | lopdf = "0.38.0" 78 | # docx-rs = "0.4.17" 79 | # lancedb = "0.13.0" 80 | # libsqlite3-sys = { version = "0.30", features = ["bundled"] } 81 | validator = "0.20" 82 | zip = "6.0.0" 83 | quick-xml = "0.38.4" 84 | # usearch = "2.17" 85 | # cxx = "1.0" 86 | # triple_accel = "0.4.0" 87 | # # [target.'cfg(not(target_os = "linux"))'.dependencies] 88 | # sqlite-vec = "0.1.6" 89 | # text-splitter = { version = "0.28.0", default-features = false, features = ["tokenizers"] } 90 | # num_cpus = "1.17.0" 91 | turso = "0.3.2" 92 | # sqlite-vec = { path = "/mnt/d/work/sqlite-vec/bindings/rust/" } 93 | 94 | [build-dependencies] 95 | flate2 = "1.1.7" 96 | # cc = "1.2.8" 97 | 98 | [target.'cfg(windows)'.dependencies] 99 | windows = {version = "0.62.2", features = ["Win32_Globalization", "Win32_System_SystemServices"]} 100 | 101 | # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies 102 | # https://doc.rust-lang.org/reference/conditional-compilation.html 103 | [target.'cfg(target_env = "gnu")'.dependencies] 104 | openssl = { version = "0.10.74", features = ["vendored"] } 105 | 106 | [target.'cfg(target_env = "musl")'.dependencies] 107 | openssl = { version = "0.10.74", features = ["vendored"] } 108 | mimalloc = "0.1.48" 109 | 110 | [profile.dev] 111 | debug = 2 112 | debug-assertions = true 113 | overflow-checks = true 114 | opt-level = 0 115 | lto = false 116 | panic = 'unwind' 117 | rpath = false 118 | 119 | [profile.test] 120 | debug = 1 121 | 122 | [profile.bench] 123 | debug = 1 124 | 125 | [profile.release] 126 | debug = false 127 | debug-assertions = false 128 | overflow-checks = false 129 | opt-level = 3 130 | codegen-units = 1 131 | lto = true 132 | panic = 'abort' 133 | -------------------------------------------------------------------------------- /frontend/src/components/flow/nodes/EndNode.vue: -------------------------------------------------------------------------------- 1 | 70 | 87 | 88 | 89 | 90 | {{ nodeData.nodeName }} 91 | 92 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | {{ nodeData.endingText }} 107 | 108 | 109 | 117 | 118 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createRouter, createWebHashHistory } from 'vue-router' 3 | // import ElementPlus from 'element-plus' 4 | // import ElementTiptapPlugin from 'element-tiptap-vue3-fixed'; 5 | // import * as ElementPlusIconsVue from '@element-plus/icons-vue' 6 | import i18n from './assets/lang/i18n' 7 | // import VueScrollTo from 'vue-scrollto' 8 | import App from './App.vue' 9 | // import Home from './components/Home.vue' 10 | // import Intro from './components/Intro.vue' 11 | // import Doc from './components/docs/Index.vue' 12 | // import Tutorial from './components/docs/Tutorial.vue' 13 | import GlobalSettings from './components/management/GlobalSettings.vue' 14 | import Settings from './components/management/Settings.vue' 15 | import MainFlow from './components/flow/MainFlow.vue' 16 | import SubFlow from './components/flow/SubFlow.vue' 17 | import IntentList from './components/knowledge/IntentList.vue' 18 | import IntentDetail from './components/knowledge/IntentDetail.vue' 19 | import Variable from './components/variable/Variable.vue' 20 | import Home from './components/Home.vue' 21 | // import Guide from './components/Guide.vue' 22 | import HttpApiList from './components/external/HttpApiList.vue' 23 | import HttpApiDetail from './components/external/HttpApiDetail.vue' 24 | import RobotDetail from './components/robot/RobotDetail.vue' 25 | import RobotFrame from './components/robot/RobotFrame.vue' 26 | import DocList from './components/knowledge/DocList.vue' 27 | import QnAList from './components/knowledge/QnAList.vue' 28 | import NotFound from './components/404.vue' 29 | 30 | // import 'element-plus/dist/index.css' 31 | // import ElementTiptapPlugin from 'element-tiptap-vue3-fixed'; 32 | // import 'element-tiptap-vue3-fixed/lib/style.css'; 33 | 34 | /* 35 | const routes = [ 36 | { path: '/', component: Home }, 37 | // { path: '/introduction', component: Intro }, 38 | // { path: '/docs', component: Doc }, 39 | { path: '/demo/:demo', component: SubFlow }, 40 | { path: '/mainflows/:robotId', name: 'mainflows', component: MainFlow }, 41 | { path: '/subflow/:robotId/:id/:name', name: 'subflow', component: SubFlow, props: true }, 42 | { path: '/settings', component: GlobalSettings }, 43 | { path: '/settings/:robotId', name: 'settings', component: Settings }, 44 | { path: '/robot/:robotId', name: 'robotDetail', component: RobotDetail }, 45 | { path: '/intents/:robotId', name: 'intents', component: IntentList }, 46 | { path: '/intent/detail', component: IntentDetail }, 47 | { path: '/variables/:robotId', name: 'variables', component: Variable }, 48 | // { path: '/tutorial', component: Tutorial }, 49 | { path: '/external/httpApis/:robotId', name: 'externalHttpApis', component: HttpApiList }, 50 | { path: '/external/httpApi/:robotId/:id', name: 'externalHttpApiDetail', component: HttpApiDetail }, 51 | { path: '/kb/doc/:robotId', name: 'kbDoc', component: DocList }, 52 | { path: '/kb/qa/:robotId', name: 'kbQA', component: QnAList }, 53 | ] 54 | */ 55 | 56 | const routes = [ 57 | { path: '/', component: Home }, 58 | // { path: '/introduction', component: Intro }, 59 | // { path: '/docs', component: Doc }, 60 | { path: '/demo/:demo', component: SubFlow }, 61 | { 62 | path: '/robot/:robotId', component: RobotFrame, children: [ 63 | { path: '', name: 'robotDetail', component: RobotDetail }, 64 | { path: '/robot/:robotId/mainflows', name: 'mainflows', component: MainFlow }, 65 | { path: '/robot/:robotId/kb/doc', name: 'kbDoc', component: DocList }, 66 | { path: '/robot/:robotId/kb/qa', name: 'kbQA', component: QnAList }, 67 | { path: '/robot/:robotId/settings', name: 'settings', component: Settings }, 68 | { path: '/robot/:robotId/intents', name: 'intents', component: IntentList }, 69 | { path: '/robot/:robotId/intent/detail', component: IntentDetail }, 70 | { path: '/robot/:robotId/variables', name: 'variables', component: Variable }, 71 | { path: '/robot/:robotId/external/httpApis', name: 'externalHttpApis', component: HttpApiList }, 72 | { path: '/robot/:robotId/external/httpApi/:id', name: 'externalHttpApiDetail', component: HttpApiDetail }, 73 | ] 74 | }, 75 | { path: '/robot/:robotId/subflow/:id/:name', name: 'subflow', component: SubFlow, props: true }, 76 | { path: '/settings', component: GlobalSettings }, 77 | // { path: '/tutorial', component: Tutorial }, 78 | { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }, 79 | ] 80 | 81 | const router = createRouter({ 82 | history: createWebHashHistory(), 83 | routes, 84 | scrollBehavior(to, from, savedPosition) { 85 | return { top: 0 } 86 | } 87 | }) 88 | 89 | import './assets/main.css' 90 | // import 'bootstrap/dist/css/bootstrap.min.css' 91 | import 'virtual:uno.css' 92 | 93 | // createApp(App).mount('#app') 94 | const app = createApp(App) 95 | 96 | // for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 97 | // app.component(key, component) 98 | // } 99 | 100 | // app.use(ElementPlus) 101 | // app.use(ElementTiptapPlugin) 102 | app.use(router) 103 | app.use(i18n) 104 | // app.use(VueScrollTo) 105 | app.mount('#app') 106 | -------------------------------------------------------------------------------- /frontend/src/components/management/GlobalSettings.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | 52 | {{ $t('settings.title') }} 53 | 54 | 55 | {{ t('settings.commonSettings') }} 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {{ $t('settings.ipNote') }} 64 | 65 | 66 | {{ $t('settings.ipNote2') }} 67 | 68 | 69 | 70 | 71 | 72 | 74 | {{ $t('settings.prompt2_2') }} 75 | 76 | 77 | {{ $t('settings.note') }} 78 | 79 | 80 | 81 | {{ $t('common.save') }} 82 | 83 | {{ $t('common.cancel') }} 84 | 85 | 86 | 87 | 88 | HuggingFace model downloading settings 89 | 90 | 91 | 92 | 93 | 95 | millis 96 | 97 | 98 | 100 | millis 101 | 102 | 103 | 104 | 105 | 106 | 107 | {{ $t('common.save') }} 108 | 109 | {{ $t('common.cancel') }} 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/flow/mainflow/crud.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{LazyLock, OnceLock}; 3 | 4 | use axum::extract::Query; 5 | use axum::{Json, response::IntoResponse}; 6 | // use redb::TableDefinition; 7 | use std::sync::Mutex; 8 | 9 | use super::dto::MainFlowDetail; 10 | use crate::db; 11 | use crate::db_executor; 12 | use crate::flow::subflow::crud as subflow; 13 | use crate::result::{Error, Result}; 14 | use crate::web::server::to_res; 15 | 16 | // const TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("mainflows"); 17 | pub(crate) const TABLE_SUFFIX: &str = "mainflows"; 18 | 19 | static LOCK: LazyLock> = LazyLock::new(|| Mutex::new(false)); 20 | static DEFAULT_NAMES: OnceLock<(String, String)> = OnceLock::new(); 21 | 22 | pub(crate) fn init_default_names(is_en: bool) -> Result<()> { 23 | let (name, subflow_name) = if is_en { 24 | ("The first main flow", "First sub-flow") 25 | } else { 26 | ("第一个主流程", "第一个子流程") 27 | }; 28 | DEFAULT_NAMES 29 | .set((String::from(name), String::from(subflow_name))) 30 | .map_err(|_| Error::WithMessage(String::from("Dup"))) 31 | } 32 | 33 | pub(crate) fn init(robot_id: &str) -> Result { 34 | // let table_name = format!("{}-mainflows", robot_id); 35 | // let main_flow_table: TableDefinition<&str, &[u8]> = TableDefinition::new(&table_name); 36 | // db::init_table(main_flow_table)?; 37 | db_executor!(db::init_table, robot_id, TABLE_SUFFIX,)?; 38 | // let table_name = format!("{}-subflows", robot_id); 39 | // let sub_flow_table: TableDefinition<&str, &[u8]> = TableDefinition::new(&table_name); 40 | // db::init_table(sub_flow_table)?; 41 | db_executor!( 42 | db::init_table, 43 | robot_id, 44 | crate::flow::subflow::crud::TABLE_SUFFIX, 45 | )?; 46 | create_main_flow(robot_id, &DEFAULT_NAMES.get().unwrap().0) 47 | } 48 | 49 | pub(crate) async fn list(Query(q): Query>) -> impl IntoResponse { 50 | // to_res::>(db::get_all(TABLE)) 51 | if let Some(robot_id) = q.get("robotId") { 52 | to_res::>(db_executor!(db::get_all, robot_id, TABLE_SUFFIX,)) 53 | } else { 54 | to_res(Err(Error::WithMessage(String::from( 55 | "Parameter: robot_id is missing.", 56 | )))) 57 | } 58 | } 59 | 60 | pub(crate) async fn new( 61 | Query(q): Query>, 62 | Json(data): Json, 63 | ) -> impl IntoResponse { 64 | if let Some(robot_id) = q.get("robotId") { 65 | to_res::(create_main_flow(robot_id, &data.name)) 66 | } else { 67 | to_res(Err(Error::WithMessage(String::from( 68 | "Parameter: robotId is missing.", 69 | )))) 70 | } 71 | } 72 | 73 | fn create_main_flow(robot_id: &str, name: &str) -> Result { 74 | let _lock = LOCK.lock(); 75 | // let count = db::count(TABLE)?; 76 | let count = db_executor!(db::count, robot_id, TABLE_SUFFIX,)?; 77 | let mut buffer = itoa::Buffer::new(); 78 | let count = buffer.format(count + 1); 79 | let id = format!("{}{}", count, scru128::new_string()); 80 | let main_flow = MainFlowDetail { 81 | id, 82 | name: String::from(name), 83 | enabled: true, 84 | }; 85 | // db::write(TABLE, main_flow.id.as_str(), &main_flow)?; 86 | db_executor!( 87 | db::write, 88 | robot_id, 89 | TABLE_SUFFIX, 90 | main_flow.id.as_str(), 91 | &main_flow 92 | )?; 93 | subflow::new_subflow(robot_id, &main_flow.id, &DEFAULT_NAMES.get().unwrap().1)?; 94 | Ok(main_flow) 95 | } 96 | 97 | pub(crate) async fn save( 98 | Query(q): Query>, 99 | Json(data): Json, 100 | ) -> impl IntoResponse { 101 | if let Some(robot_id) = q.get("robotId") { 102 | let main_flow = MainFlowDetail { 103 | id: data.id.clone(), 104 | name: data.name.clone(), 105 | enabled: data.enabled, 106 | }; 107 | to_res(db_executor!( 108 | db::write, 109 | robot_id, 110 | TABLE_SUFFIX, 111 | &data.id, 112 | &main_flow 113 | )) 114 | } else { 115 | to_res(Err(Error::WithMessage(String::from( 116 | "Parameter: robotId is missing.", 117 | )))) 118 | } 119 | } 120 | 121 | pub(crate) async fn delete( 122 | Query(q): Query>, 123 | Json(data): Json, 124 | ) -> impl IntoResponse { 125 | if let Some(robot_id) = q.get("robotId") { 126 | let main_flow_id = data.id.as_str(); 127 | match crate::flow::rt::crud::remove_runtime_nodes(main_flow_id) { 128 | Ok(_) => to_res(db_executor!( 129 | db::remove, 130 | robot_id, 131 | TABLE_SUFFIX, 132 | main_flow_id 133 | )), 134 | Err(e) => to_res(Err(e)), 135 | } 136 | } else { 137 | to_res(Err(Error::WithMessage(String::from( 138 | "Parameter: robotId is missing.", 139 | )))) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/external/http/client.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time::Duration; 3 | use std::vec::Vec; 4 | 5 | use reqwest::Client; 6 | use reqwest::RequestBuilder; 7 | use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; 8 | 9 | use super::dto::{HttpReqInfo, Method, PostContentType, Protocol, ResponseData, ValueSource}; 10 | use crate::result::Result; 11 | use crate::variable::dto::VariableValue; 12 | 13 | pub(crate) fn get_client( 14 | connect_timeout_millis: u64, 15 | read_timeout_millis: u64, 16 | proxy_url: &str, 17 | ) -> Result { 18 | let mut client = reqwest::Client::builder() 19 | .connect_timeout(Duration::from_millis(connect_timeout_millis)) 20 | .read_timeout(Duration::from_millis(read_timeout_millis)) 21 | // Since can not reuse Client currently, so set pool size to 0 22 | .pool_max_idle_per_host(0) 23 | .pool_idle_timeout(Duration::from_secs(1)); 24 | if proxy_url.is_empty() { 25 | client = client.no_proxy(); 26 | } else { 27 | let proxy = reqwest::Proxy::http(proxy_url)?; 28 | client = client.proxy(proxy); 29 | } 30 | let client = client.build()?; 31 | Ok(client) 32 | } 33 | 34 | pub(crate) async fn status_code( 35 | info: HttpReqInfo, 36 | timeout_milliseconds: u64, 37 | vars: HashMap, 38 | ) -> Result { 39 | let req = build_req(&info, timeout_milliseconds, &vars)?; 40 | let res = req.send().await?; 41 | Ok(res.status().as_u16()) 42 | } 43 | 44 | pub(crate) async fn req( 45 | info: HttpReqInfo, 46 | timeout_milliseconds: u64, 47 | vars: &HashMap, 48 | ) -> reqwest::Result { 49 | let req = build_req(&info, timeout_milliseconds, vars)?; 50 | let res = req.send().await?; 51 | // println!("http status code {}", res.status().as_str()); 52 | if res.status() != reqwest::StatusCode::OK { 53 | return Ok(ResponseData::None); 54 | } 55 | let content_type = res 56 | .headers() 57 | .get(reqwest::header::CONTENT_TYPE) 58 | .map_or("", |h| h.to_str().unwrap()); 59 | let data = if content_type.contains("text/") 60 | || content_type.contains("/json") 61 | || content_type.contains("/xml") 62 | { 63 | let s = res.text().await?; 64 | // println!("{}", s); 65 | ResponseData::Str(s) 66 | } else { 67 | ResponseData::Bin(res.bytes().await?.to_vec()) 68 | }; 69 | Ok(data) 70 | } 71 | 72 | fn build_req( 73 | info: &HttpReqInfo, 74 | timeout_milliseconds: u64, 75 | vars: &HashMap, 76 | ) -> reqwest::Result { 77 | let client = reqwest::Client::builder() 78 | .connect_timeout(Duration::from_millis(1000)) 79 | .read_timeout(Duration::from_millis(timeout_milliseconds)) 80 | .build()?; 81 | let mut url = String::with_capacity(512); 82 | match info.protocol { 83 | Protocol::HTTP => url.push_str("http"), 84 | Protocol::HTTPS => url.push_str("https"), 85 | } 86 | url.push_str("://"); 87 | url.push_str(&info.address); 88 | let mut req = match info.method { 89 | Method::GET => client.get(&url), 90 | Method::POST => { 91 | let r = client.post(&url); 92 | if !info.request_body.is_empty() { 93 | r.body(info.request_body.clone()) 94 | } else { 95 | r 96 | } 97 | } 98 | }; 99 | if !info.headers.is_empty() { 100 | let mut headers: HeaderMap = HeaderMap::with_capacity(info.headers.len()); 101 | for p in info.headers.iter() { 102 | match p.value_source { 103 | ValueSource::Val => headers.insert( 104 | HeaderName::from_bytes(p.name.as_bytes()).unwrap(), 105 | p.value.parse().unwrap(), 106 | ), 107 | ValueSource::Var => headers.insert( 108 | HeaderName::from_bytes(p.name.as_bytes()).unwrap(), 109 | vars.get(&p.value) 110 | .map_or(String::new(), |v| v.val_to_string()) 111 | .parse() 112 | .unwrap(), 113 | ), 114 | }; 115 | } 116 | req = req.headers(headers); 117 | } 118 | if !info.query_params.is_empty() { 119 | let mut queries: Vec<(&str, String)> = Vec::with_capacity(info.query_params.len()); 120 | for p in info.query_params.iter() { 121 | match p.value_source { 122 | ValueSource::Val => queries.push((&p.name, p.value.clone())), 123 | ValueSource::Var => queries.push(( 124 | &p.name, 125 | vars.get(&p.value) 126 | .map_or(String::new(), |v| v.val_to_string()), 127 | )), 128 | } 129 | } 130 | req = req.query(&queries); 131 | } 132 | if info.post_content_type == PostContentType::JSON { 133 | req = req.header("Content-Type", "application/json"); 134 | } 135 | if !info.user_agent.is_empty() { 136 | req = req.header("User-Agent", &info.user_agent); 137 | } 138 | // Ok(req.timeout(Duration::from_millis(info.timeout_milliseconds))) 139 | Ok(req) 140 | } 141 | -------------------------------------------------------------------------------- /src/ai/phi3.rs: -------------------------------------------------------------------------------- 1 | use candle::{DType, Device, IndexOp, Tensor}; 2 | use candle_transformers::generation::LogitsProcessor; 3 | use candle_transformers::models::phi3::Model; 4 | use frand::Rand; 5 | use tokenizers::Tokenizer; 6 | 7 | use super::chat::ResultSender; 8 | use crate::flow::rt::dto::StreamingResponseData; 9 | use crate::result::{Error, Result}; 10 | 11 | // static TEXT_GENERATION_MODEL: OnceLock>> = 12 | // OnceLock::new(); 13 | 14 | // pub(super) fn replace_model_cache(robot_id: &str, info: &HuggingFaceModelInfo) -> Result<()> { 15 | // let device = device()?; 16 | // let c = load_phi3_model_files(info, &device)?; 17 | // if let Some(lock) = TEXT_GENERATION_MODEL.get() { 18 | // if let Ok(mut cache) = lock.lock() { 19 | // cache.insert(String::from(robot_id), c); 20 | // } 21 | // } 22 | // Ok(()) 23 | // } 24 | 25 | pub(super) fn gen_text( 26 | device: &Device, 27 | model: &Model, 28 | tokenizer: &Tokenizer, 29 | prompt: &str, 30 | sample_len: usize, 31 | top_p: Option, 32 | result_sender: &mut ResultSender<'_, StreamingResponseData>, 33 | ) -> Result<()> { 34 | // let device = device()?; 35 | // let lock = TEXT_GENERATION_MODEL.get_or_init(|| Mutex::new(HashMap::with_capacity(32))); 36 | // let mut model = lock.lock().unwrap_or_else(|e| { 37 | // log::warn!("{:#?}", &e); 38 | // e.into_inner() 39 | // }); 40 | // if !model.contains_key(robot_id) { 41 | // let r = load_phi3_model_files(info, &device)?; 42 | // model.insert(String::from(robot_id), r); 43 | // }; 44 | // let (model, tokenizer) = model.get(robot_id).unwrap(); 45 | 46 | // log::info!("starting the inference loop"); 47 | let mut tokenizer = super::token_output_stream::TokenOutputStream::new(tokenizer.clone()); 48 | let mut tokens = match tokenizer.tokenizer().encode(prompt, true) { 49 | Ok(t) => t.get_ids().to_vec(), 50 | Err(e) => return Err(Error::WithMessage(format!("{}", &e))), 51 | }; 52 | if tokens.is_empty() { 53 | return Err(Error::WithMessage(String::from( 54 | "Empty prompts are not supported in the phi model.", 55 | ))); 56 | } 57 | let mut generated_tokens = 0usize; 58 | let eos_token = match tokenizer.get_token("<|endoftext|>") { 59 | Some(token) => token, 60 | None => { 61 | return Err(Error::WithMessage(String::from( 62 | "cannot find the endoftext token", 63 | ))); 64 | } 65 | }; 66 | // log::info!("{prompt}"); 67 | // std::io::stdout().flush()?; 68 | let start_gen = std::time::Instant::now(); 69 | let mut pos = 0; 70 | let mut rng = Rand::new(); 71 | let mut model = model.clone(); 72 | let mut logits_processor = LogitsProcessor::new( 73 | rng.r#gen::(), 74 | Some(super::completion::TEMPERATURE), 75 | top_p, 76 | ); 77 | for index in 0..sample_len { 78 | let context_size = if index > 0 { 1 } else { tokens.len() }; 79 | let ctxt = &tokens[tokens.len().saturating_sub(context_size)..]; 80 | let input = Tensor::new(ctxt, device)?.unsqueeze(0)?; 81 | let logits = model.forward(&input, pos)?.i((.., 0, ..))?; 82 | let logits = logits.squeeze(0)?.to_dtype(DType::F32)?; 83 | let logits = if super::completion::REPEAT_PENALTY == 1. { 84 | logits 85 | } else { 86 | let start_at = tokens 87 | .len() 88 | .saturating_sub(super::completion::REPEAT_LAST_N); 89 | candle_transformers::utils::apply_repeat_penalty( 90 | &logits, 91 | super::completion::REPEAT_PENALTY, 92 | &tokens[start_at..], 93 | )? 94 | }; 95 | 96 | let next_token = logits_processor.sample(&logits)?; 97 | tokens.push(next_token); 98 | generated_tokens += 1; 99 | if next_token == eos_token { 100 | if let Some(t) = tokenizer.decode_rest()? { 101 | match result_sender { 102 | ResultSender::ChannelSender(sender_wrapper) => { 103 | // crate::sse_send!(sender, t); 104 | sender_wrapper.send(t); 105 | } 106 | ResultSender::StrBuf(sb) => { 107 | sb.push_str(&t); 108 | // ResultReceiver::StrBuf(sb) 109 | } 110 | } 111 | } 112 | break; 113 | } 114 | if let Some(t) = tokenizer.next_token(next_token)? { 115 | match result_sender { 116 | ResultSender::ChannelSender(sender_wrapper) => { 117 | sender_wrapper.send(t); 118 | } 119 | ResultSender::StrBuf(sb) => { 120 | sb.push_str(&t); 121 | // ResultReceiver::StrBuf(sb) 122 | } 123 | } 124 | // std::io::stdout().flush()?; 125 | } 126 | pos += context_size; 127 | } 128 | let dt = start_gen.elapsed(); 129 | log::info!( 130 | "\n{generated_tokens} tokens generated ({:.2} token/s)", 131 | generated_tokens as f64 / dt.as_secs_f64(), 132 | ); 133 | Ok(()) 134 | } 135 | -------------------------------------------------------------------------------- /src/intent/detector.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use unicase::UniCase; 3 | 4 | use super::dto::IntentDetail; 5 | use super::phrase; 6 | use crate::ai::embedding::embedding; 7 | use crate::db; 8 | use crate::db_executor; 9 | use crate::result::Result; 10 | 11 | pub(crate) async fn detect(robot_id: &str, s: &str) -> Result> { 12 | // let now = std::time::Instant::now(); 13 | let r: Result> = 14 | db_executor!(db::get_all, robot_id, super::crud::TABLE_SUFFIX,); 15 | let intents = match r { 16 | Ok(v) => { 17 | // log::info!("v.len {}", v.len()); 18 | if v.is_empty() { 19 | return Ok(None); 20 | } 21 | v 22 | } 23 | Err(e) => { 24 | log::warn!("Detecting intent failed: {:?}", &e); 25 | return Ok(None); 26 | } 27 | }; 28 | // log::info!("intents.len {}", intents.len()); 29 | let mut empty_phrase = true; 30 | for detail in intents.iter() { 31 | // log::info!("intent detail {} {}", detail.intent_id, serde_json::to_string(&detail).unwrap()); 32 | // log::info!("detail.keywords.len {}", detail.keywords.len()); 33 | let unicase_s = UniCase::new(s); 34 | for k in detail.keywords.iter() { 35 | // log::info!("intent compare {} {}", k, s); 36 | if UniCase::new(k) == unicase_s { 37 | // println!("{} {} {}", s, k, &i.name); 38 | return Ok(Some(detail.intent_name.clone())); 39 | } 40 | } 41 | for r in detail.regexes.iter() { 42 | let re = Regex::new(r)?; 43 | if re.is_match(s) { 44 | return Ok(Some(detail.intent_name.clone())); 45 | } 46 | } 47 | empty_phrase = empty_phrase && detail.phrases.is_empty(); 48 | } 49 | if empty_phrase { 50 | return Ok(None); 51 | } 52 | let embedding = embedding(robot_id, s).await; 53 | let embedding = match embedding { 54 | Ok(embedding) => { 55 | if embedding.0.is_empty() { 56 | return Ok(None); 57 | } 58 | embedding 59 | } 60 | Err(e) => { 61 | log::warn!("Detecting intent failed: {:?}", &e); 62 | return Ok(None); 63 | } 64 | }; 65 | // log::info!("Generate embedding cost {:?}", now.elapsed()); 66 | // let s = format!("{:?}", &embedding); 67 | // let regex = regex::Regex::new(r"\s").unwrap(); 68 | // log::info!("detect embedding {}", regex.replace_all(&s, "")); 69 | // let now = std::time::Instant::now(); 70 | let search_vector: Vec = embedding.0; 71 | let similarity_threshold = embedding.1 as f64; 72 | let mut result = phrase::search(robot_id, &search_vector).await?; 73 | // log::info!("Searching vector took {:?}", now.elapsed()); 74 | if !result.is_empty() { 75 | if let Some(record) = result.get_mut(0) { 76 | // log::info!("Record distance: {}", record.1); 77 | if (1f64 - record.1) >= similarity_threshold { 78 | let s = std::mem::take(&mut record.0); 79 | return Ok(Some(s)); 80 | } 81 | } 82 | } 83 | Ok(None) 84 | } 85 | 86 | /* 87 | pub(crate) async fn save_intent_embedding( 88 | robot_id: &str, 89 | intent_id: &str, 90 | intent_name: &str, 91 | s: &str, 92 | ) -> Result { 93 | let embedding = embedding(robot_id, s).await?; 94 | if embedding.0.is_empty() { 95 | let err = format!("{s} embedding data is empty"); 96 | log::warn!("{}", &err); 97 | return Err(Error::ErrorWithMessage(err)); 98 | } 99 | log::info!("embedding.0.len() = {}", embedding.0.len()); 100 | let id = phrase::add(robot_id, intent_id, intent_name, &embedding.0).await?; 101 | Ok(id) 102 | } 103 | 104 | pub(crate) async fn save_intent_embeddings( 105 | robot_id: &str, 106 | intent_id: &str, 107 | intent_name: &str, 108 | array: Vec<&str>, 109 | ) -> Result<()> { 110 | phrase::remove_by_intent_id(robot_id, intent_id).await?; 111 | let mut embeddings: Vec> = Vec::with_capacity(array.len()); 112 | for &s in array.iter() { 113 | let embedding = embedding(robot_id, s).await?; 114 | if embedding.0.is_empty() { 115 | let err = format!("{s} embedding data is empty"); 116 | log::warn!("{}", &err); 117 | } else { 118 | embeddings.push(embedding.0); 119 | } 120 | } 121 | // if embeddings.is_empty() { 122 | // return Err(Error::ErrorWithMessage(String::from( 123 | // "No embeddings were generated.", 124 | // ))); 125 | // } 126 | if !embeddings.is_empty() { 127 | phrase::batch_add(robot_id, intent_id, intent_name, &embeddings).await?; 128 | } 129 | Ok(()) 130 | } 131 | */ 132 | 133 | // pub(crate) async fn save_intent_embedding2(intent_id: &str, s: &str) -> Result<()> { 134 | // let embeddings = embedding(s)?; 135 | // if embeddings.is_none() { 136 | // return Ok(()); 137 | // } 138 | // let vectors: Vec = embeddings.unwrap().iter().map(|v| v.into()).collect(); 139 | // let records: Vec = vectors.iter().map(|v| Record::new(v, &"".into())).collect(); 140 | // let mut config = Config::default(); 141 | // config.distance = Distance::Cosine; 142 | // let collection = Collection::build(&config, &records)?; 143 | // let mut db = Database::open(&format!("{}{}",SAVING_PATH,robot_id))?; 144 | // db.save_collection(intent_id, &collection)?; 145 | // Ok(()) 146 | // } 147 | -------------------------------------------------------------------------------- /src/robot/crud.rs: -------------------------------------------------------------------------------- 1 | use std::fs::DirEntry; 2 | use std::path::Path; 3 | use std::vec::Vec; 4 | 5 | use axum::extract::Query; 6 | use axum::{Json, response::IntoResponse}; 7 | use redb::TableDefinition; 8 | 9 | use super::dto::{RobotData, RobotQuery, RobotType}; 10 | use crate::db_executor; 11 | use crate::external::http::crud as http; 12 | use crate::flow::mainflow::crud as mainflow; 13 | use crate::intent::crud as intent; 14 | use crate::man::settings; 15 | use crate::result::{Error, Result}; 16 | use crate::variable::crud as variable; 17 | use crate::web::server; 18 | use crate::{db, web::server::to_res}; 19 | 20 | const TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("robots"); 21 | 22 | fn get_robot_id() -> String { 23 | let mut id = String::with_capacity(32); 24 | id.push('r'); 25 | let gen_id = scru128::new_string(); 26 | id.push_str(&gen_id); 27 | id 28 | } 29 | 30 | pub(crate) async fn init(is_en: bool) -> Result<()> { 31 | db::init_table(TABLE)?; 32 | let name = if is_en { 33 | "My first robot" 34 | } else { 35 | "我的第一个机器人" 36 | }; 37 | let d = RobotData { 38 | robot_id: get_robot_id(), 39 | robot_name: String::from(name), 40 | robot_type: RobotType::TextBot, 41 | }; 42 | new(&d, is_en).await 43 | } 44 | 45 | pub(crate) async fn save( 46 | headers: axum::http::HeaderMap, 47 | Json(mut d): Json, 48 | ) -> impl IntoResponse { 49 | if d.robot_id.is_empty() { 50 | let is_en = server::is_en(&headers); 51 | d.robot_id = get_robot_id(); 52 | if let Err(e) = new(&d, is_en).await { 53 | return to_res(Err(Error::WithMessage(format!( 54 | "Failed to create robot, error detail was: {:?}", 55 | &e 56 | )))); 57 | } 58 | } 59 | let r = persist(&d); 60 | to_res(r) 61 | } 62 | 63 | async fn new(d: &RobotData, is_en: bool) -> Result<()> { 64 | persist(d)?; 65 | // 机器人意图 66 | settings::init(&d.robot_id)?; 67 | // crate::intent::phrase::init_tables(&d.robot_id).await?; 68 | crate::kb::qa::init_tables(&d.robot_id).await?; 69 | crate::kb::doc::init_tables(&d.robot_id).await?; 70 | // 意图 71 | intent::init(&d.robot_id, is_en)?; 72 | // 变量 73 | variable::init(&d.robot_id, is_en)?; 74 | // 主流程 75 | mainflow::init(&d.robot_id)?; 76 | // Http 接口 77 | http::init(&d.robot_id)?; 78 | Ok(()) 79 | } 80 | 81 | fn persist(d: &RobotData) -> Result<()> { 82 | db::write(TABLE, d.robot_id.as_str(), &d)?; 83 | Ok(()) 84 | } 85 | 86 | pub(crate) async fn list() -> impl IntoResponse { 87 | to_res::>(db::get_all(TABLE)) 88 | } 89 | 90 | pub(crate) async fn detail(Query(q): Query) -> impl IntoResponse { 91 | to_res::>(db::query(TABLE, q.robot_id.as_str())) 92 | } 93 | 94 | pub(crate) async fn delete(Query(q): Query) -> impl IntoResponse { 95 | to_res(purge(&q.robot_id).await) 96 | } 97 | 98 | fn delete_entry(entry: &DirEntry) -> Result<()> { 99 | log::info!("Deleting file {:?}", entry.path()); 100 | std::fs::remove_file(entry.path())?; 101 | Ok(()) 102 | } 103 | 104 | fn delete_dirs(dir: &Path, cb: &dyn Fn(&DirEntry) -> Result<()>) -> Result<()> { 105 | if dir.is_dir() { 106 | for entry in std::fs::read_dir(dir)? { 107 | let entry = entry?; 108 | let path = entry.path(); 109 | if path.is_dir() { 110 | delete_dirs(&path, cb)?; 111 | log::info!("Deleting dir {:?}", &path); 112 | std::fs::remove_dir(path)?; 113 | } else { 114 | cb(&entry)?; 115 | } 116 | } 117 | } 118 | Ok(()) 119 | } 120 | 121 | async fn purge(robot_id: &str) -> Result<()> { 122 | crate::intent::phrase::remove_tables(robot_id).await?; 123 | // let root = &format!("{}{}", crate::intent::detector::SAVING_PATH_ROOT, robot_id); 124 | // let path = Path::new(&root); 125 | // if path.exists() { 126 | // delete_dirs(&path, &delete_entry)?; 127 | // std::fs::remove_dir(path)?; 128 | // } 129 | // if let Err(e) = std::fs::remove_dir(format!( 130 | // "{}{}", 131 | // crate::intent::detector::SAVING_PATH_ROOT, 132 | // robot_id 133 | // )) { 134 | // if e.kind() != std::io::ErrorKind::NotFound { 135 | // return Err(e.into()); 136 | // } 137 | // } 138 | db::remove(crate::man::settings::TABLE, robot_id)?; 139 | db_executor!( 140 | db::delete_table, 141 | robot_id, 142 | crate::external::http::crud::TABLE_SUFFIX, 143 | )?; 144 | db_executor!( 145 | db::delete_table, 146 | robot_id, 147 | crate::variable::crud::TABLE_SUFFIX, 148 | )?; 149 | db_executor!( 150 | db::delete_table, 151 | robot_id, 152 | crate::intent::crud::TABLE_SUFFIX, 153 | )?; 154 | db_executor!( 155 | db::delete_table, 156 | robot_id, 157 | crate::flow::subflow::crud::TABLE_SUFFIX, 158 | )?; 159 | let r: Vec = db_executor!( 160 | db::get_all, 161 | robot_id, 162 | crate::flow::mainflow::crud::TABLE_SUFFIX, 163 | )?; 164 | for v in r.iter() { 165 | crate::flow::rt::crud::remove_runtime_nodes(&v.id)?; 166 | } 167 | db_executor!( 168 | db::delete_table, 169 | robot_id, 170 | crate::flow::mainflow::crud::TABLE_SUFFIX, 171 | )?; 172 | db::remove(TABLE, robot_id) 173 | } 174 | -------------------------------------------------------------------------------- /src/ai/gemma.rs: -------------------------------------------------------------------------------- 1 | use candle::{DType, Device, Tensor}; 2 | use candle_transformers::generation::LogitsProcessor; 3 | use candle_transformers::models::gemma::Model as GemmaModel; 4 | // use crossbeam_channel::Sender; 5 | use frand::Rand; 6 | use tokenizers::Tokenizer; 7 | 8 | use super::chat::ResultSender; 9 | use crate::flow::rt::dto::StreamingResponseData; 10 | use crate::result::{Error, Result}; 11 | 12 | // static TEXT_GENERATION_MODEL: OnceLock>> = 13 | // OnceLock::new(); 14 | 15 | // pub(super) fn replace_model_cache(robot_id: &str, info: &HuggingFaceModelInfo) -> Result<()> { 16 | // let device = device()?; 17 | // let c = load_gemma_model_files(info, &device)?; 18 | // if let Some(lock) = TEXT_GENERATION_MODEL.get() { 19 | // if let Ok(mut cache) = lock.lock() { 20 | // cache.insert(String::from(robot_id), c); 21 | // } 22 | // } 23 | // Ok(()) 24 | // } 25 | 26 | pub(super) fn gen_text( 27 | device: &Device, 28 | model: &GemmaModel, 29 | tokenizer: &Tokenizer, 30 | prompt: &str, 31 | sample_len: usize, 32 | top_p: Option, 33 | result_sender: &mut ResultSender<'_, StreamingResponseData>, 34 | ) -> Result<()> { 35 | // let device = device()?; 36 | // let lock = TEXT_GENERATION_MODEL.get_or_init(|| Mutex::new(HashMap::with_capacity(32))); 37 | // let mut model = lock.lock().unwrap_or_else(|e| { 38 | // log::warn!("{:#?}", &e); 39 | // e.into_inner() 40 | // }); 41 | // if !model.contains_key(robot_id) { 42 | // let r = load_gemma_model_files(info, &device)?; 43 | // model.insert(String::from(robot_id), r); 44 | // }; 45 | // let (model, tokenizer) = model.get(robot_id).unwrap(); 46 | let mut tokens = match tokenizer.encode(prompt, true) { 47 | Ok(t) => t.get_ids().to_vec(), 48 | Err(e) => return Err(Error::WithMessage(format!("{}", &e))), 49 | }; 50 | let mut tokenizer = super::token_output_stream::TokenOutputStream::new(tokenizer.clone()); 51 | let eos_token = match tokenizer.get_token("") { 52 | Some(token) => token, 53 | None => { 54 | return Err(Error::WithMessage(String::from( 55 | "cannot find the token", 56 | ))); 57 | } 58 | }; 59 | let mut generated_tokens = 0usize; 60 | let start_gen = std::time::Instant::now(); 61 | let mut model = model.clone(); 62 | // let rr = Rc::new(result_sender); 63 | for index in 0..sample_len { 64 | let context_size = if index > 0 { 1 } else { tokens.len() }; 65 | let start_pos = tokens.len().saturating_sub(context_size); 66 | let ctxt = &tokens[start_pos..]; 67 | let input = Tensor::new(ctxt, device)?.unsqueeze(0)?; 68 | let logits = model.forward(&input, start_pos)?; 69 | let logits = logits.squeeze(0)?.squeeze(0)?.to_dtype(DType::F32)?; 70 | let logits = if super::completion::REPEAT_PENALTY == 1. { 71 | logits 72 | } else { 73 | let start_at = tokens 74 | .len() 75 | .saturating_sub(super::completion::REPEAT_LAST_N); 76 | candle_transformers::utils::apply_repeat_penalty( 77 | &logits, 78 | super::completion::REPEAT_PENALTY, 79 | &tokens[start_at..], 80 | )? 81 | }; 82 | 83 | let mut rng = Rand::new(); 84 | let mut logits_processor = LogitsProcessor::new( 85 | rng.r#gen::(), 86 | Some(super::completion::TEMPERATURE), 87 | top_p, 88 | ); 89 | let next_token = logits_processor.sample(&logits)?; 90 | tokens.push(next_token); 91 | generated_tokens += 1; 92 | if next_token == eos_token { 93 | break; 94 | } 95 | // let rr: ResultReceiver; 96 | if let Some(t) = tokenizer.next_token(next_token)? { 97 | // print!("{t}"); 98 | // std::io::stdout().flush()?; 99 | // let result_sender = rr.clone(); 100 | match result_sender { 101 | ResultSender::ChannelSender(sender_wrapper) => { 102 | if let Err(e) = sender_wrapper.try_send(t) { 103 | log::warn!( 104 | "Sent failed, maybe receiver dropped or queue was full, err: {:?}", 105 | &e 106 | ); 107 | break; 108 | } 109 | // ResultReceiver::SseSender(sender) 110 | } 111 | ResultSender::StrBuf(sb) => { 112 | sb.push_str(&t); 113 | // ResultReceiver::StrBuf(sb) 114 | } 115 | } 116 | } 117 | } 118 | let dt = start_gen.elapsed(); 119 | if let Some(rest) = tokenizer.decode_rest()? { 120 | // print!("{rest}"); 121 | match result_sender { 122 | ResultSender::ChannelSender(sender_wrapper) => { 123 | if let Err(e) = sender_wrapper.try_send(rest) { 124 | log::warn!( 125 | "Sent failed, maybe receiver dropped or queue was full, err: {:?}", 126 | &e 127 | ); 128 | } 129 | } 130 | ResultSender::StrBuf(sb) => sb.push_str(&rest), 131 | } 132 | } 133 | // std::io::stdout().flush()?; 134 | println!( 135 | "\n{generated_tokens} tokens generated ({:.2} token/s)", 136 | generated_tokens as f64 / dt.as_secs_f64(), 137 | ); 138 | Ok(()) 139 | } 140 | -------------------------------------------------------------------------------- /frontend/src/components/flow/MainFlow.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 115 | 125 | {{ $t('mainflow.title') }} 126 | {{ $t('mainflow.add') }} 127 | 128 | 129 | 130 | 131 | 132 | 133 | {{ scope.row.name }} 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | {{ $t('common.edit') }} 143 | | 144 | 145 | {{ $t('common.changeName') }} 146 | | 147 | 148 | {{ $t('common.del') }} 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | {{ $t('common.save') }} 161 | {{ $t('common.cancel') }} 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/variable/crud.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | // use std::borrow::BorrowMut; 3 | use std::vec::Vec; 4 | 5 | use axum::Json; 6 | use axum::extract::Query; 7 | use axum::response::IntoResponse; 8 | 9 | use super::dto::Variable; 10 | use super::dto::{VariableObtainValueExpressionType, VariableType, VariableValueSource}; 11 | use crate::db; 12 | use crate::db_executor; 13 | use crate::flow::rt::context::Context; 14 | use crate::flow::rt::dto::Request; 15 | use crate::result::{Error, Result}; 16 | use crate::web::server::to_res; 17 | 18 | // const TABLE: redb::TableDefinition<&str, &[u8]> = redb::TableDefinition::new("variables"); 19 | // pub(crate) const VARIABLE_LIST_KEY: &str = "variables"; 20 | pub(crate) const TABLE_SUFFIX: &str = "vars"; 21 | 22 | // #[macro_export] 23 | // macro_rules! db_executor ( 24 | // ($func: expr, $robot_id: expr, $($bind: expr),*) => ({ 25 | // let table_name = format!("{}vars", $robot_id); 26 | // let table: redb::TableDefinition<&str, &[u8]> = redb::TableDefinition::new(&table_name); 27 | // $func(table $(,($bind))*) 28 | // }); 29 | // ); 30 | 31 | // #[inline] 32 | // fn get_table_name(robot_id: &str) -> String { 33 | // format!("{}vars", robot_id) 34 | // } 35 | 36 | pub(crate) fn init(robot_id: &str, is_en: bool) -> Result<()> { 37 | let v = Variable { 38 | var_name: String::from(if is_en { 39 | "CollectionVar" 40 | } else { 41 | "采集变量" 42 | }), 43 | var_type: VariableType::Str, 44 | var_val_source: VariableValueSource::Collect, 45 | var_constant_value: String::new(), 46 | var_associate_data: String::new(), 47 | obtain_value_expression_type: VariableObtainValueExpressionType::None, 48 | obtain_value_expression: String::new(), 49 | timeout_milliseconds: 1500u64, 50 | cache_enabled: true, 51 | }; 52 | // let result = db_executor!(db::write, robot_id, &v.var_name, &v); 53 | // let table_name = get_table_name(robot_id); 54 | // let table: redb::TableDefinition<&str, &[u8]> = redb::TableDefinition::new(&table_name); 55 | // db::write(table, &v.var_name, &v) 56 | db_executor!(db::write, robot_id, TABLE_SUFFIX, &v.var_name, &v) 57 | } 58 | 59 | pub(crate) async fn list(Query(q): Query>) -> impl IntoResponse { 60 | // let result:Result> = db_executor!(db::get_all, "robot_id",); 61 | // to_res::>(db::get_all(TABLE)) 62 | if let Some(robot_id) = q.get("robotId") { 63 | to_res::>(db_executor!(db::get_all, robot_id, TABLE_SUFFIX,)) 64 | } else { 65 | to_res(Err(Error::WithMessage(String::from( 66 | "Parameter: robotId is missing.", 67 | )))) 68 | } 69 | } 70 | 71 | pub(crate) async fn add( 72 | Query(q): Query>, 73 | Json(v): Json, 74 | ) -> impl IntoResponse { 75 | /* 76 | let r: Result>> = db::query(TABLE, VARIABLE_LIST_KEY); 77 | let r = r.and_then(|op| { 78 | let mut new_record = true; 79 | if let Some(mut d) = op { 80 | d.retain_mut(|x| { 81 | if x.var_name.eq(&v.var_name) { 82 | x.var_type = v.var_type.clone(); 83 | x.var_val_source = v.var_val_source.clone(); 84 | new_record = false; 85 | } 86 | true 87 | }); 88 | if new_record { 89 | d.push(v.clone()); 90 | } 91 | db::write(TABLE, VARIABLE_LIST_KEY, &d) 92 | } else { 93 | let d = vec![v]; 94 | db::write(TABLE, VARIABLE_LIST_KEY, &d) 95 | } 96 | }); 97 | to_res(r) 98 | */ 99 | // to_res(db::write(TABLE, &v.var_name, &v)) 100 | if let Some(robot_id) = q.get("robotId") { 101 | to_res(db_executor!( 102 | db::write, 103 | robot_id, 104 | TABLE_SUFFIX, 105 | &v.var_name, 106 | &v 107 | )) 108 | } else { 109 | to_res(Err(Error::WithMessage(String::from( 110 | "Parameter: robotId is missing.", 111 | )))) 112 | } 113 | } 114 | 115 | // fn t1 R>(s: String, mut f: F) -> R { 116 | // f(s) 117 | // } 118 | 119 | // fn t2() -> Result<()> { 120 | // let r = t1(String::new(), |s| Ok(())); 121 | // r 122 | // } 123 | 124 | pub(crate) async fn delete( 125 | Query(q): Query>, 126 | Json(v): Json, 127 | ) -> impl IntoResponse { 128 | /* 129 | let r = v 130 | .var_name 131 | .parse::() 132 | .map_err(|e| Error::ErrorWithMessage(format!("{:?}", e))) 133 | .and_then(|idx| { 134 | let mut op: Option> = db::query(TABLE, VARIABLE_LIST_KEY)?; 135 | if op.is_some() { 136 | let d = op.as_mut().unwrap(); 137 | d.remove(idx); 138 | db::write(TABLE, VARIABLE_LIST_KEY, &d)?; 139 | } 140 | Ok(()) 141 | }); 142 | to_res(r) 143 | */ 144 | // to_res(db::remove(TABLE, v.var_name.as_str())) 145 | if let Some(robot_id) = q.get("robotId") { 146 | to_res(db_executor!( 147 | db::remove, 148 | robot_id, 149 | TABLE_SUFFIX, 150 | v.var_name.as_str() 151 | )) 152 | } else { 153 | to_res(Err(Error::WithMessage(String::from( 154 | "Parameter: robotId is missing.", 155 | )))) 156 | } 157 | } 158 | 159 | pub(crate) fn get(robot_id: &str, name: &str) -> Result> { 160 | /* 161 | db::query(TABLE, VARIABLE_LIST_KEY).and_then(|op: Option>| { 162 | if let Some(d) = op { 163 | for v in d { 164 | if v.var_name.eq(name) { 165 | return Ok(Some(v.clone())); 166 | } 167 | } 168 | } 169 | return Ok(None); 170 | }) 171 | */ 172 | // db::query(TABLE, name) 173 | db_executor!(db::query, robot_id, TABLE_SUFFIX, name) 174 | } 175 | 176 | pub(crate) async fn get_value(name: &str, req: &Request, ctx: &mut Context) -> String { 177 | if let Ok(Some(v)) = get(&req.robot_id, name) { 178 | if let Some(val) = v.get_value2(req, ctx).await { 179 | return val.val_to_string(); 180 | } 181 | } 182 | String::new() 183 | } 184 | -------------------------------------------------------------------------------- /sdk/python/sdk.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from enum import Enum 3 | from dataclasses import dataclass, field, asdict 4 | from typing import List, Optional 5 | from pydantic import BaseModel 6 | 7 | 8 | class Status(Enum): 9 | SUCCESS = "success" 10 | FAILURE = "failure" 11 | 12 | 13 | class VarKind(Enum): 14 | STRING = "String" 15 | NUMBER = "Number" 16 | 17 | 18 | class UserInputResult(Enum): 19 | SUCCESSFUL = "Successful" 20 | TIMEOUT = "Timeout" 21 | 22 | 23 | class NextAction(Enum): 24 | TERMINATE = "Terminate" 25 | NONE = "None" 26 | 27 | 28 | class VarDataBase(BaseModel): 29 | varName: str 30 | varValue: str 31 | 32 | 33 | class ImportVariable(VarDataBase): 34 | varKind: VarKind 35 | 36 | 37 | @dataclass 38 | class RequestData: 39 | robotId: str = "" 40 | mainFlowId: str = "" 41 | sessionId: str = "" 42 | userInputResult: Optional[UserInputResult] = None 43 | userInput: str = "" 44 | importVariables: List[ImportVariable] = field(default_factory=list) 45 | userInputIntent: str = "" 46 | 47 | def set_robot_id(self, robotId: str): 48 | self.robotId = robotId 49 | 50 | def set_main_flow_id(self, mainFlowId: str): 51 | self.mainFlowId = mainFlowId 52 | 53 | def set_session_id(self, sessionId: str): 54 | self.sessionId = sessionId 55 | 56 | def set_user_input_result(self, userInputResult: UserInputResult): 57 | self.userInputResult = userInputResult 58 | 59 | def set_user_input(self, userInput: str): 60 | self.userInput = userInput 61 | 62 | def add_import_variable(self, importVariable: ImportVariable): 63 | self.importVariables.append(importVariable) 64 | 65 | def set_user_input_intent(self, userInputIntent: str): 66 | self.userInputIntent = userInputIntent 67 | 68 | def to_dict(self): 69 | data = asdict(self) 70 | if data['userInputResult'] is not None: 71 | data['userInputResult'] = data['userInputResult'].value 72 | return data 73 | 74 | 75 | class Answer(BaseModel): 76 | content: str 77 | contentType: str 78 | 79 | 80 | class ExtraData(BaseModel): 81 | externalLink: str 82 | 83 | 84 | class ResponseData(BaseModel): 85 | sessionId: str 86 | answers: List['Answer'] 87 | collectData: List['CollectData'] 88 | nextAction: NextAction 89 | extraData: ExtraData 90 | 91 | 92 | class CollectData(VarDataBase): 93 | pass 94 | 95 | 96 | ResponseData.update_forward_refs() 97 | 98 | 99 | class Response(BaseModel): 100 | status: int 101 | data: ResponseData 102 | err: Optional[str] = None 103 | 104 | 105 | class DialogFlowAiSDK: 106 | def __init__(self, endpoint: str): 107 | if not endpoint: 108 | raise ValueError("Endpoint cannot be empty.") 109 | if not (endpoint.startswith("http://") or endpoint.startswith("https://")): 110 | raise ValueError("Endpoint must start with 'http://' or 'https://'.") 111 | self.endpoint = endpoint 112 | 113 | def send_post_request(self, data: RequestData) -> Response: 114 | # Validate and set default values if necessary 115 | if data.userInput is None: 116 | data.userInput = "" 117 | if data.userInputResult is None: 118 | data.userInputResult = UserInputResult.SUCCESSFUL 119 | 120 | try: 121 | print(data.to_dict()) 122 | res = requests.post(self.endpoint, json=data.to_dict()) 123 | if res.status_code == 200: 124 | return Response(**res.json()) 125 | else: 126 | return Response( 127 | status=res.status_code, 128 | data=ResponseData( 129 | sessionId="", 130 | answers=[], 131 | collectData=[], 132 | nextAction=NextAction.NONE, 133 | extraData=ExtraData(externalLink="") 134 | ), 135 | err=res.text 136 | ) 137 | except requests.exceptions.RequestException as e: 138 | return Response( 139 | status=-1, 140 | data=ResponseData( 141 | sessionId="", 142 | answers=[], 143 | collectData=[], 144 | nextAction=NextAction.NONE, 145 | extraData=ExtraData(externalLink="") 146 | ), 147 | err=str(e) 148 | ) 149 | 150 | 151 | # 示例用法 152 | if __name__ == "__main__": 153 | try: 154 | sdk = DialogFlowAiSDK("http://127.0.0.1:12715/flow/answer") 155 | request_data = RequestData( 156 | robotId="r03dkmf6c5psjbiwx52gr436pu", 157 | mainFlowId="103dkmf6cu67ip1l7sidpmt889" 158 | ) 159 | 160 | while True: 161 | try: 162 | response = sdk.send_post_request(request_data) 163 | 164 | if response is None: 165 | print("Response is None") 166 | break 167 | 168 | if response.status != 200: 169 | print(f"Response failed with status code: {response.status}") 170 | if response.err: 171 | print(f"Error: {response.err}") 172 | break 173 | 174 | if response.data is None: 175 | print("Response data is None") 176 | break 177 | 178 | for answer in response.data.answers: 179 | print(f"Answer: {answer.content} (Type: {answer.contentType})") 180 | 181 | if response.data.nextAction == NextAction.TERMINATE: 182 | print("Terminating the conversation.") 183 | break 184 | 185 | if request_data.userInputIntent is not None and len(request_data.userInputIntent) == 0: 186 | request_data.userInputIntent = None 187 | 188 | if request_data.sessionId is None or len(request_data.sessionId) == 0: 189 | request_data.sessionId = response.data.sessionId 190 | 191 | user_input = input("Input your question: ") 192 | request_data.set_user_input(user_input) 193 | 194 | except Exception as e: 195 | print(f"An error occurred: {e}") 196 | 197 | except ValueError as ve: 198 | print(f"Invalid endpoint: {ve}") 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/flow/rt/node_typetag.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::condition::ConditionData; 4 | use super::context::Context; 5 | use super::dto::{CollectData, Request, Response}; 6 | use crate::flow::canvas::dto::NextActionType; 7 | use crate::flow::rt::collector; 8 | use crate::result::Result; 9 | use crate::variable::crud as variable; 10 | use crate::variable::dto::{VariableType, VariableValue}; 11 | 12 | const VAR_WRAP_SYMBOL: char = '`'; 13 | 14 | // pub(crate) trait RuntimeNode { 15 | // type Request; 16 | // type Context; 17 | // type Response; 18 | // fn exec(&self, req: Self::Request, ctx: Self::Context, response: Self::Response,); 19 | // } 20 | 21 | // type BoxedRuntimeNode = Box>; 22 | 23 | // trait RuntimeNode<'a>: erased_serde::Deserializer<'a> + erased_serde::Serialize { 24 | // fn exec(&self, req: Request, ctx: Context, response: Response); 25 | // } 26 | 27 | // pub(super) type BoxedRuntimeNode<'a> = Box>; 28 | 29 | #[typetag::serde(tag = "nodeType")] 30 | pub(crate) trait RuntimeNode { 31 | fn exec(&self, req: &Request, ctx: &mut Context, response: &mut Response) -> bool; 32 | } 33 | 34 | pub(crate) type BoxedRuntimeNode = Box; 35 | 36 | fn replace_vars(text: &str, req: &Request, ctx: &Context) -> Result { 37 | let mut new_str = String::with_capacity(128); 38 | let mut start = 0usize; 39 | loop { 40 | if let Some(mut begin) = text[start..].find(VAR_WRAP_SYMBOL) { 41 | begin = start + begin; 42 | new_str.push_str(&text[start..begin]); 43 | if let Some(mut end) = text[begin + 1..].find(VAR_WRAP_SYMBOL) { 44 | end = begin + end + 1; 45 | // println!("{} {} {} {}", &text[begin + 1..],start, begin,end); 46 | let var = variable::get(&text[begin + 1..end])?; 47 | if let Some(v) = var { 48 | let value = v.get_value(req, ctx); 49 | new_str.push_str(&value); 50 | start = end + 1; 51 | } else { 52 | new_str.push_str(&text[begin..end]); 53 | start = end; 54 | } 55 | // new_str.push_str(&variable::get_value(&text[begin + 1..end - 1], req, ctx)); 56 | } else { 57 | start = begin; 58 | break; 59 | } 60 | } else { 61 | break; 62 | } 63 | } 64 | new_str.push_str(&text[start..]); 65 | Ok(new_str) 66 | } 67 | 68 | fn add_next_node(ctx: &mut Context, next_node_id: &str) { 69 | ctx.add_node(next_node_id); 70 | } 71 | 72 | #[derive(Deserialize, Serialize)] 73 | pub(in crate::flow::rt) struct TextNode { 74 | pub(super) text: String, 75 | pub(super) ret: bool, 76 | pub(super) next_node_id: String, 77 | } 78 | 79 | #[typetag::serde] 80 | impl RuntimeNode for TextNode { 81 | fn exec(&self, req: &Request, ctx: &mut Context, response: &mut Response) -> bool { 82 | // println!("Into TextNode"); 83 | // let now = std::time::Instant::now(); 84 | match replace_vars(&self.text, &req, &ctx) { 85 | Ok(answer) => response.answers.push(answer), 86 | Err(e) => log::error!("{:?}", e), 87 | }; 88 | add_next_node(ctx, &self.next_node_id); 89 | // println!("TextNode used time:{:?}", now.elapsed()); 90 | self.ret 91 | } 92 | } 93 | 94 | #[derive(Deserialize, Serialize)] 95 | pub(in crate::flow::rt) struct GotoAnotherNode { 96 | pub(super) next_node_id: String, 97 | } 98 | 99 | #[typetag::serde] 100 | impl RuntimeNode for GotoAnotherNode { 101 | fn exec(&self, _req: &Request, ctx: &mut Context, _response: &mut Response) -> bool { 102 | // println!("Into GotoAnotherNode"); 103 | add_next_node(ctx, &self.next_node_id); 104 | false 105 | } 106 | } 107 | 108 | #[derive(Deserialize, Serialize)] 109 | pub(in crate::flow::rt) struct CollectNode { 110 | pub(super) var_name: String, 111 | pub(super) collect_type: collector::CollectType, 112 | pub(super) successful_node_id: String, 113 | pub(super) failed_node_id: String, 114 | } 115 | 116 | #[typetag::serde] 117 | impl RuntimeNode for CollectNode { 118 | fn exec(&self, req: &Request, ctx: &mut Context, response: &mut Response) -> bool { 119 | // println!("Into CollectNode"); 120 | if let Some(r) = collector::collect(&req.user_input, &self.collect_type) { 121 | let v = VariableValue { 122 | var_name: self.var_name.clone(), 123 | var_type: VariableType::String, 124 | var_value: String::from(r), 125 | }; 126 | ctx.vars.push(v); 127 | let collect_data = CollectData { 128 | var_name: self.var_name.clone(), 129 | value: String::from(r), 130 | }; 131 | response.collect_data.push(collect_data); 132 | add_next_node(ctx, &self.successful_node_id); 133 | // println!("{} {}", r, &self.successful_node_id); 134 | } else { 135 | add_next_node(ctx, &self.failed_node_id); 136 | } 137 | false 138 | } 139 | } 140 | 141 | #[derive(Deserialize, Serialize)] 142 | pub(in crate::flow::rt) struct ConditionNode { 143 | pub(super) next_node_id: String, 144 | pub(super) goto_node_id: String, 145 | pub(super) conditions: Vec>, 146 | } 147 | 148 | #[typetag::serde] 149 | impl RuntimeNode for ConditionNode { 150 | fn exec(&self, req: &Request, ctx: &mut Context, _response: &mut Response) -> bool { 151 | // println!("Into ConditionNode"); 152 | let mut r = false; 153 | for and_conditions in self.conditions.iter() { 154 | for cond in and_conditions.iter() { 155 | r = cond.compare(req, ctx); 156 | if !r { 157 | break; 158 | } 159 | } 160 | if r { 161 | add_next_node(ctx, &self.goto_node_id); 162 | return false; 163 | } 164 | } 165 | add_next_node(ctx, &self.next_node_id); 166 | false 167 | } 168 | } 169 | 170 | #[derive(Deserialize, Serialize)] 171 | pub(in crate::flow::rt) struct TerminateNode {} 172 | 173 | #[typetag::serde] 174 | impl RuntimeNode for TerminateNode { 175 | fn exec(&self, _req: &Request, _ctx: &mut Context, response: &mut Response) -> bool { 176 | // println!("Into TerminateNode"); 177 | response.next_action = NextActionType::Terminate; 178 | true 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/flow/subflow/crud.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{LazyLock, Mutex}; 2 | 3 | use axum::Json; 4 | use axum::extract::Query; 5 | use axum::http::{StatusCode, header::HeaderMap}; 6 | use axum::response::{IntoResponse, Response}; 7 | // use redb::TableDefinition; 8 | 9 | use super::dto::{SubFlowDetail, SubFlowFormData}; 10 | use crate::db; 11 | use crate::db_executor; 12 | use crate::flow::demo; 13 | use crate::result::{Error, Result}; 14 | use crate::web::server::{self, to_res}; 15 | 16 | pub(crate) const TABLE_SUFFIX: &str = "subflows"; 17 | // pub(crate) const TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("subflows"); 18 | // pub(crate) const SUB_FLOW_LIST_KEY: &str = "subflows"; 19 | static LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); 20 | 21 | // pub(crate) fn init(is_en: bool, mainflow_id: &str) -> Result<()> { 22 | // let flow = vec![SubFlowDetail::new(if is_en { 23 | // "First sub-flow" 24 | // } else { 25 | // "第一个子流程" 26 | // })]; 27 | // db::write(TABLE, mainflow_id, &flow) 28 | // } 29 | 30 | pub(crate) async fn list(headers: HeaderMap, Query(q): Query) -> Response { 31 | let is_en = server::is_en(&headers); 32 | let template = demo::get_demo(is_en, &q.main_flow_id); 33 | if let Some(t) = template { 34 | return (StatusCode::OK, t).into_response(); 35 | } 36 | // to_res::>>(db::query(TABLE, q.main_flow_id.as_str())).into_response() 37 | to_res::>>(db_executor!( 38 | db::query, 39 | &q.robot_id, 40 | TABLE_SUFFIX, 41 | q.main_flow_id.as_str() 42 | )) 43 | .into_response() 44 | // let r = db::process_data(FLOW_LIST_KEY, |mut flows: Vec| { 45 | // flows.iter_mut().for_each(|f| f.nodes.clear()); 46 | // Ok(flows) 47 | // }); 48 | // to_res(r) 49 | } 50 | 51 | pub(crate) async fn simple_list(Query(q): Query) -> Response { 52 | // let r: Result>> = db::query(TABLE, q.main_flow_id.as_str()); 53 | let r: Result>> = db_executor!( 54 | db::query, 55 | &q.robot_id, 56 | TABLE_SUFFIX, 57 | q.main_flow_id.as_str() 58 | ); 59 | if let Ok(Some(mut d)) = r { 60 | for f in d.iter_mut() { 61 | f.canvas.clear(); 62 | } 63 | return to_res::>(Ok(d)).into_response(); 64 | } 65 | "[]".into_response() 66 | } 67 | 68 | pub(crate) fn new_subflow( 69 | robot_id: &str, 70 | mainflow_id: &str, 71 | subflow_name: &str, 72 | ) -> Result> { 73 | let _lock = LOCK.lock(); 74 | db_executor!(db::query, robot_id, TABLE_SUFFIX, mainflow_id) 75 | .map(|op: Option>| { 76 | let mut subflow = SubFlowDetail::new(subflow_name); 77 | if let Some(mut flows) = op { 78 | flows.push(subflow); 79 | flows 80 | } else { 81 | subflow.id.clear(); 82 | subflow.id.push_str(mainflow_id); 83 | vec![subflow] 84 | } 85 | }) 86 | .and_then(|subflows| { 87 | db_executor!(db::write, robot_id, TABLE_SUFFIX, mainflow_id, &subflows)?; 88 | Ok(subflows) 89 | }) 90 | } 91 | 92 | pub(crate) async fn new(Query(form): Query) -> impl IntoResponse { 93 | to_res(new_subflow(&form.robot_id, &form.main_flow_id, &form.data)) 94 | } 95 | 96 | pub(crate) async fn save( 97 | Query(q): Query, 98 | Json(data): Json, 99 | ) -> impl IntoResponse { 100 | let r: Result> = q 101 | .data 102 | .parse::() 103 | .map_err(|e| Error::WithMessage(format!("{e:?}"))) 104 | .and_then(|idx| { 105 | // let op: Option> = db::query(TABLE, form.main_flow_id.as_str())?; 106 | let op: Option> = db_executor!( 107 | db::query, 108 | &q.robot_id, 109 | TABLE_SUFFIX, 110 | q.main_flow_id.as_str() 111 | )?; 112 | if let Some(mut flows) = op { 113 | if let Some(flow) = flows.get_mut(idx) { 114 | flow.canvas = data.canvas.clone(); 115 | // db::write(TABLE, &q.main_flow_id, &flows)?; 116 | db_executor!( 117 | db::write, 118 | &q.robot_id, 119 | TABLE_SUFFIX, 120 | &q.main_flow_id, 121 | &flows 122 | )?; 123 | } 124 | Ok(flows) 125 | } else { 126 | Ok(vec![]) 127 | } 128 | }); 129 | to_res(r) 130 | } 131 | 132 | pub(crate) async fn delete(Query(q): Query) -> impl IntoResponse { 133 | let r = q 134 | .data 135 | .parse::() 136 | .map_err(|e| Error::WithMessage(format!("{e:?}"))) 137 | .and_then(|idx| { 138 | let result: Result>> = db_executor!( 139 | db::query, 140 | &q.robot_id, 141 | TABLE_SUFFIX, 142 | q.main_flow_id.as_str() 143 | ); 144 | if let Ok(Some(mut flows)) = result { 145 | if idx < flows.len() { 146 | flows.remove(idx); 147 | db_executor!( 148 | db::write, 149 | &q.robot_id, 150 | TABLE_SUFFIX, 151 | &q.main_flow_id, 152 | &flows 153 | )?; 154 | } 155 | } 156 | Ok(()) 157 | }); 158 | to_res(r) 159 | } 160 | 161 | pub(crate) async fn release( 162 | headers: HeaderMap, 163 | Query(q): Query, 164 | ) -> impl IntoResponse { 165 | // let now = std::time::Instant::now(); 166 | let is_en = server::is_en(&headers); 167 | let r = crate::flow::rt::convertor::convert_flow(is_en, &q.robot_id, &q.main_flow_id); 168 | // println!("release used time:{:?}", now.elapsed()); 169 | to_res(r) 170 | } 171 | 172 | pub(crate) async fn output(Query(q): Query) -> impl IntoResponse { 173 | // let flows: Option> = db::query(TABLE, q.main_flow_id.as_str()).unwrap(); 174 | let flows: Option> = db_executor!( 175 | db::query, 176 | &q.robot_id, 177 | TABLE_SUFFIX, 178 | q.main_flow_id.as_str() 179 | ) 180 | .unwrap(); 181 | serde_json::to_string(&flows).unwrap() 182 | } 183 | --------------------------------------------------------------------------------
http://127.0.0.1:12715/flow/answer