diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..58b3427 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/*venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/.db +**/.python-version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bb2bad --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.vscode/ +*.pyc +*.log +rootfs/email/config.local.toml +rootfs/data +venv +aios_shell.log +history.txt +aios_shell_history.txt +math_school_env.db +workflows.db + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2045f08 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11 +WORKDIR /opt/aios +COPY ./src /opt/aios +COPY ./rootfs /opt/aios/app + +RUN mkdir -p /root/myai/app +RUN mkdir -p /root/myai/data +RUN mkdir -p /root/myai/etc + +RUN pip install --no-cache-dir -r /opt/aios/requirements.txt + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +CMD ["python3","./service/aios_shell/aios_shell.py"] \ No newline at end of file diff --git a/PoC/README.md b/PoC/README.md new file mode 100644 index 0000000..7fea841 --- /dev/null +++ b/PoC/README.md @@ -0,0 +1,90 @@ +# **OpenDAN: Personal AI OS** +[](https://opendan.ai) +[](https://github.com/fiatrete/OpenDAN-Personal-AI-OS/stargazers) +[](https://twitter.com/openDAN_AI) + +OpenDAN is an open source Personal AI OS , which consolidates various AI modules in one place for your personal use. + +## **Project Introduction** + +The goal of OpenDAN (Open and Do Anything Now with AI) is to create a Personal AI OS , which provides a runtime environment for various Al modules as well as protocols for interoperability between them. With OpenDAN, users can securely collaborate with various AI modules using their private data to create powerful personal AI agents, such as butler, lawyer, doctor, teacher, assistant, girl or boy friends. + +This project is still in its very early stages, and there may be significant changes in the future. + +## **Updates** + +### 1. Adding Knowledge Base Infrastructure +We're currently working on implementing the infrastructure for the Knowledge Base, aiming to enhance AI agent's access to Personal Data stored on Personal Servers. Leveraging these new features, we want to empower everyone to build their own personal homepage in the AI era using OpenDAN. + +### 2. Development Based on SourceDAO Contract +We are in the process of developing the OpenDAN's DAO page, which is based on the SourceDAO contract. The future of OpenDAN should be determined by the community. + +### 3. Welcoming waterflier as a Core Contributor +We're excited to officially invite waterflier to join as a core contributor to our community. We recognize and appreciate his innovative ideas and deep insights in the realm of Personal AI OS. Waterflier will be spearheading the development of the MVP version. As a key contributor to the CYFS CoreDev Team, he brings extensive experience in the domain of Personal Servers. + +## **Intro video - What is OpenDAN?** +Click the image below for a demo: + +[](https://www.youtube.com/watch?v=l2QmsIOXhdQ "Intro Video") + +## **Demo video - What can OpenDAN do?** +Click the image below for a demo: + +[](https://youtu.be/13wdyoT0VHQ "Demo Video") + +
[^\r]+?<\/pre>)/gm,function(e,a){return a.replace(/^ /gm,"¨0").replace(/¨0/g,"")}),P.subParser("makehtml.hashBlock")("\n"+e+"\n
",a,r)}),e=r.converter._dispatch("makehtml.blockQuotes.after",e,a,r).getText()}),P.subParser("makehtml.codeBlocks",function(e,n,s){"use strict";e=s.converter._dispatch("makehtml.codeBlocks.before",e,n,s).getText();return e=(e=(e+="¨0").replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g,function(e,a,r){var t="\n",a=P.subParser("makehtml.outdent")(a,n,s);return a=P.subParser("makehtml.encodeCode")(a,n,s),a=""+(a=(a=(a=P.subParser("makehtml.detab")(a,n,s)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+(t=n.omitExtraWLInCodeBlocks?"":t)+"
",P.subParser("makehtml.hashBlock")(a,n,s)+r})).replace(/¨0/,""),e=s.converter._dispatch("makehtml.codeBlocks.after",e,n,s).getText()}),P.subParser("makehtml.codeSpans",function(e,n,s){"use strict";return e=(e=void 0===(e=s.converter._dispatch("makehtml.codeSpans.before",e,n,s).getText())?"":e).replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(e,a,r,t){return t=(t=t.replace(/^([ \t]*)/g,"")).replace(/[ \t]*$/g,""),t=a+""+(t=P.subParser("makehtml.encodeCode")(t,n,s))+"",t=P.subParser("makehtml.hashHTMLSpans")(t,n,s)}),e=s.converter._dispatch("makehtml.codeSpans.after",e,n,s).getText()}),P.subParser("makehtml.completeHTMLDocument",function(e,a,r){"use strict";if(!a.completeHTMLDocument)return e;e=r.converter._dispatch("makehtml.completeHTMLDocument.before",e,a,r).getText();var t,n="html",s="\n",i="",o='\n',l="",c="";for(t in void 0!==r.metadata.parsed.doctype&&(s="\n","html"!==(n=r.metadata.parsed.doctype.toString().toLowerCase())&&"html5"!==n||(o='')),r.metadata.parsed)if(r.metadata.parsed.hasOwnProperty(t))switch(t.toLowerCase()){case"doctype":break;case"title":i=""+r.metadata.parsed.title+" \n";break;case"charset":o="html"===n||"html5"===n?'\n':'\n';break;case"language":case"lang":l=' lang="'+r.metadata.parsed[t]+'"',c+='\n';break;default:c+='\n'}return e=s+"\n\n"+i+o+c+"\n\n"+e.trim()+"\n\n",e=r.converter._dispatch("makehtml.completeHTMLDocument.after",e,a,r).getText()}),P.subParser("makehtml.detab",function(e,a,r){"use strict";return e=(e=(e=(e=(e=(e=r.converter._dispatch("makehtml.detab.before",e,a,r).getText()).replace(/\t(?=\t)/g," ")).replace(/\t/g,"¨A¨B")).replace(/¨B(.+?)¨A/g,function(e,a){for(var r=a,t=4-r.length%4,n=0;n/g,">"),e=r.converter._dispatch("makehtml.encodeAmpsAndAngles.after",e,a,r).getText()}),P.subParser("makehtml.encodeBackslashEscapes",function(e,a,r){"use strict";return e=(e=(e=r.converter._dispatch("makehtml.encodeBackslashEscapes.before",e,a,r).getText()).replace(/\\(\\)/g,P.helper.escapeCharactersCallback)).replace(/\\([`*_{}\[\]()>#+.!~=|:-])/g,P.helper.escapeCharactersCallback),e=r.converter._dispatch("makehtml.encodeBackslashEscapes.after",e,a,r).getText()}),P.subParser("makehtml.encodeCode",function(e,a,r){"use strict";return e=(e=r.converter._dispatch("makehtml.encodeCode.before",e,a,r).getText()).replace(/&/g,"&").replace(//g,">").replace(/([*_{}\[\]\\=~-])/g,P.helper.escapeCharactersCallback),e=r.converter._dispatch("makehtml.encodeCode.after",e,a,r).getText()}),P.subParser("makehtml.escapeSpecialCharsWithinTagAttributes",function(e,a,r){"use strict";return e=(e=(e=r.converter._dispatch("makehtml.escapeSpecialCharsWithinTagAttributes.before",e,a,r).getText()).replace(/<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi,function(e){return e.replace(/(.)<\/?code>(?=.)/g,"$1`").replace(/([\\`*_~=|])/g,P.helper.escapeCharactersCallback)})).replace(/-]|-[^>])(?:[^-]|-[^-])*)--)>/gi,function(e){return e.replace(/([\\`*_~=|])/g,P.helper.escapeCharactersCallback)}),e=r.converter._dispatch("makehtml.escapeSpecialCharsWithinTagAttributes.after",e,a,r).getText()}),P.subParser("makehtml.githubCodeBlocks",function(e,s,i){"use strict";return s.ghCodeBlocks?(e=i.converter._dispatch("makehtml.githubCodeBlocks.before",e,s,i).getText(),e=(e=(e+="¨0").replace(/(?:^|\n) {0,3}(```+|~~~+) *([^\n\t`~]*)\n([\s\S]*?)\n {0,3}\1/g,function(e,a,r,t){var n=s.omitExtraWLInCodeBlocks?"":"\n";return r=r.trim().split(" ")[0],t=P.subParser("makehtml.encodeCode")(t,s,i),t=""+(t=(t=(t=P.subParser("makehtml.detab")(t,s,i)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+n+"
",t=P.subParser("makehtml.hashBlock")(t,s,i),"\n\n¨G"+(i.ghCodeBlocks.push({text:e,codeblock:t})-1)+"G\n\n"})).replace(/¨0/,""),i.converter._dispatch("makehtml.githubCodeBlocks.after",e,s,i).getText()):e}),P.subParser("makehtml.hashBlock",function(e,a,r){"use strict";return e=(e=r.converter._dispatch("makehtml.hashBlock.before",e,a,r).getText()).replace(/(^\n+|\n+$)/g,""),e="\n\n¨K"+(r.gHtmlBlocks.push(e)-1)+"K\n\n",e=r.converter._dispatch("makehtml.hashBlock.after",e,a,r).getText()}),P.subParser("makehtml.hashCodeTags",function(e,n,s){"use strict";e=s.converter._dispatch("makehtml.hashCodeTags.before",e,n,s).getText();return e=P.helper.replaceRecursiveRegExp(e,function(e,a,r,t){r=r+P.subParser("makehtml.encodeCode")(a,n,s)+t;return"¨C"+(s.gHtmlSpans.push(r)-1)+"C"},"]*>","","gim"),e=s.converter._dispatch("makehtml.hashCodeTags.after",e,n,s).getText()}),P.subParser("makehtml.hashElement",function(e,a,r){"use strict";return function(e,a){return a=(a=(a=a.replace(/\n\n/g,"\n")).replace(/^\n/,"")).replace(/\n+$/g,""),a="\n\n¨K"+(r.gHtmlBlocks.push(a)-1)+"K\n\n"}}),P.subParser("makehtml.hashHTMLBlocks",function(e,a,n){"use strict";e=n.converter._dispatch("makehtml.hashHTMLBlocks.before",e,a,n).getText();function r(e,a,r,t){return-1!==r.search(/\bmarkdown\b/)&&(e=r+n.converter.makeHtml(a)+t),"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"}var t=["pre","div","h1","h2","h3","h4","h5","h6","blockquote","table","dl","ol","ul","script","noscript","form","fieldset","iframe","math","style","section","header","footer","nav","article","aside","address","audio","canvas","figure","hgroup","output","video","details","p"];a.backslashEscapesHTMLTags&&(e=e.replace(/\\<(\/?[^>]+?)>/g,function(e,a){return"<"+a+">"}));for(var s=0;s]*>)","im"),o="<"+t[s]+"\\b[^>]*>",l=""+t[s]+">";-1!==(c=P.helper.regexIndexOf(e,i));){var c=P.helper.splitAtIndex(e,c),u=P.helper.replaceRecursiveRegExp(c[1],r,o,l,"im");if(u===c[1])break;e=c[0].concat(u)}return e=e.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,P.subParser("makehtml.hashElement")(e,a,n)),e=(e=P.helper.replaceRecursiveRegExp(e,function(e){return"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"},"^ {0,3}\x3c!--","--\x3e","gm")).replace(/\n\n( {0,3}<([?%])[^\r]*?\2>[ \t]*(?=\n{2,}))/g,P.subParser("makehtml.hashElement")(e,a,n)),e=n.converter._dispatch("makehtml.hashHTMLBlocks.after",e,a,n).getText()}),P.subParser("makehtml.hashHTMLSpans",function(e,a,r){"use strict";return e=(e=(e=(e=(e=r.converter._dispatch("makehtml.hashHTMLSpans.before",e,a,r).getText()).replace(/<[^>]+?\/>/gi,function(e){return P.helper._hashHTMLSpan(e,r)})).replace(/<([^>]+?)>[\s\S]*?<\/\1>/g,function(e){return P.helper._hashHTMLSpan(e,r)})).replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g,function(e){return P.helper._hashHTMLSpan(e,r)})).replace(/<[^>]+?>/gi,function(e){return P.helper._hashHTMLSpan(e,r)}),e=r.converter._dispatch("makehtml.hashHTMLSpans.after",e,a,r).getText()}),P.subParser("makehtml.unhashHTMLSpans",function(e,a,r){"use strict";e=r.converter._dispatch("makehtml.unhashHTMLSpans.before",e,a,r).getText();for(var t=0;t]*>\\s*]*>","^ {0,3}\\s* ","gim"),e=s.converter._dispatch("makehtml.hashPreCodeTags.after",e,n,s).getText()}),P.subParser("makehtml.headers",function(e,n,s){"use strict";e=s.converter._dispatch("makehtml.headers.before",e,n,s).getText();var i=isNaN(parseInt(n.headerLevelStart))?1:parseInt(n.headerLevelStart),a=n.smoothLivePreview?/^(.+)[ \t]*\n={2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n=+[ \t]*\n+/gm,r=n.smoothLivePreview?/^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n-+[ \t]*\n+/gm,a=(e=(e=e.replace(a,function(e,a){var r=P.subParser("makehtml.spanGamut")(a,n,s),a=n.noHeaderId?"":' id="'+o(a)+'"',a="]*>/.test(c)&&(u=!0)}n[i]=c}return e=(e=(e=n.join("\n")).replace(/^\n+/g,"")).replace(/\n+$/g,""),r.converter._dispatch("makehtml.paragraphs.after",e,a,r).getText()}),P.subParser("makehtml.runExtension",function(e,a,r,t){"use strict";return e.filter?a=e.filter(a,t.converter,r):e.regex&&((t=e.regex)instanceof RegExp||(t=new RegExp(t,"g")),a=a.replace(t,e.replace)),a}),P.subParser("makehtml.spanGamut",function(e,a,r){"use strict";return e=r.converter._dispatch("makehtml.span.before",e,a,r).getText(),e=P.subParser("makehtml.codeSpans")(e,a,r),e=P.subParser("makehtml.escapeSpecialCharsWithinTagAttributes")(e,a,r),e=P.subParser("makehtml.encodeBackslashEscapes")(e,a,r),e=P.subParser("makehtml.images")(e,a,r),e=r.converter._dispatch("smakehtml.links.before",e,a,r).getText(),e=P.subParser("makehtml.links")(e,a,r),e=r.converter._dispatch("smakehtml.links.after",e,a,r).getText(),e=P.subParser("makehtml.emoji")(e,a,r),e=P.subParser("makehtml.underline")(e,a,r),e=P.subParser("makehtml.italicsAndBold")(e,a,r),e=P.subParser("makehtml.strikethrough")(e,a,r),e=P.subParser("makehtml.ellipsis")(e,a,r),e=P.subParser("makehtml.hashHTMLSpans")(e,a,r),e=P.subParser("makehtml.encodeAmpsAndAngles")(e,a,r),a.simpleLineBreaks?/\n\n¨K/.test(e)||(e=e.replace(/\n+/g,"
\n")):e=e.replace(/ +\n/g,"
\n"),e=r.converter._dispatch("makehtml.spanGamut.after",e,a,r).getText()}),P.subParser("makehtml.strikethrough",function(e,a,r){"use strict";return a.strikethrough&&(e=(e=r.converter._dispatch("makehtml.strikethrough.before",e,a,r).getText()).replace(/(?:~){2}([\s\S]+?)(?:~){2}/g,function(e,a){return""+a+""}),e=r.converter._dispatch("makehtml.strikethrough.after",e,a,r).getText()),e}),P.subParser("makehtml.stripLinkDefinitions",function(o,l,c){"use strict";function e(e,a,r,t,n,s,i){return a=a.toLowerCase(),o.toLowerCase().split(a).length-1<2?e:(r.match(/^data:.+?\/.+?;base64,/)?c.gUrls[a]=r.replace(/\s/g,""):(r=P.helper.applyBaseUrl(l.relativePathBaseUrl,r),c.gUrls[a]=P.subParser("makehtml.encodeAmpsAndAngles")(r,l,c)),s?s+i:(i&&(c.gTitles[a]=i.replace(/"|'/g,""")),l.parseImgDimensions&&t&&n&&(c.gDimensions[a]={width:t,height:n}),""))}return o=(o=(o=(o+="¨0").replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*(data:.+?\/.+?;base64,[A-Za-z0-9+/=\n]+?)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm,e)).replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*([^>\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm,e)).replace(/¨0/,"")}),P.subParser("makehtml.tables",function(e,y,x){"use strict";if(!y.tables)return e;function a(e){for(var a=e.split("\n"),r=0;r"+(n=P.subParser("makehtml.spanGamut")(n,y,x))+"\n"));for(r=0;r"+P.subParser("makehtml.spanGamut")(o,y,x)+"\n"));d.push(g)}for(var _=h,b=d,f="\n\n\n",k=_.length,w=0;w\n \n\n",w=0;w\n";for(var v=0;v\n"}return f+=" \n
\n"}return e=(e=(e=(e=x.converter._dispatch("makehtml.tables.before",e,y,x).getText()).replace(/\\(\|)/g,P.helper.escapeCharactersCallback)).replace(/^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*[-=]{2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*[-=]{2,}[\s\S]+?(?:\n\n|¨0)/gm,a)).replace(/^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*[-=]{2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm,a),e=x.converter._dispatch("makehtml.tables.after",e,y,x).getText()}),P.subParser("makehtml.underline",function(e,a,r){"use strict";return a.underline?(e=r.converter._dispatch("makehtml.underline.before",e,a,r).getText(),e=(e=a.literalMidWordUnderscores?(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,a){return""+a+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,a){return""+a+""}):(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,a){return/\S$/.test(a)?""+a+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,a){return/\S$/.test(a)?""+a+"":e})).replace(/(_)/g,P.helper.escapeCharactersCallback),r.converter._dispatch("makehtml.underline.after",e,a,r).getText()):e}),P.subParser("makehtml.unescapeSpecialChars",function(e,a,r){"use strict";return e=(e=r.converter._dispatch("makehtml.unescapeSpecialChars.before",e,a,r).getText()).replace(/¨E(\d+)E/g,function(e,a){a=parseInt(a);return String.fromCharCode(a)}),e=r.converter._dispatch("makehtml.unescapeSpecialChars.after",e,a,r).getText()}),P.subParser("makeMarkdown.blockquote",function(e,a){"use strict";var r="";if(e.hasChildNodes())for(var t=e.childNodes,n=t.length,s=0;s ")}),P.subParser("makeMarkdown.break",function(){"use strict";return" \n"}),P.subParser("makeMarkdown.codeBlock",function(e,a){"use strict";var r=e.getAttribute("language"),e=e.getAttribute("precodenum");return"```"+r+"\n"+a.preList[e]+"\n```"}),P.subParser("makeMarkdown.codeSpan",function(e){"use strict";return"`"+e.innerHTML+"`"}),P.subParser("makeMarkdown.emphasis",function(e,a){"use strict";var r="";if(e.hasChildNodes()){r+="*";for(var t=e.childNodes,n=t.length,s=0;s",e.hasAttribute("width")&&e.hasAttribute("height")&&(a+=" ="+e.getAttribute("width")+"x"+e.getAttribute("height")),e.hasAttribute("title")&&(a+=' "'+e.getAttribute("title")+'"'),a+=")"),a}),P.subParser("makeMarkdown.input",function(e,a){"use strict";for(var r="",t=(null!==e.getAttribute("checked")?r+="[x]":r+="[ ]",e.childNodes),n=t.length,s=0;s"),e.hasAttribute("title")&&(r+=' "'+e.getAttribute("title")+'"'),r+=")"}return r}),P.subParser("makeMarkdown.list",function(e,a,r){"use strict";var t="";if(!e.hasChildNodes())return"";for(var n=e.childNodes,s=n.length,i=e.getAttribute("start")||1,o=0;o"+a.preList[e]+""}),P.subParser("makeMarkdown.strikethrough",function(e,a){"use strict";var r="";if(e.hasChildNodes()){r+="~~";for(var t=e.childNodes,n=t.length,s=0;str>th"),s=e.querySelectorAll("tbody>tr"),i=0;i/g,"\\$1>")).replace(/^#/gm,"\\#")).replace(/^(\s*)([-=]{3,})(\s*)$/,"$1\\$2$3")).replace(/^( {0,3}\d+)\./gm,"$1\\.")).replace(/^( {0,3})([+-])/gm,"$1\\$2")).replace(/]([\s]*)\(/g,"\\]$1\\(")).replace(/^ {0,3}\[([\S \t]*?)]:/gm,"\\[$1]:")}),P.Converter=function(e){"use strict";var a,r,n={},o=[],l=[],c={},t=d,s={parsed:{},raw:"",format:""};for(a in e=e||{},m)m.hasOwnProperty(a)&&(n[a]=m[a]);if("object"!=typeof e)throw Error("Converter expects the passed parameter to be an object, but "+typeof e+" was passed instead.");for(r in e)e.hasOwnProperty(r)&&(n[r]=e[r]);function i(e,a){if(a=a||null,P.helper.isString(e)){if(a=e=P.helper.stdExtName(e),P.extensions[e]){console.warn("DEPRECATION WARNING: "+e+" is an old extension that uses a deprecated loading method.Please inform the developer that the extension should be updated!");var r=P.extensions[e],t=e;if("function"==typeof r&&(r=r(new P.Converter)),P.helper.isArray(r)||(r=[r]),!(t=p(r,t)).valid)throw Error(t.error);for(var n=0;n [ \t]+,">¨NBSP;<");for(var a=P.helper.document.createElement("div"),r=(a.innerHTML=e,{preList:function(e){for(var a=e.querySelectorAll("pre"),r=[],t=0;t'}else r.push(a[t].innerHTML),a[t].innerHTML="",a[t].setAttribute("prenum",t.toString());return r}(a)}),t=(!function e(a){for(var r=0;r
- 
- 
- 
-
-
-
## **Subscribe to updates here**
-https://twitter.com/openDAN_AI
+
-## **Core Features of OpenDAN**
+## **Installation**
-To achieve the goal of OpenDAN, we provide the following key features:
+There are two ways to install the Internal Test Version of OpenDAN:
-1. **Hardware-specific optimization**: Optimize for specific hardware to enable smooth local running of most open-source AI applications.
-2. **Open AI App Marketplace**: Offer a solution for one-click installation and use of various AI applications, helping users easily access and manage AI apps.
-3. **Open AI Model Solution**: Provide a unified entry point for model search, download, and access control, making it convenient for users to find and use models suitable for their needs.
-4. **Strict Privacy Protection and Management**: Strictly manage personal data, ranging from family albums to chat records and social media records, and provide a unified access control interface for AI applications.
-5. **Integrated Tools**: Offer tools for users to train their own voice models, Lora models, knowledge models, etc., using personal data.
-6. **AI Butler Assistant**: Driven by a large language model, the AI assistant completes tasks through natural language interaction.
-7. **Development Framework**: Provide a development framework for customizing AI assistants for specific purposes, making it easy for developers to create unique AI applications for users.
+1. Installation through docker, this is also the installation method we recommend now
+2. Installing through the source code, this method may encounter some traditional Pyhont dependence problems and requires you to have a certain ability to solve.But if you want to do secondary development of OpenDAN, this method is necessary.
-## **Roadmap**
+### Preparation before installation
-- [x] Project Initialization
- - [x] Basic code for operating system image packaging script
- - [x] Opendan.ai website homepage
-- [x] OpenDAN Prototype Version
- - [x] AI butler assistant driven by GPT-3.5 or GPT-4.0
- - [x] Integration of Stable Diffusion
- - [x] Integration of TTS (Text-to-Speech)
- - [x] Integration of Telegram Chatbot as an interaction entrance
-- [ ] OpenDAN 1.0
- - [ ] AI butler assistant's large language model core switched to locally running open-source model
- - [ ] Offer more AI applications through AI App Marketplace
- - [ ] Provide an AI application development framework to support the community in integrating more AI applications
- - [ ] Provide a model management framework
+1. Docker environment
+This article does not introduce how to install the docker, execute it under your console
+
+```
+docker -version
+```
+
+If you can see the docker version number (> 20.0), it means that you have installed Docker.
+If you don't know how to install docker, you can refer to [here](https://docs.docker.com/engine/install/)
+
+2. OpenAI API Token
+If there is no api token, you can apply for [here](https://beta.openai.com/)
+
+Applying for the API Token may have some thresholds for new players. You can find friends around you, and you can give you a temporary, or join our internal test experience group. We will also release some free experience API token from time to time.These token is limited to the maximum consumption and effective time
+
+### Install
+
+After executing the following command, you can install the Docker Image of OpenDAN
+
+```
+docker pull paios/aios:latest
+```
+
+## **Run OpenDAN**
+
+The first Run of OpenDAN needs to be initialized. You need to enter some information in the process of initialization. Therefore, when starting the docker, remember to bring the -it parameter.
+
+OpenDAN is your Personal AIOS, so it will generate some important personal data (such as chat history with agent, schedule data, etc.) during its operation. These data will be stored on your local disk. ThereforeWe recommend that you mount the local disk into the container of Docker so that the data can be guaranteed.
+
+```
+docker run -v /your/local/myai/:/root/myai --name aios -it paios/aios:latest
+```
+
+In the above command, we also set up a Docker instance for Docker Run named AIOS, which is convenient for subsequent operations.You can also use your favorite name instead.
+
+After the first operation of the docker instance is created, it only needs to be executed again:
+
+```
+docker start -ai aios
+```
+
+If you plan to run in a service mode (NO UI), you don't need to bring the -AI parameter:
+
+```
+docker start aios
+```
+
+### Hello, Jarvis
+
+After the configuration is completed, you will enter a AIOS Shell, which is similar to Linux Bash and similar. The meaning of this interface is:
+The current user "username" is communicating with the name "Agent/Workflow of Jarvis". The current topic is default.
+
+Say Hello to your private AI assistant Jarvis !
+
+**If everything is OK, you will get a reply from Jarvis after a moment .At this time, the OpenDAN system is running .**
+
+
+## **Core Concepts and Features of OpenDAN**
+1. **AI Agent**: Driven by a large language model, having own memory.The AI Agent completes tasks through natural language interaction.
+2. **AI Workflow**: Organize different AI Agents into an AI Agent Group to complete complex tasks.
+
+3. **AI Envriment**: Supports AI Agents to access file systems, IoT devices, network services, smart contracts, and everything on today's internet once authorized.
+4. **AI Marketplace**: Offer a solution for one-click installation and use of various AI applications, helping users easily access and manage AI apps.
+5. **AI Model Solution**: Provide a unified entry point for model search, download, and access control, making it convenient for users to find and use models suitable for their needs.
+6. **Hardware-specific optimization**: Optimize for specific hardware to enable smooth local running of most open-source AI applications.
+7. **Strict Privacy Protection and Management**: Strictly manage personal data, ranging from family albums to chat records and social media records, and provide a unified access control interface for AI applications.
+8. **Personal Knowlege Base**:
+9. **Integrated AIGC Workflow**: Offer AIGC Agent/Workflow for users to train their own voice models, Lora models, knowledge models, etc., using personal data. Based on these private model data, integrate the most advanced AIGC algorithm to help people release creativity easily and build more COOL and more personalized content.
+10. **Development Framework**: Provide a development framework for customizing AI assistants for specific purposes, making it easy for developers to create unique AI applications / service for their customers.
+
+## **Deeply Understanding OpenDAN**
+
+### Build OpenDAN from source code
+1. Install the latest version of python (>= 3.11) and pip
+2. Clone the source code
+```
+git clone https://github.com/fiatrete/OpenDAN-Personal-AI-OS.git
+cd OpenDAN-Personal-AI-OS
+```
+3. Install the dependent python library
+```
+pip install -r ./src/requirements.txt
+```
+Waiting for installation.
+
+4. Start OpenDAN through aios_shell
+```
+python ./src/srvice/aios_shell/aios_shell.py
+```
+Now OpenDAN runs in the development mode, and the directory is:
+- AIOS_ROOT: ./rootfs (/opt/aios in docker)
+- AIOS_MYAI: ~/myai (/root/myai in docer)
+
+### OpenDAN Cookbook
+
+#### Chapter 1: Hello, Jarvis!
+- 1.1 Installation of OpenDAN
+- 1.2 Initial Configuration of OpenDAN
+- 1.3 Introduction to Agent and Using Jarvis
+- 1.4 Communicating with Jarvis Anytime and Anywhere via Telegram and Email
+- 1.5 Using Jarvis in Daily Life
+- 1.6 Mia and the Knowledge Base
+- 1.7 Introduction to Other Built-in Agents
+
+[Click to Read](./doc/QuickStart.md)
+
+#### Chapter 2: AIGC Workflow (Coming Soon)
+Using Workflow to activate the AIGC feature and let the Agent team (director, artist, and narrator) collaborate to create a unique bedtime story for your child based on your instructions!
+
+- 2.1 Using Workflow `story_maker`
+- 2.2 Enabling Your Own AIGC Computation Node
+- 2.3 Training and Using Your Own AIGC LoRA Model.
+
+#### Chapter 3: Develop Agent/Workflow on OpenDAN (Writing)
+
+What's the most crucial design aspect of an operating system? Defining new forms of applications!
+
+This article will systematically introduce what future Intelligence Applications look like, how to develop and release Intelligence Applications, and how to connect new-age Intelligence Applications with traditional computing.
+
+- 3.1 Developing Agents that Run on OpenDAN
+- 3.2 Developing Workflows that Run on OpenDAN
+- 3.3 Extending the Environments Accessible by Agents
+- 3.4 Releasing Various Models Trained by Yourself
+- 3.5 Expanding More Tunnels to Enhance the Accessibility of Agents/Workflow
+- 3.6 Developing Traditional dApps on the Personal Server.
+
+#### Chapter 4: OpenDAN Kernel Development (Writing)
+This article will introduce the design and implementation of OpenDAN's architecture
+
+
+
+
+- 4.1 Integrate your own LLM core into OpenDAN.
+- 4.2 Knowledge Base: Expand more file types, allowing Agents to better understand your knowledge graph.
+- 4.3 AI computation engine, integrating more AIGC capabilities, and accessing more computational power.
+- 4.4 OpenDAN's state management: File system and vector database.
+- 4.5 Kernel services and permission isolation.
+- 4.6 Smart gateway.
+
+
+## **Upcoming Roadmap**
+
+- [x] Release PoC of OpenDAN
+- [x] **0.5.1** Implement personal data embeding to Knownlege-Base(KB) via Spider, followed by access by AI Agent
+- [ ] 0.5.2 Separate user mode and kernel mode, Knowledge Base supports scene format and more Spiders, supports personal AIGC model training
+- [ ] 0.5.3 Release Home Environment, allowing Agents to access and control your home's IoT devices
+- [ ] 0.5.x Official version of OpenDAN Alpha. Release OpenDAN SDK 1.0.
## **Contributing**
@@ -79,12 +218,14 @@ We welcome community members to contribute to the project, including but not lim
- Submit a Pull Request to the repository
- Participate in discussions and development
-## **⭐Star History**
+OpenDAN utilizes the SourceDAO smart contract to incentivize the community. Developers who contribute can receive rewards in the form of OpenDAN DAO Tokens. DAO Token holders can collaboratively determine the development direction of OpenDAN. You can learn more about the rules of SourceDAO by reading this article( https://github.com/fiatrete/OpenDAN-Personal-AI-OS/issues/25 )
+The DAO governance page for OpenDAN is under development. Once officially launched, all contributors will receive DAO Tokens according to the rules.
+
+## **⭐Star History**
[](https://star-history.com/#fiatrete/OpenDAN-Personal-AI-Server-OS&Date)
-
## **License**
-MIT
+The current license is MIT, but it will transition to SourceDAO in the future.
diff --git a/build_all_in_one.sh b/build_all_in_one.sh
new file mode 100644
index 0000000..9beaab5
--- /dev/null
+++ b/build_all_in_one.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# Build the docker image
+docker build -t aios .
+docker tag aios:latest paios/aios:latest
+docker push paios/aios:latest
diff --git a/doc/QuickStart zh-CN.md b/doc/QuickStart zh-CN.md
new file mode 100644
index 0000000..326229e
--- /dev/null
+++ b/doc/QuickStart zh-CN.md
@@ -0,0 +1,334 @@
+# OpenDAN Quick Start
+OpenDAN (Open and Do Anything Now with AI) is revolutionizing the AI landscape with its Personal AI Operating System. Designed for seamless integration of diverse AI modules, it ensures unmatched interoperability. OpenDAN empowers users to craft powerful AI agents—from butlers and assistants to personal tutors and digital companions—all while retaining control. These agents can team up to tackle complex challenges, integrate with existing services, and command smart(IoT) devices.
+
+With OpenDAN, we're putting AI in your hands, making life simpler and smarter.
+
+This project is still in its very early stages, and there may be significant changes in the future.
+
+## Installation
+
+OpenDAN的Internal test版本有两种安装方式:
+1.通过Docker安装,这也是我们现在推荐的安装方法
+2.通过源代码安装,这种方法可能会遇到一些传统的Python依赖问题,需要你有一定的解决能力。但是如果你想要对OpenDAN进行二次开发,这种方法是必须的。
+
+### 安装前准备工作
+
+1. Docker环境
+OpenDAN通过适配Docker实现了对多平台的适配。本文不介绍怎么安装Docker,在你的控制台下执行
+
+```
+docker --version
+```
+
+如果能够看到Docker的版本号(>20.0),说明你已经安装了Docker.
+不知道怎么安装Docker的话,可以参考[这里](https://docs.docker.com/engine/install/)
+
+2. OpenAI的API Token
+如果你还没有API Token的话,可以通过[这里](https://beta.openai.com/)申请
+(申请API Token对新玩家可能有一些门槛,可以在身边找找朋友,可以让他们给你一个临时的,或则加入我们的内测体验群,我们也会不时放出一些免费体验的API Token,这些Token被限制了最大消费和有效时间)
+
+#### 安装OpenDAN
+
+执行下面的命令,就可以安装OpenDAN的Docker Image了
+
+```
+docker pull paios/aios:latest
+```
+
+## 运行OpenDAN
+
+首次运行OpenDAN需要进行初始化,初始化过程中会下载一些用于本地Knowledge Base库的基础模型,并需要你输入一些个人信息,因此启动Docker的时候记住要带上 -it参数。
+OpenDAN是你的Personal AIOS,其运行过程中会产生一些重要的个人数据(比如和Agent的对话记录,日程数据等),这些数据会保存在你的本地磁盘上,因此在启动Docker的时候,要将本地磁盘挂载到Docker的容器中,这样才能保证数据的持久化。
+
+```
+docker run -v /your/local/myai/:/root/myai --name aios -it paios/aios:latest
+```
+
+在上述命令中,我们还为docker run创建的docker 实例起了一个名字叫aios,方便后续的操作。你也可以用自己喜欢的名字来代替
+
+执行上述命令后,如果一切正常,你会看到如下界面
+
+
+首次运行完成Docker实例的创建后,再次运行只需要执行:
+
+```
+docker start -ai aios
+```
+
+如果打算以服务模式运行,则不用带 -ai参数:
+
+```
+docker start aios
+```
+
+## OpenDAN的首次运行配置
+
+如果你过去没有用字符界面(CLI)的产品,可能会有一点点不习惯。但别紧张,即使在Internal Test版本中,你也只会在极少数的情况下需要使用CLI。
+
+OpenDAN必须是所有人都能轻松使用的未来操作系统,因此我们希望OpenDAN的使用和配置都是非常友好和简单的。但在Internal Test版本中,我们还没有足够的资源来实现这一目标。经过思考,我们决定先支持以CLI的方式来使用OpenDAN。
+
+OpenDAN以LLM为AIOS的内核,通过不同的Agent/Workflow整合了很多很Cool的AI功能,你能在OpenDAN里一站式的体验AI工业的一些最新的成功。激活全部的功能需要做比较多的配置,但首次运行我们只需要做两项配置就可以了
+
+1. LLM内核。OpenDAN是围绕LLM构建的未来智能操作系统,因此系统必须有至少一个LLM内核。
+ OpenDAN以Agent为单位对LLM进行配置,未指定LLM模型名的Agent将会默认使用GPT4(GPT4也是目前最聪明的LLM)。你可以修改该配置到llama或其它安装的Local LLM。今天使用Local LLM需要相当强的本地算力的支持,这需要一笔不小的一次性投入。
+ 但我们相信LLM领域也会遵循摩尔定律,未来的LLM模型会越来越强大,越来越小,越来越便宜。因此我们相信在未来,每个人都会有自己的Local LLM。
+2. 你的个人信息,这能让你的私人AI管家Jarvis更好的为你服务。注意这里一定要输入你自己正确的Telegram username ,否则由于权限控制,后续将无法通过Telegram访问OpenDAN上安装的Agent/Workflow。
+
+好的,简单的了解了上述背景后,请按界面提示完成必要信息的输入。
+
+P.S:
+上述配置会保存在`/your/local/myai/etc/system.cfg.toml`中,如果你想要修改配置,可以直接修改这个文件。如果你想要调整配置,可以直接编辑这个文件。
+
+
+## (实验性)安装本地LLM内核
+
+首次快速体验OpenDAN,我们强烈的推荐你使用GPT4,虽然它很慢,也很贵,但它也是目前最强大和稳定的LLM内核。OpenDAN在架构设计上,允许不同的Agent选择不同的LLM内核(系统里至少要有一个可用的LLM内核),如果你因为各种原因无法使用GPT4,可以是用下面方法安装Local LLM让系统能跑起来。OpenDAN是面向未来设计的系统,我们相信今天GPT4的能力一定会是未来所有LLM的下限。但目前的现实情况,其它的LLM不管是效果还是功能和GPT4都还有比较明显的差距,所以要完整体验OpenDAN,在一定时间内,我们还是推荐使用GPT4.
+
+目前我们只完成了基于Llama.cpp的Local LLM的适配,为OpenDAN适配新的LLM内核并不是复杂的工作,有需要的工程师朋友可以自行扩展(记得给我们PR~)。如果你有一定的动手能力,可以用下面的方法安装基于Llama.cpp的Compute Node:
+
+### 安装Llama.cpp ComputeNode
+
+OpenDAN支持分布式计算资源调度,因此你可以把LLaMa的计算节点安装在和OpenDAN不同的机器上。根据模型的大小需要相当的算力支持,请根据自己的机器配置量力而行。我们使用llama.cpp构建LLaMa LLM ComputeNode,llama.cpp也是一个正在高速演化的项目,正致力降低LLM的运行需要的设备门槛,提高运行速度。请阅读llamap.cpp的项目了解其支持的各个模型的最低系统要求。
+
+
+安装LLama.cpp 总共分两步:
+
+Step1: 下载LLama.cpp的模型,有3个选择:7B-Chat,13B-Chat,70B-Chat. 我们的实践经验最少需要13B的才能工作。LLaMa2 目前官方的模型并不支持inner function call,而目前OpenDAN的很多Agent都高度依赖inner function call.所以我们推荐您下载通过Fine-Tune 的 13B模型:
+
+```
+https://huggingface.co/Trelis/Llama-2-13b-chat-hf-function-calling
+```
+
+Step2 运行llama-cpp-python镜像
+
+```
+docker run --rm -it -p 8000:8000 -v /path/to/models:/models -e MODEL=/models/llama-2-13b-chat.gguf ghcr.io/abetlen/llama-cpp-python:latest
+```
+
+完成上述步骤后,如果输出如下,说明LLaMa已经正确加载模型并正常运行了
+```
+....................................................................................................
+llama_new_context_with_model: kv self size = 640.00 MB
+llama_new_context_with_model: compute buffer total size = 305.47 MB
+AVX = 1 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 0 | SSE3 = 1 | SSSE3 = 1 | VSX = 0 |
+INFO: Started server process [171]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+```
+
+### 将LLama.cpp ComputeNode增加到OpenDAN中
+
+ComputeNode是OpenDAN的底层组件,而且可能不会与OpenDAN运行在同一个机器上。因此从依赖关系的角度,OpenDAN并没有“主动检测”ComputeNode的能力,需要用户(或系统管理员)在OpenDAN的命令行中通过下面命令手工添加
+
+```
+/node add llama Llama-2-13b-chat http://localhost:8000
+```
+
+上面添加的是运行在本地的13b模型,如果你使用的是其它模型,或则跑在了不同的机器上。请修改上述命令中的模型名和端口号。
+
+### 配置Agent使用LLaMa
+
+OpenDAN的Agent可以选择最适合其职责的LLM-Model,我们内置了一个Agent叫Lachlan的私人西班牙语老师Agent,已经被配置成了使用LLaMa-2-13b-chat模型。你可以通过下面命令与其聊天:
+
+```
+/open Lachlan
+```
+
+
+因此添加了一个新的LLM后,需要手工修改Agent的配置,才能让其使用新的LLM。比如我们的私人英文老师Tracy,其配置文件是`/opt/aios/agents/Tracy/Agent.toml`,修改配置如下:
+```
+llm_model_name="Llama-2-13b-chat"
+max_token_size = 4000
+```
+然后重新启动OpenDAN,你就可以让Tracy使用LLaMa了(你也可以通过该方法查看其它内置的Agent使用了哪些LLM模型)
+
+
+## Hello, Jarvis!
+
+配置完成后,你会进入一个AIOS Shell,这和linux bash 和相似,这个界面的含义是:
+当前用户 "username" 正在 和 名“为Jarvis的Agent/Workflow” 进行交流,当前话题是default。
+和你的私人AI管家Jarvis Say Hello吧!
+
+***如果一切正常,你将会在一小会后得到Jarvis的回复。此时OpenDAN系统已经正常运行了***
+
+## 给Jarvis注册Telegram账号
+你已经完成了OpenDAN的安装和配置,并已经验证了其可以正常工作。下面让我们尽快回到熟悉的图形界面,回到移动互联网吧!
+我们将给Jarvis注册一个Telegram账号,通过Telegram,我们可以使用熟悉的方式和Jarvis进行交流了~
+在OpenDAN的aios_shell输入
+
+```
+/connect Jarvis
+```
+
+按照提示输入Telegram Bot Token就完成了Jarvis的账号注册. 你可以通过阅读下面文章来了解如何获取Telegram Bot Token
+https://core.telegram.org/bots#how-do-i-create-a-bot,
+
+我们还支持给Agent注册email账号,用下面命令行
+
+```
+/connect Jarvis email
+```
+
+然后根据提示就可以给Jarvis绑定电子邮件账号了。但由于目前系统并未对email内容定制前置过滤,所以可能会带来潜在的大量LLM访问费用,因此Email的支持是实验性的。我们推荐给Agent创建全新的电子邮件账号。
+
+## 以服务方式运行OpenDAN
+
+上述的运行方式是以交互方式运行OpenDAN,这种方式适合在开发和调试的时候使用。在实际使用的时候,我们推荐以服务方式运行OpenDAN,这样可以让OpenDAN在后台默默的运行,不会影响你的正常使用。
+先输入
+
+```
+/exit
+```
+
+关闭并退出OpenDAN,随后我们再用服务的方式启动OpenDAN:
+
+```
+docker start aios
+```
+
+Jarvis是运行在OpenDAN上的Agent,当OpenDAN退出后,其活动也会被终止。因此如果想随时随地通过Telegram和Jarvis交流,请记住保持OpenDAN的运行(不要关闭你的电脑,并保持其网络连接)。
+
+实际上,OpenDAN是一个典型的Personal OS,运行在Personal Server之上。关于Personal Servier的详细定义可以参考[CYFS Owner Online Device(OOD) ](https://github.com/buckyos/CYFS)。因此运行在PC或笔记本上并不是一个正式选择,但谁要我们正在Internal Test呢?
+
+我们正在进行的很多研发工作,其中有很大一部分的目标,就是能让你轻松的拥有一个搭载AIOS的Personal Server.相对PC,我们将把这个新设备叫PI(Personal Intelligence),OpenDAN是面向PI的首个OS。
+
+## 你的私人管家 Jarvis 前来报道!
+
+现在你已经可以随时随地通过Telegram和Jarvis交流了,但只是把他看成更易于访问的ChatGPT,未免有点小瞧他了。让我们来看一下运行在OpenDAN里的Jarvis有什么新本事吧!
+
+## 让Jarvis给你安排日程
+
+相信不少朋友有长期使用Outlook等传统Calender软件来管理自己日程的习惯。像我自己通常每周会花至少2个小时来是使用这类软件,当发生一些计划外的情况时,对计划进行手工调整是一个枯燥的工作。作为你的私人管家,Jarvis必须能够帮用自然语言的方式帮你管理日程!
+试试和Jarvis说:
+
+```
+我周六和Alic上午去爬山,下午去看电影!
+```
+
+如果一切正常,你会看到Jarvis的回复,并且已经记住了你的日程安排。
+
+你可以通过自然语言的方式和Jarvis查询
+```
+我这周末有哪些安排?
+```
+
+你会看到Jarvis的回复,其中包含了你的日程安排。
+由于Jarvis使用LLM作为思考内核,他能以非常自然的方式和你进行交流,并在合适的时候管理你的日程。比如你可以说
+
+```
+我周六有朋友从LA过来,很久没见了,所有周六的约会都移动到周日吧!
+```
+
+你会看到Jarvis会自动的帮你吧周六的日程移动到周日。
+实际上在整个交流的过程中,你不需要有明确的“使用日程管理语言的意识”,Jarvis作为你的管家,在理解你的个人数据的基础上,会在合适的时机和你进行交流,帮你管理日程。
+这是一个非常简单而又常用的例子,通过这个例子,我们可以看到未来人们不再需要学习一些今天非常重要的基础软件了。
+
+欢迎来到新时代!
+
+Agent安排的日程数据都保存在 ~/myai/calender.db 文件中,格式是sqlite DB. 我们后续计划授权让Jarvis可以操作你生产环境中的Calender(比如常用的Google Calender)。但我们还是希望未来,人们可以把重要的个人数据都保存在自己物理上拥有的Personal Server中。
+
+## 介绍Jarvis给你的朋友
+
+把Jarvis的telegram账号分享给你的朋友,可以做一些有趣的事情。比如你的朋友可以在联系不到你的时候,通过Jarvis,你的高级私人助理来处理一些事务性的工作,比如了解你最近的日程安排或计划。
+尝试后你会发现,Jarvis并不会按预期工作。是因为站在数据隐私的角度,Jarvis默认只会和“可信的人”进行交流。要实现上面目标,你需要让Jarvis能了解你的人际关系。
+
+### 让Jarvis管理你的联系人
+
+OpenDAN在 myai/contacts.toml 文件中保存了系统已知的所有人的信息。现在非常简单的分成了两组
+1. Family Member,现在该文件里保存里你自己的信息(在系统首次初始化时登陆的)添加
+2. Contact,通常是你的好友
+
+任何不存在上述列表中的联系人,都会被系统划分到`Guest`。Jarvis默认不允许和`Guest`进行交流。因此如果你想要让Jarvis和你的朋友进行交流,你需要把他添加到`Contact`中。
+你可以手工修改 myai/contacts.toml 文件,也可以通过Jarvis来添加联系人。试试和Jarvis说
+
+```
+Jarvis,请添加我的朋友Alic到我的联系人中,他的telegram username是xxxx,email是xxxx
+```
+
+Jarvis能够理解你的意图,并完成添加联系人的工作。
+添加联系人后,你的朋友就可以和你的私人管家Jarvis进行交流了。
+
+## 更新OpenDAN的镜像
+
+现在OpenDAN还处在早期阶段,因此我们会定期发布OpenDAN的镜像来修正一些BUG。因此你可能需要定期更新你的OpenDAN镜像。更新OpenDAN的镜像非常简单,只需要执行下面的命令就可以了
+```
+docker stop aios
+docker rm aios
+docker pull paios/aios:latest
+docker run -v /your/local/myai/:/root/myai --name aios -it paios/aios:latest
+```
+
+
+## 让Agent进一步访问你的信息
+
+你已经知道Jarvis可以帮你管理一些重要的信息。但这些信息都是“新增信息”。在上世纪80年代PC发明以后,我们的一切都在高速的数字化。每个人都已有了海量的数字信息,包括你通过智能手机拍摄的照片,视频,你工作中产生的邮件文档等等。过去我们通过文件系统来管理这些信息,在AI时代,我们将通过Knowledge Base来管理这些信息,保存在Knowledge Base中的信息能更好的被AI访问,让你的Agent更理解你,更好的为你服务。
+
+Knowledge Base是OpenDAN里非常重要的一个基础概念,也是我们为什么需要Personal AIOS的一个关键原因。Knowledge Base相关的技术目前正在快速发展,因此OpenDAN的Knowledge Base的实现也在快速的进化。目前我们的实现更多的是让大家能体验Knowledge Base与Agent结合带来的新能力,其效果还远远未达我们的预期。站在系统设计的角度,我们尽快开放这个组件的另一个目的,是希望找到在产品上对用户更友好,更平滑的方法来把已经存在的个人信息导入进Knowledge Base。
+
+Knowledge Base功能已经默认开启了,将自己的数据放入Knowledge Base有两种方法
+
+1. 把要放入KnowledgeBase的数据复制到 `~myai/data`` 文件夹中。
+2. 通过输入`/Knowledge add dir` ,系统会要求你输入一个将要导入到Knowledge Base的本地目录。注意OpenDAN默认运行在容器中,因此$dir是相对于容器的路径,如果你想要加入本地磁盘的数据,需要先把本地数据挂载到容器中。
+
+OpenDAN会在后台不断分析已加入Knowledge Base文件夹中的文件,分析结果保存在 ~/myai/knowledge 目录中。将该目录删除后,系统会重新分析已加入Knowledge Base的文件。由于目前OpenDAN的Knowledge Base还处在早期阶段,因此目前只支持分析识别文本文件,图片,短视频等。未来OpenDAN将会支持所有的主流文件格式,尽可能把所有的已有信息都能导入到Knowledge。可以aios_shel中通过下面命令来查询Knowledge Base 分析任务的运行状态。
+
+```
+/Knowledge journal
+```
+
+### Mia:个人信息助手
+
+然后我们可以通过 Agent "Mia"来访问Knwolege Base,
+
+```
+/open Mia
+```
+
+试着与Mia交流一下吧!我想这会带来完全不同的体验!
+Mia找到的信息会用下面方式展示:
+
+```
+{"id": "7otCtsrzbAH3Nq8KQGtWivMXV5p54qrv15xFZPtXWmxh", "type": "image"}
+```
+
+可以用`/knowledge query 7otCtsrzbAH3Nq8KQGtWivMXV5p54qrv15xFZPtXWmxh` 命令来调用本地的文件查看器来查看结果。
+
+我们更推荐把Mia接入到Telegram中,这样Mia会把查询结果直接用图片的方式展现,用起来更加方便~
+
+### Embeding Pipeline
+
+Knowledge Base读取并分析文件,产生Agent可以访问的信息的过程被称作Embeding.这个过程需要一定的计算资源。经过我们的测试,目前OpenDAN基于“Sentence Transformers”构建的Embeding Pipeline是可以在绝大多数类型的机器上运行起来的。不同能力的机器的区别主要在于Embeding的速度和质量。了解OpenDAN进度的朋友可能知道,我们在实现的过程中也曾支持过云端Embeding,用来彻底的减少OpenDAN的最小系统性能要求。不过考虑到Embeding过程中涉及到的大量的个人隐私数据,我们还是决定关闭云端Embeding这个特性。有需要的同学可以通过修改源代码来打开云端Embeding,让OpenDAN可以在非常低性能的设备上工作起来。
+
+遗憾的是,现在并没有统一的Embeding标准,因此不同的Embeding Pipeline产生的结果不能互相兼容。这意味着一旦切换了Embeding Pipline,知识库的所有信息都要重新扫描。
+
+## bash@aios
+
+如果你有一定的工程背景,通过让Agent 执行bash命令,也可以非常简单快速的让OpenDAN具有你的私有数据的访问能力。
+使用命令
+
+```
+/open ai_bash
+```
+
+打开ai_bash,然后你就可以在aios_shell的命令行中执行传统的bash命令了。同时你还拥有了智能命令的能力,比如查找文件,你可以用
+
+```
+帮我查找 ~/Documents 目录下所有包含OpenDAN的文件
+```
+
+来代替输入find命令~ 非常酷吧!
+
+OpenDAN目前默认运行在容器中,因此ai_bash也只能访问docker容器中的文件。这相对安全,但我们还是提醒你不要轻易的把ai_bash这个agent暴露出去,可能会带来潜在的安全风险。
+
+## 我们为什么需要Personal AIOS?
+
+很多人会第一个想到隐私,这是一个重要的原因,但我们不认为这是人们真正离开ChatGPT,选择Personal AIOS的真正原因。毕竟今天很多人并不对隐私敏感。而且今天的平台厂商一般都是默默的使用你的隐私赚钱,而很少会真正泄露你的隐私。
+
+我们认为Personal AIOS的真正价值在于:
+
+1. 成本是一个重要的决定因素。LLM是非常强大的,边界非常清楚的核心组件,是新时代的CPU。从产品和商业的角度,ChatGPT类产品只允许用有限的方法来使用它。让我想起了小型机刚刚出现时大家分时使用系统的时代:有用,但有限。要真正发挥LLM的价值,我们需要让每个人都能拥有自己的LLM,并能自由的使用LLM作为任何应用的底层组件,这就必须要有一个新的,以LLM为核心构建的操作系统来重新抽象应用(Agent/Workflow)和应用所使用的资源(算力,数据,环境)
+
+2. 当拥有LLM后,当能把LLM放到每一个计算前面时,你会看到真正的宝藏!现在的ChatGPT通过Plugin对LLM能力的扩展,其能力和边界都是非常有限的,这里既有商业成本的原因,也有传统云服务的法律边界问题:平台要承担的责任太多了。而通过在Personal AIOS中使用LLM,你可以自由的把自然语言,LLM,已有服务,私人数据,智能设备连接在一起,并不用担心隐私泄露和责任问题(你自己承担了授权给LLM后产生后果的责任)!
+
+OpenDAN is an open-source project, let's define the future of Humans and AI together!
\ No newline at end of file
diff --git a/doc/QuickStart.md b/doc/QuickStart.md
new file mode 100644
index 0000000..1203eb0
--- /dev/null
+++ b/doc/QuickStart.md
@@ -0,0 +1,348 @@
+# OpenDAN Quick Start
+
+OpenDAN (Open and Do Anything Now with AI) is revolutionizing the AI landscape with its Personal AI Operating System. Designed for seamless integration of diverse AI modules, it ensures unmatched interoperability. OpenDAN empowers users to craft powerful AI agents—from butlers and assistants to personal tutors and digital companions—all while retaining control. These agents can team up to tackle complex challenges, integrate with existing services, and command smart(IoT) devices.
+
+With OpenDAN, we're putting AI in your hands, making life simpler and smarter.
+
+This project is still in its very early stages, and there may be significant changes in the future.
+
+## Installation
+
+There are two ways to install the Internal Test Version of OpenDAN:
+
+1. Installation through docker, this is also the installation method we recommend now
+2. Installing through the source code, this method may encounter some traditional Pyhont dependence problems and requires you to have a certain ability to solve.But if you want to do secondary development of OpenDAN, this method is necessary.
+
+### Preparation before installation
+
+1. Docker environment
+This article does not introduce how to install the docker, execute it under your console
+
+```
+docker -version
+```
+
+If you can see the docker version number (> 20.0), it means that you have installed Docker.
+If you don't know how to install docker, you can refer to [here](https://docs.docker.com/engine/install/)
+
+2. OpenAI API Token
+If there is no api token, you can apply for [here](https://beta.openai.com/)
+(Applying for the API Token may have some thresholds for new players. You can find friends around you, and you can give you a temporary, or join our internal test experience group. We will also release some free experience API token from time to time.These token is limited to the maximum consumption and effective time)
+
+### Install OpenDAN
+
+After executing the following command, you can install the Docker Image of OpenDAN
+```
+docker pull paios/aios:latest
+```
+
+## Run
+
+When you first run OpenDAN, you need to initialize it. During the initialization process, some basic models will be downloaded for the local Knowledge Base library, and you'll need to input some personal information. Therefore, remember to include the -it parameter when starting Docker.
+
+OpenDAN is your Personal AIOS, so it will generate some important personal data (such as chat history with agent, schedule data, etc.) during its operation. These data will be stored on your local disk. ThereforeWe recommend that you mount the local disk into the container of Docker so that the data can be guaranteed.
+
+```
+docker run -v /your/local/myai/:/root/myai --name aios -it paios/aios:latest
+```
+
+In the above command, we also set up a Docker instance for Docker Run named AIOS, which is convenient for subsequent operations.You can also use your favorite name instead
+
+After executing the above command, if everything is normal, you will see the following interface
+
+
+After the first operation of the docker instance is created, it only needs to be executed again:
+
+```
+docker start -ai aios
+```
+
+If you plan to run in a service mode (NO UI), you don't need to bring the -AI parameter:
+
+```
+docker start aios
+```
+
+## The first run configuration
+
+If you have not used the character interface (CLI) in the past, you may not be used to it.But don't be nervous, even in the Internal Test version, you will only need to use CLI in a few cases.
+
+OpenDAN must be a future operating system that everyone can easily use, so we hope that the use and configuration of OpenDAN are very friendly and simple.But in the Internal Test, we have not enough resources to achieve this goal.After thinking, we decided to support the use of OpenDAN by CLI.
+
+OpenDAN uses LLM as the kernel of AIOS, and integrates many very COOL AI functions through different Agent/Workflow. You can experience some of the latest success of the AI industry in OpenDAN.To activate all the functions requires more configuration, but we only need to do two configurations for the first operation.
+
+1. LLM Kernel
+
+OpenDAN is a future AI Operating Yystem built around LLM, so the system must have `at least one LLM core`.
+
+OpenDan configures LLM in the agent unit. Agent, which does not specify the LLM model name, will use GPT4 by default (GPT4 is also the smartest LLM).You can modify the local LLM that configures to LLaMa or other installed.Today, Local LLM requires the support of quite strong local computing resource, which requires a lot of one-time investment.
+
+But we believe that the LLM field will also follow `Moore's law`. The future LLM model will become more and more powerful, smaller and cheaper.Therefore, we believe that in the future, everyone will have their own Local LLM.
+
+2. Your personal information
+
+This allows your personal AI assistant Jarvis to better serve you. Note that you must enter your own correct Telegram username, otherwise due to authority control, you will not be able to access Agent/Workflow installed on OpenDan through Telegram.
+
+Okay, after a simple understanding of the above background, press the interface to prompt the input of the necessary information.
+
+P.S:
+The above configuration will be saved in the `/your/local/myai/etc/system.cfg.toml`, if you want to modify the configuration, you can directly modify this file.If you want to adjust the configuration, you can edit this file directly.
+
+
+## (Experimental) Install the local LLM kernel
+
+For a quick first experience with OpenDAN, we strongly recommend using GPT4. Although it's slow and expensive, it's currently the most powerful and stable LLM core. OpenDAN's architectural design allows different agents to choose different LLM cores (there must be at least one available LLM core in the system). If for any reason you can't use GPT4, you can install Local LLM using the method below to get the system running. OpenDAN is a system designed for the future, and we believe that the capabilities of GPT4 today will definitely be the minimum for all LLMs in the future. But the current reality is that other LLMs, whether in terms of effects or functions, still have a noticeable gap with GPT4, so to fully experience OpenDAN, we still recommend using GPT4 for a certain period of time.
+
+At present, we have only completed the adaptation of Local LLM based on Llama.cpp. Adapting new LLM cores for OpenDAN is not a complicated task, and engineers who need it can expand it on their own (remember to PR us~). If you have certain hands-on abilities, you can install the Compute Node based on Llama.cpp using the method below:
+
+### Install LLama.cpp ComputeNode
+
+OpenDAN supports distributed computing resource scheduling, so you can install the LLaMa computing node on a machine different from OpenDAN. Depending on the size of the model, substantial computing power is needed, so please proceed according to your machine's configuration. We use llama.cpp to build the LLaMa LLM ComputeNode. Llama.cpp is a rapidly evolving project that is committed to reducing the device threshold required to run LLM and improving running speed. Please read the llama.cpp project to understand the minimum system requirements for each supported model.
+
+Installing LLaMa.cpp involves two steps:
+
+Step1: Download the LLaMa.cpp model. There are three choices: 7B-Chat, 13B-Chat, 70B-Chat. Our practical experience shows that at least the 13B model is required to work. LLaMa2's official models currently do not support inner function calls, and many of OpenDAN's Agents heavily depend on inner function calls. So we recommend you download the Fine-Tuned 13B model:
+
+```
+https://huggingface.co/Trelis/Llama-2-13b-chat-hf-function-calling
+```
+
+Step2: Run the llama-cpp-python image
+
+```
+docker run --rm -it -p 8000:8000 -v /path/to/models:/models -e MODEL=/models/llama-2-13b-chat.gguf ghcr.io/abetlen/llama-cpp-python:latest
+```
+
+After completing the above steps, if the output is as follows, it means that LLaMa has correctly loaded the model and is running normally.
+
+```
+....................................................................................................
+llama_new_context_with_model: kv self size = 640.00 MB
+llama_new_context_with_model: compute buffer total size = 305.47 MB
+AVX = 1 | AVX2 = 1 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | FMA = 1 | NEON = 0 | ARM_FMA = 0 | F16C = 1 | FP16_VA = 0 | WASM_SIMD = 0 | BLAS = 0 | SSE3 = 1 | SSSE3 = 1 | VSX = 0 |
+INFO: Started server process [171]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+```
+
+### Add LLaMa.cpp ComputeNode to OpenDAN
+
+ComputeNode is a basic component of OpenDAN and may not run on the same machine as OpenDAN. Therefore, from a dependency perspective, OpenDAN does not have the ability to "actively detect" ComputeNode. Users (or system administrators) need to manually add it in the command line of OpenDAN with the following command:
+
+```
+/node add llama Llama-2-13b-chat http://localhost:8000
+```
+
+The above command adds a 13b model running locally. If you are using a different model or running it on a different machine, please modify the model name and port number in the command.
+
+### Configure Agent to Use LLaMa
+
+OpenDAN's Agents can choose the LLM-Model that best suits their responsibilities. We have built-in an Agent called Lachlan, a private Spanish teacher Agent, which has been configured to use the LLaMa-2-13b-chat model. You can chat with it using the following command:
+
+```
+/open Lachlan
+```
+
+Therefore, after adding a new LLM-Kernel, you need to manually modify the Agent's configuration to allow it to use the new LLM. For example, our private English teacher Tracy, whose configuration file is `/opt/aios/agents/Tracy/Agent.toml`, modify the configuration as follows:
+
+```
+llm_model_name="Llama-2-13b-chat"
+max_token_size = 4000
+```
+
+Then, after restarting OpenDAN, you can have Tracy use LLaMa (you can also use this method to see which LLM models other built-in Agents are using).
+
+## Hello, Jarvis!
+
+After the configuration is completed, you will enter a AIOS Shell, which is similar to Linux Bash and similar. The meaning of this interface is:
+The current user "username" is communicating with the name "Agent/Workflow of Jarvis". The current topic is default.
+
+Say Hello to your private AI assistant Jarvis !
+
+If everything is OK, you will get a reply from Jarvis after a moment .At this time, the OpenDAN system is running
+
+## Give a Telegram account to Jarvis
+
+You've successfully installed and configured OpenDAN, and verified that it's working properly. Now, let's quickly return to the familiar graphical interface, back to the mobile internet world!
+We'll be registering a Telegram account for Jarvis. Through Telegram, you can communicate with Jarvis in a way that feels familiar.
+In the OpenDAN's aios_shell, type:
+
+```
+/connect Jarvis
+```
+
+Follow the prompts to input the Telegram Bot Token, and you'll have Jarvis set up. To learn how to obtain a Telegram Bot Token, you can refer to the following article:
+https://core.telegram.org/bots#how-do-i-create-a-bot.
+
+Additionally, we offer the option to register an email account for the Agent. Use the following command:
+
+```
+/connect Jarvis email
+```
+
+Then follow the prompts to link an email account to Jarvis. However, as the current system doesn't have a pre-filter customized for email contents, there's a potential for significant LLM access costs. Hence, email support is experimental. We recommend creating a brand-new email account for the Agent."
+
+## Running OpenDAN as a Daemon
+
+The method described above runs OpenDAN interactively, which is suitable for development and debugging purposes. For regular use, we recommend running OpenDAN as a service. This ensures OpenDAN operates silently in the background without disturbing your usual tasks.
+First, input:
+
+```
+/exit
+```
+
+to shut down and exit OpenDAN. Then, we'll start OpenDAN as a service using:
+
+```
+docker start aios
+```
+
+Jarvis, which is an Agent running on OpenDAN, will also be terminated once OpenDAN exits. So, if you wish to communicate with Jarvis via Telegram anytime, anywhere, remember to keep OpenDAN running (don't turn off your computer and maintain an active internet connection).
+
+In fact, OpenDAN is a quintessential Personal OS, operating atop a Personal Server. For a detailed definition of a Personal Server, you can refer to the [CYFS Owner Online Device(OOD)](https://github.com/buckyos/CYFS). Running on a PC or laptop isn't the formal choice, but then again, aren't we in an Internal Test phase?
+
+Much of our ongoing research and development work aims to provide an easy setup for a Personal Server equipped with AIOS. Compared to a PC, we're coining this new device as PI (Personal Intelligence), with OpenDAN being the premier OS tailored for the PI.
+
+## Introducing Jarvis: Your Personal Butler!
+
+Now you can talk with Jarvis anytime, anywhere via Telegram. However, merely seeing him as a more accessible ChatGPT doesn't do justice to his capabilities. Let's dive in and see what new tricks Jarvis, running on OpenDAN, brings to the table!
+
+## Let Jarvis Plan Your Schedule
+
+Many folks rely on traditional calendar software like Outlook to manage their schedules. I personally spend at least two hours each week using such applications. Manual adjustments to plans, especially unforeseen ones, can be tedious. As your personal butler, Jarvis should effortlessly manage your schedule through natural language!
+Try telling Jarvis:
+
+```
+I'm going hiking with Alice on Saturday morning and seeing a movie in the afternoon!
+```
+
+If everything's in order, you'll see Jarvis' response, and he'll remember your plans.
+You can inquire about your plans with Jarvis using natural language, like:
+
+```
+What are my plans for this weekend?
+```
+
+Jarvis will respond with a list of your scheduled activities.
+Since Jarvis uses LLM as its thinking core, he can communicate with you seamlessly, adjusting your schedule when needed. For instance, you can tell him:
+
+```
+A friend is coming over from LA on Saturday, and it's been ages since we last met. Shift all of Saturday's appointments to Sunday, please!
+```
+
+Jarvis will seamlessly reschedule your Saturday plans for Sunday.
+Throughout these interactions, there's no need to consciously use "schedule management language." As your butler, Jarvis understands your personal data and engages at the right moments, helping manage your schedule.
+This is a basic yet practical illustration. Through this example, it's clear that people might no longer need to familiarize themselves with foundational software of today.
+
+Welcome to the new era!
+
+All the schedules set by the Agent are stored in the ~/myai/calender.db file, formatted as sqlite DB. In future updates, we plan to authorize Jarvis to access your production environment calendars (like the commonly-used Google Calendar). Still, our hope for the future is that people store vital personal data on a physically-owned Personal Server.
+
+## Introducing Jarvis to Your Friends
+
+Sharing Jarvis's Telegram account with your friends can lead to some interesting interactions. For instance, if they can't get in touch with you directly, they can communicate with Jarvis, your advanced personal assistant, to handle transactional tasks like inquiring about your recent schedules or plans.
+
+After trying, you'll realize that Jarvis doesn't operate as anticipated. From a data privacy standpoint, Jarvis, by default, interacts only with "trusted individuals". To achieve the above objectives, you need to let Jarvis understand your interpersonal relationships.
+
+### Let Jarvis Manage Your Contacts
+
+OpenDAN stores the information of all known individuals in the myai/contacts.toml file. Currently, it's simply divided into two groups:
+
+1. Family Member, At present, this group stores your information (logged during the system's initial setup).
+2. Contact,These are typically your friends.
+
+Anyone not listed in the aforementioned categories is classified as a Guest by the system. By default, Jarvis doesn't engage with Guests. Hence, if you want Jarvis to interact with your friends, you must add them to the Contact list.
+
+You can manually edit the myai/contacts.toml file, or you can let Jarvis handle the contact addition. Try telling Jarvis:
+
+```
+Please add my friend Alice to my contacts. Her Telegram username is xxxx, and her email is xxxx.
+```
+
+Jarvis will comprehend your intent and carry out the task of adding the contact.
+Once the contact is added, your friend can interact with your personal butler, Jarvis.
+
+## Update OpenDAN
+
+As OpenDAN is still in its early stages, we regularly release images of OpenDAN to fix some bugs. Therefore, you may need to update your OpenDAN image regularly. Updating the OpenDAN image is very simple, just execute the following commands:
+
+```
+docker stop aios
+docker rm aios
+docker pull paios/aios:latest
+docker run -v /your/local/myai/:/root/myai --name aios -it paios/aios:latest
+```
+
+## Allowing Agents to Further Access Your Information
+
+You already know that Jarvis can help you manage some important information. But these are all "new information". Since the invention of the PC in the 1980s, everything has been rapidly digitizing. Everyone already has a massive amount of digital information, including photos and videos you take with your smartphone, emails and documents generated at work, and so on. In the past, we managed this information through a file system, in the AI era, we will manage these pieces of information through a Knowledge Base. Information stored in the Knowledge Base can be better accessed by AI, allowing your Agent to understand you better and provide better service.
+
+The Knowledge Base is a very important basic concept in OpenDAN, and it is a key reason why we need Personal AIOS. Technologies related to the Knowledge Base are currently developing rapidly, so the implementation of OpenDAN's Knowledge Base is also evolving rapidly. At present, our implementation is more about letting everyone experience the new capabilities brought by the combination of the Knowledge Base and Agent, and its effect is far from our expectations. From the perspective of system design, another purpose of our opening this component as soon as possible is to find a more user-friendly and smoother method on the product to import existing personal information into the Knowledge Base.
+
+The Knowledge Base function is already turned on by default. There are two ways to put your own data into the Knowledge Base:
+
+1. Copy the data to be put into the Knowledge Base into the `~myai/data` folder.
+2. By entering `/Knowledge add dir`, the system will ask you to input a local directory to be imported into the Knowledge Base. Note that OpenDAN runs in a container by default, so $dir is a path relative to the container. If you want to add data from the local disk, you need to mount the local data to the container first.
+
+OpenDAN will continuously analyze the files in the Knowledge Base folder in the background, and the analysis results are saved in the ~/myai/knowledge directory. After deleting this directory, the system will re-analyze the files that have been added to the Knowledge Base. Since OpenDAN's Knowledge Base is still in its early stages, it currently only supports analyzing and recognizing text files, pictures, short videos, etc. In the future, OpenDAN will support all mainstream file formats and try to import all existing information into Knowledge. You can query the running status of the Knowledge Base analysis task in aios_shel with the following command:
+
+```
+/Knowledge journal
+```
+
+### Meet Your Personal Information Assistant, Mia
+
+Then we can access the Knowledge Base through the Agent "Mia",
+
+```
+/open Mia
+```
+
+Try to communicate with Mia! I think this will bring a completely different experience! The information Mia finds will be displayed in the following way:
+
+```
+{"id": "7otCtsrzbAH3Nq8KQGtWivMXV5p54qrv15xFZPtXWmxh", "type": "image"}
+```
+
+You can use the `/knowledge query 7otCtsrzbAH3Nq8KQGtWivMXV5p54qrv15xFZPtXWmxh` command to call the local file viewer to view the results.
+
+We recommend integrating Mia into Telegram, so Mia will directly display the query results in the form of images, which is more convenient to use~
+
+### Embeding Pipeline
+
+The process by which the Knowledge Base reads and analyzes files to generate information that the Agent can access is called Embedding. This process requires certain computational resources. According to our tests, the Embedding Pipeline built by OpenDAN based on "Sentence Transformers" can run on the vast majority of types of machines. The difference between machines of different capabilities mainly lies in the speed and quality of Embedding. Friends who understand the progress of OpenDAN may know that we have also supported cloud Embedding during the implementation process to completely reduce the minimum system performance requirements of OpenDAN. However, considering the large amount of personal privacy data involved in the Embedding process, we decided to turn off the cloud Embedding feature. Those in need can modify the source code to open cloud Embedding, allowing OpenDAN to work on very low-performance devices.
+
+Unfortunately, there is no unified Embedding standard now, so the results generated by different Embedding Pipelines are not compatible with each other. This means that once the Embedding Pipeline is switched, all the information in the Knowledge Base needs to be rescanned.
+
+## bash@ai
+
+If you have a certain engineering background, by letting the Agent execute bash commands, you can also quickly and easily give OpenDAN access to your private data.
+
+Use the command
+
+```
+/open ai_bash
+```
+
+to activate ai_bash. From there, you can execute traditional bash commands within the aios_shell command line. Plus, you'll have the ability to use smart commands. For example, to search for files, instead of using the 'find' command, you can simply say:
+
+```
+Help me find all files in ~/Documents that contain OpenDAN.
+```
+
+Pretty cool, right?
+
+By default, OpenDAN operates inside a container, meaning ai_bash can only access files within that docker container. While this provides a relative level of security, we still advise caution. Do not expose the ai_bash agent recklessly, as it could pose potential security risks."
+
+## Why Do We Need Personal AIOS?
+
+Many people will first think of privacy, which is an important reason, but we don't think this is the real reason why people leave ChatGPT and choose Personal AIOS. After all, many people are not sensitive to privacy today. Moreover, today's platform manufacturers generally use your privacy to make money silently, and rarely really leak your privacy.
+
+We believe the real value of Personal AIOS lies in:
+
+1. Cost is an important determinant. LLM is a very powerful, clearly bounded core component, the CPU of the new era. From a product and business perspective, ChatGPT-like products only allow it to be used in limited ways. It reminds me of the era when minicomputers first appeared and everyone used the system in a time-sharing manner: useful, but limited. To truly realize the value of LLM, we need to allow everyone to have their own LLM and freely use LLM as the underlying component of any application. This requires a new operating system built around LLM to re-abstract applications (Agent/Workflow) and the resources used by applications (computing power, data, environment).
+
+2. When you have LLM, when you can put LLM in front of every compute, you will see the real treasure! The current ChatGPT's extension of LLM capabilities through plugins is very limited in both capabilities and boundaries. This is due to both commercial cost reasons and the legal boundary issues of traditional cloud services: the platform bears too much responsibility. But by using LLM in Personal AIOS, you can freely connect natural language, LLM, existing services, private data, and smart devices without worrying about privacy leaks and liability issues (you bear the consequences of authorizing LLM yourself)!
+
+OpenDAN is an open-source project, let's define the future of Humans and AI together!
\ No newline at end of file
diff --git a/doc/aios_kernel.md b/doc/aios_kernel.md
new file mode 100644
index 0000000..57a957b
--- /dev/null
+++ b/doc/aios_kernel.md
@@ -0,0 +1,9 @@
+
+# aios kernel部分的核心介绍
+
+## 核心理论
+
+
+## 以 workflow为核心组织ai agent
+定义了未来工程师使用LLM构造应用的方法
+
diff --git a/doc/compute_kernel.png b/doc/compute_kernel.png
new file mode 100644
index 0000000..f0ddbf1
Binary files /dev/null and b/doc/compute_kernel.png differ
diff --git a/doc/compute_task.drawio b/doc/compute_task.drawio
new file mode 100644
index 0000000..ede6cf1
--- /dev/null
+++ b/doc/compute_task.drawio
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/install_package.png b/doc/install_package.png
new file mode 100644
index 0000000..599313a
Binary files /dev/null and b/doc/install_package.png differ
diff --git a/doc/knowleadge.drawio b/doc/knowleadge.drawio
new file mode 100644
index 0000000..bdb5534
--- /dev/null
+++ b/doc/knowleadge.drawio
@@ -0,0 +1,1478 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/knowleage.md b/doc/knowleage.md
new file mode 100644
index 0000000..7b48cac
--- /dev/null
+++ b/doc/knowleage.md
@@ -0,0 +1,20 @@
+# Objected knowleadge base, a specialized implemention for emails
+## Vectorized Knowledge
+Large language models are trained on general corpora and without fine-tuning on user-specific data, they struggle to utilize user-related context effectively.
+
+Users accumulate a vast amount of content that reflects their personality during their regular internet usage. This includes personal photos, tweets, Facebook posts, emails, etc. While it's possible to include all this content in the prompt during each interaction with the large language model, this approach is costly and can easily reach the token limit.
+
+A common solution is to generate feature vectors from this content using word embedding techniques and store them in a vector database. During an interaction, the vector that is most relevant to the prompt is retrieved from the database, merged with the prompt, and then passed to the large language model.
+
+We refer to this vectorized content as "knowledge".
+
+## Objected knownleadge base
+In a personal AI system, to build a user's own knowledge base, we first need to implement various spider programs to crawl and retrieve all user-related data. Modern web content is typically rich text, including text, images, videos, hyperlinks, etc. Organizing this rich text in a tree-like structure similar to HTML is necessary, hence the need to introduce an object structure to represent this content.
+
+Different parts of this content cannot be vectorized using the same embedding model. For instance, text and images, as well as the content of an image and its EXIF information, need separate embeddings. This means that in the vector database, the same content may have multiple vector values, and a row can represent a whole content item or just a part of it.
+
+We need a comprehensive object structure to represent the hierarchy and relationships of content, as well as to implement the indexing and storage of objects. In the Minimum Viable Product (MVP) version, we'll implement a specialized solution for email content. In future versions, we can generalize this to handle other types of content, such as Facebook posts, tweets, etc.
+
+## Agent with knowleadge base
+At the same time, we also need to explore the paradigm of using the knowledge base in Agents and workflows, so that the agent can better complete tasks in interaction with users through the context provided by the knowledge base.
+
diff --git a/doc/load_package.png b/doc/load_package.png
new file mode 100644
index 0000000..f503c13
Binary files /dev/null and b/doc/load_package.png differ
diff --git a/doc/mvp plan 2.md b/doc/mvp plan 2.md
new file mode 100644
index 0000000..2623120
--- /dev/null
+++ b/doc/mvp plan 2.md
@@ -0,0 +1,500 @@
+# Overview
+
+The core goal of version 0.5.1 is to turn the concept of AIOS into code and get it up and running as quickly as possible. After three weeks of development, our plans have undergone some changes based on the actual progress of the system. Under the guidance of this goal, some components do not need to be fully implemented. Furthermore, based on the actual development experience from several demo Intelligent Applications, we intend to strengthen some components. This document will explain these changes and provide an update on the current development progress of MVP(0.5.1,0.5.2)
+
+The previous plan, please see here: [MVP Plan](./mvp%20plan.md)
+
+# Progress Status of MVP
+
+- Each module includes whether the current version goals have been met, the current person in charge, and workload assessment.
+- Modules that are not marked for version 0.5.2 and do not have a designated person in charge are modules for which we are currently recruiting contributors.
+- Modules that have not been completed but already have a designated person in charge are modules that are currently in development.
+
+- [x] AIOS Kernel
+ - [x] Agent,@waterflier, A2
+ - [ ] Optimization of system prompts,A2
+ - [x] Workflow,@waterflier, A2
+ - [x] AI Environments,@waterflier, A2
+ - [x] Celender Environment,@waterflier, S2
+ - [ ] Compatible with common Celender services(0.5.2),A2
+ - [ ] Microsoft Outlook Calendar, S2
+ - [ ] Google Calendar, S2
+ - [ ] Apple Calendar, S2
+ - [ ] Workspace Environment(0.5.2) @waterflier, A2
+ - [ ] AI Functions,@waterflier,A2
+ - [ ] Basic AI Functions(0.5.2)
+ - [x] AI BUS,@waterflier, A2
+ - [x] Chatsession,@waterflier, S2
+ - [ ] Knowlege Base,@lurenpluto ,@photosssa, A8
+ - [ ] Personal Models(>0.5.2),A8
+- [ ] Frame Services(0.5.2)
+ - [ ] Kernel Service
+ - [ ] System-Call Interface,A2
+ - [ ] Name Service,A4
+ - [ ] Node Daemon,A2
+ - [ ] ACL Control,A4
+ - [ ] Contact Manager,A2
+ - [ ] Runtime Context (0.5.2),A4
+ - [ ] Package System,@waterflier, A2+S4
+- [x] AI Compute System,@waterflier, A2
+ - [ ] Scheduler,@streetycat, A2
+ - [x] LLM
+ - [x] GPT4 (Cloud),@waterflier, S1
+ - [ ] LLaMa2,@streetycat, A2
+ - [ ] Claude2, S2
+ - [ ] Falcon2, S2
+ - [ ] MPT-7B, S2
+ - [ ] Vicuna, S2
+ - [ ] Embeding,@photosssa,@lurenpluto , A4
+ - [x] Txt2img,@glen0125,A4
+ - [ ] Img2txt(0.5.2),A3
+ - [ ] Txt2voice,A3
+ - [ ] Voice2txt, @wugren,A3
+ - [ ] Language Translate (Pending)
+- [x] Storage System
+ - [ ] DFS (0.5.2),A4
+ - [ ] Object Storage, @lurenpluto ,A2
+ - [ ] D-RDB (0.5.2),A2
+ - [ ] D-VDB,@lurenpluto , A4
+- [ ] Embeding Piplines,@photosssa, A2
+- [ ] Network Gateway,A6
+ - [x] NDN Client, @waterflier, A1
+- [ ] Build-in Service
+ - [x] Spider,@alexsunxl, A2
+ - [x] E-mail Spider,@alexsunxl, S2
+ - [ ] Telegram Spider,S2
+ - [ ] Twitter Spider (0.5.2)
+ - [ ] Facebook Spider (0.5.2)
+ - [ ] Agent Message Tunnel (0.5.2)
+ - [ ] E-mail Tunnel,A2
+ - [ ] Telegram Tunnel,S2
+ - [ ] Discord Tunnel,S2
+ - [ ] Home IoT Environment (0.5.2), A4
+ - [ ] Compatible Home Assistant (0.5.2), A4
+- [ ] Build-in Agents/Apps
+ - [ ] Agent: Personal Information Assistant,@photosssa,@lurenpluto , A2
+ - [ ] Agent: Bulter Jarvis,@waterflier, A2
+ - [ ] App: Personal Station (0.5.2),A4+S4
+- [ ] UI
+ - [x] CLI UI (aios_shell),@waterflier,S2
+ - [ ] Web UI (0.5.2),A4+S4
+- [ ] 0.5.1 Integration Test (Senior*3)
+ - [x] Workflow -> AI Agent -> AI Agent,@waterflier,S1
+ - [ ] Spider -> Pipline -> Knowledge Base,@photosssa,S2
+ - [ ] AI Agent <- Functions <- Knowledge Base,@lurenpluto,S2
+- [ ] SDK
+ - [x] Workflow SDK,@waterflier, A2
+ - [ ] AI Environments SDK (0.5.2), A2
+ - [ ] Compute Kernel SDK (0.5.2), A2
+- [ ] Document (>0.5.2)
+ - [ ] System design document, including the design document of each subsystem
+ - [ ] Installation/use document for end users
+ - [ ] SDK document for developers
+
+
+The following is the introduction of the adjustment of each component after the current implementation.
+
+# AIOS Kernel
+
+Define some of the important basic concepts of Intelligent Applications running on OpenDAN
+
+## Agent
+
+Agent is the core concept of the system, created through appropriate LLM, prompt words, and memory. Agents support our vision of a new relationship between humans and computation in the future:
+```
+Human <-> Agent <-> Compute
+```
+Agents form the basis of future intelligent applications. From the user's perspective, the strength of AIOS is primarily determined by "how many agents with different capabilities it has."
+The above process has now been implemented. In practice, I found a key issue is that we need to continuously seek the optimal solution. This issue directly relates to how application developers of OpenDAN build intelligent applications, so I think it has a high priority.
+
+### Optimization of system prompts
+
+The goal is to allow Agents to communicate with other Agents (forming a team), call Functions at the right time, and read/write status through the environment at the right time, using prompt words. The existing implementation is usually:
+
+```
+When you decide to communicate with a work group, please use : sendmsg(group_name, content).
+```
+
+Our optimization direction is:
+
+1. To allow Agents to initiate calls accurately
+2. To use as few precious prompt word resources as possible.
+
+If there are already systematic studies in this field, introductions are also welcome!
+
+## Workflow
+
+Workflow has realized the concept of allowing multiple Agents to play different roles and collaboratively solve complex problems within an organization. It is also the main form of intelligent applications on OpenDAN. Compared to a single Agent, building a team composed of Agents can effectively solve three inherent problems of LLM:
+
+1. The prompt word window will grow, but it will remain limited for a long time.
+2. Like humans, Agents trained on different corpora and algorithms will have different personalities and will excel in different roles.
+3. The inference results of LLM are uncontrollable, so accuracy cannot be guaranteed. Just like humans make mistakes, the collaboration of multiple Agents is needed to improve accuracy.
+
+The basic framework of Workflow has been completed (which is also the core of version 0.5.1). Following the subsequent SDK documentation, we now have a basic framework for third-party developers to develop applications on OpenDAN.
+
+## AI Environments
+
+Environments provide an abstraction for AI Agents to access the real world.
+
+Environments include properties, events, methods (Env.Function), and come with natural language descriptions that Agents can understand. This allows AI Agents to understand the current environment and when to access it. For example, an Agent planning a trip needs to understand the real weather conditions at the destination in the future to make the right decisions. This weather condition needs to be provided to the Agent through Environments.
+
+The events in Environments also provide logic for the autonomous work of Agents. For example, an Agent can track changes in the user's schedule and date, automatically helping the user plan and track the specific itinerary for the day.
+
+### Celender Environment
+
+The system's default environment, which can access the current time, the user's schedule, and the weather information at a specific location. It also contains some important and basic user information, including home address and office address.
+
+#### Compatible with common Celender services. (0.5.2)
+
+- Microsoft Outlook Calendar
+- Google Calendar
+- Apple Calendar
+
+### Workspace Environment (0.5.2)
+
+A file system-based workspace environment that allows the Agent to read/write files at appropriate times.
+
+## AI Functions
+
+Function is a core concept of AIOS, providing the Agent with descriptions of suitable callable Functions, allowing the Agent to invoke Functions at the right time. Through Functions, the Agent gains "execution power," rather than just being an advisor that only provides suggestions. The Function framework allows third-party developers to develop and publish Functions, supporting Agents and Workflows to have a list of available Functions, through which they can build appropriate prompt words, enabling the Agent to invoke Functions at the right time.
+
+**Under development.**
+
+### Basic AI Functions (0.5.2)
+
+There are already a plethora of basic services in the world, such as querying the weather at a specific location at a specific time, checking hotel prices, or booking plane tickets. The system should separate the definition and implementation of Basic (generic) Functions, allowing Agent developers to implement common scenarios with generic logic. The definition of generic Functions is undoubtedly similar to standard setting work.
+
+ I know that many other projects have done a lot of work in this field, and ChatGPT also has dedicated function support. What we need to do is to find the open standards that are closest to our goals and then integrate them.
+
+## AI BUS
+
+The AI BUS connects various conceptual entities of OpenDAN. For example, if Agent A wants to send a message to another Agent B and wait for the processing result of the message, it can simply use the AI BUS:
+
+```python
+resp = await AIBus.send_msg(agentA,agentB,msg)
+```
+
+The abstraction of AI BUS allows different Agents to choose suitable physical hosts to run according to the system's needs. This is also why we define AIOS as a "Network OS". All entities registered on the AI BUS can be accessed via the AI BUS interface. As needed, we will also persist the messages in the AI BUS, so that when a distributed system experiences regular failures, it can continue to work after being pulled up again.
+
+The concept of AI BUS has many similarities with traditional MessageQueues.
+
+## Chatsession
+
+Intuitively, ChatSession saves the "chat history". The chat history is currently the natural source of Agent Memory capability.
+Determining a ChatSession has three key attributes: Owner, Topic, and Remote. An operation where A sends a message to B and gets B's reply will generate two messages, and save them in two different ChatSessions.
+
+Currently, ChatSession is saved based on sqlite. After the Zone-level D-RDB is set up in the future, it will be migrated to RD-DB.
+
+## Knowlege Base
+
+Provide a unified interface, support switching vector database kernel
+Integrate open source vector database (pay attention to Lience selection)
+When designing the interface, prepare for future access control
+
+**Under development.**
+
+## Personal Models (>0.5.2)
+
+The goal of this subsystem is to support users in training models based on their own data, including subsequent usage, management, deployment, and other operations of the model. In the early stages, invoking this module and adding new models should be operations performed by advanced users.
+
+It is still uncertain whether this module will be actively used in intelligent applications.
+
+# Frame Services
+
+The implementation offers a range of fundamental services for traditional Network OS. It connects users' devices to the same Zone via the network and provides a unified abstraction for application access. This component serves as a basic framework and computing resource for the operation of intelligent applications on the upper layer. On the lower layer, it connects various types of hardware through different protocols, integrates resources, and offers a unified abstraction for intelligent applications to access.
+
+## Kernel Service (0.5.2)
+
+The Kernel Service implements the System Calls for OpenDAN and provides a "kernel mode" abstraction. In version 0.5.1, since this component is not yet implemented, all code—whether system services or application code—runs in kernel mode.
+
+In the future, we plan to maintain the system running in this mode for an extended period, as it facilitates debugging.
+
+The Kernel Service is mainly composed of the following component:
+
+### System-Call Interface
+
+Centralizes the provision and management of system call interfaces.
+
+### Name Service
+
+It is the most crucial foundational state service in a cluster (Zone) comprised of all the user's devices. As the core service of the Zone, it provides the most basic guarantee for the availability and reliability of all services within the Zone. When a user needs to restore the Zone from a backup, the Name Service is the first service to be restored.
+
+Its functionality is similar to that of `etcd` but includes a on-chain component. From a deployment standpoint, it needs to be operationally optimized for small clusters made up of consumer-level user devices.
+
+### Node Daemon
+
+It is a foundational service that runs on all devices that join the Zone, responding to essential kernel scheduling commands. It adjusts the services and data running on that particular device.
+
+### ACL Control (>0.5.2)
+
+Another essential foundational service of the kernel, it is responsible for the overall management of permissions related to users, applications, and data. The Runtime Context reads the relevant information and implements proper isolation.
+
+### Contact Manager
+
+From the perspectives of permission control and some early application scenarios, understanding the user's basic interpersonal relationships is an important component of OpenDAN's intelligent permission system. Therefore, we provide a contact management component at the system kernel layer. This component can be considered an upgraded version of the traditional operating system's "User Group" module.
+
+## Runtime Context (0.5.2)
+
+It serves as the runtime container for user-mode code, offering isolation guarantees for user-mode code.
+
+ Depending on the type of service, we offer three different Runtime Contexts. The most commonly used is Docker, followed by virtual machines, and finally, entire physical machines.
+
+## Package System
+
+The Package Manager is a fundamental component of the system for managing Packages. The sub system provides fundamental support for packaging, publishing, downloading, verifying, installing, and loading folders containing required packages under different scenarios. Based on relevant modules, it's easy to build a package management system similar to apt/pip/npm.
+
+The system design has deeply referenced Git and NDN networks. The distinction between client and server is not that important. Through cryptography, it achieves decentralized trustworthy verification. Any client can become an effective repo server through simple configuration.
+
+Based on the Package System, we can implement the publishing, downloading, and installation of extendable foundational entities such as Agents, Functions, and Environments. This enables the creation of an app store on OpenDAN.
+
+**Under development.**
+
+# AI Compute System
+
+The purpose of designing Compute System is to enable our users to use their computational resources more efficiently. These computational resources can come from devices they own (such as their workstations and gaming laptops), as well as from cloud computing and decentralized computing networks.
+
+[](compute_task.drawio)
+
+The interface of this component is designed from the perspective of the model user rather than the model trainer. The basic form of its interface is:
+
+```python
+compute_kernel.do_compute(function_name, model_name,args)
+```
+
+## Scheduler
+
+The goal of the Scheduler component is to select an appropriate ComputeNode to run tasks based on the tasks in the task queue and the known status of all ComputeNodes (which may be delayed). In the current version (0.5.1), the implementation of the Scheduler is only to get the system up and running. In the next version (0.5.2), the overall framework for computing resource scheduling needs to be established.
+
+## LLM
+
+LLM support is the system's most core functionality. OpenDAN requires that there be at least one available LLM computing node in the system. The supported interfaces are as follows:
+```
+def llm_completion(self,prompt:AgentPrompt,mode_name:Optional[str] = None,max_token:int = 0):
+```
+In the current era, many teams are working hard to develop new LLMs . We will also actively integrate these LLMs into OpenDAN.
+
+- [x] GPT4 (Cloud)
+- [ ] LLaMa2 **Under development.**
+- [ ] Claude2
+- [ ] Falcon2
+- [ ] MPT-7B
+- [ ] Vicuna
+
+
+## Embeding
+
+Provides computational support for the vectorization of different types of user data. The specific algorithms supported depend on the requirements of the entire pipeline.
+
+***Under development.***
+
+## Txt2img
+
+Generate images based on text descriptions. According to the implementation mode, we can interface with a cloud-based implementation and a local implementation.
+
+The local implementation will definitely use Stable Diffusion.
+
+***Under development.***
+
+## Img2txt (>0.5.2)
+
+Generate appropriate text descriptions for the specified images.
+
+## Txt2voice
+
+Generate voice based on specified text, using a selected model (the focus is on personal models), and guided by certain emotional cue words.
+
+***To be developed***
+
+## Voice2txt
+
+Extract text information from a segment of audio (or video) through speech recognition.
+
+***To be developed***
+
+## Language Translate
+
+Translate a segment of text into a specified target language.
+
+Since LLM itself is developed based on the foundation of translation, I am currently considering whether it is necessary to provide a text translation interface within the computing kernel. Following the principle of not adding entities if they are not needed, it can be postponed from development.
+***pending***
+
+# Storage System
+
+The file system (state storage) has always been a critical part of operating systems. Its implementation directly impacts the system's reliability and performance. The challenge of this section is how to transfer key technologies that are already mature in traditional cloud computing to clusters composed of consumer-level electronic devices with low operational maintenance, while still maintaining sufficient reliability and performance. The implementation of the subsystems in this section is of limited stability. Therefore, I believe the focus of OpenDAN in the early stages for this section should be on establishing stable interfaces to get the system running as quickly as possible, with independent improvements to be made in the future.
+
+From the standpoint of trade-offs, our priorities are:
+
+- Abandoning continuous consistency guarantees, the system only provides strong assurance for reliability up to "backup points." This means we allow the loss of some newly added data if the system experiences a failure.
+
+- Allowing downtime, considering the consumer-level power supply, a short period of unavailability of the system itself will not have a significant impact. We can stop the service for backup/migration when necessary.
+
+## DFS
+
+Distributed file system, combining the public storage space on all devices to form a highly reliable, highly available file system.
+
+## Object Storage
+
+Distributed object storage, and based on MapObject, it implements trustworthy RootState management.
+
+(MapObject and RootState is a concept from CYFS)
+
+**Under development.**
+
+## D-RDB
+
+Distributed relational database, providing highly reliable and highly available relational database services (mainly used for OLTP - Online Transaction Processing). We do not encourage application developers to use RDB on a large scale; the main reason for offering this component is for compatibility considerations.
+
+***Pending.***
+
+## D-VDB
+
+Distributed vector database, which currently appears to be the core foundational component of the Knowledge Base library.
+
+***Under development.***
+
+# Embeding Piplines
+
+Read appropriate Raw Files and Meta Data from the specified location in the Storage System. After passing through a series of Embedding Pipelines, save the results to the Vector Database as defined by the Knowledge Base.
+
+***Under development.***
+
+# Network Gateway (0.5.2)
+
+Obtain user data by recognizing network data.
+The Gateway also provides an external access entrance for the entire system, and access control can be unified.
+Provides the bus abstraction in the network operating system (the network cable is the bus), devices within the Zone are recognized by the system as plug-and-play devices, and can be called by applications/Agents
+
+## NDN Client
+
+AI-related models are all quite large, so we offer a download tool based on NDN (Named Data Networking) theory to replace curl. The NDN Client will continue to support new Content-Based protocols in the future, allowing OpenDAN developers to publish large packages more quickly, at lower costs, and more conveniently.
+
+# Build-in Service
+
+The basic functions of the system implemented by "user mode" can be regarded as pre -installed applications of the system.Let the system have basic availability without installing any intelligent applications.
+We should build built-in applications for 1,2 early preset scenarios, rather than all possible scenarios.This allows us to run the system faster and allow us to discover the shortcomings of the system faster, so as to improve the system faster.
+
+## Spider
+
+A series of reptiles are provided to help users import their data into the system.
+
+### E-mail Spider
+
+The most basic spider is used to capture user mail data.The main purpose of this is to determine the general data format(include text,image,contact) and location to save the grabbed data.
+
+### Telegram Spider
+
+Allow users to capture their own Telegram chat records and save them in the Knowlege Base
+
+**To be developed.**
+
+### Twitter Spider (0.5.2)
+
+Allows users to scrape their own Twitter data and save it in the Knowledge Base.
+
+### Facebook Spider (0.5.2)
+
+Allows users to scrape their own Facebook data and save it in the Knowledge Base.
+
+## Agent Message Tunnel (0.5.2)
+
+The original ROBOT module, after considering its actual function, was renamed the Agent Message Tunnel.
+This is the default function supported by the system. It supports users to configure different message channels for different Agent/Workflow, so that users can interact with Agent/Worflow through existing software/services.From the perspective of product, the goal of this module can use the core function of OpenDAN without installing any new software on the one hand. On the other hand, it also creates a stronger mental model for users: My Agent can registered social account, so that Agent has his own identity in the virtual world.
+
+### E-mail Tunnel
+
+Let Agent have its own email account. After registration, users can interact with Agent through mail.
+
+### Telegram Tunnel
+
+Let Agent have his own Telegram account. After registration, users can interact with Agent through Telegram.
+
+### Discord Tunnel
+
+Let Agent have its own discord account. After registration, users can interact with agent through Discord.
+
+## Home IoT Environment (0.5.2)
+
+We've implemented a significant built-in environment: the Home Environment. Through this environment, the AI Agent can access real-time status of the home via installed IoT devices, including reading temperature and humidity information, accessing security camera data, and controlling smart devices in the home. This allows users to better manage a large number of smart IoT devices through AI technology. For instance, a user can simply tell the Agent, "Richard is coming over to watch a movie this afternoon," and the AI Agent will automatically read the security camera data, recognize Richard upon arrival, turn on the home projector, close the curtains, and turn on the wall lights.
+
+Thanks to LLM's powerful natural language understanding, all we need to do is connect a smart microphone to the Home Environment and configure a simple voice-to-text feature. This makes it easy to implement a privately deployed and very intelligent version of Alexa.
+
+In terms of system design, we use the Home Environment as an intermediary layer, freeing OpenDAN from having to spend energy on dealing with compatibility issues with various existing, complex IoT protocols. This keeps the system simple and makes it easier to expand.
+
+### Compatible Home Assistant
+
+Home Assistant is a well-known, open-source IoT system. We could consider implementing the Home Environment based on the Home Assistant's API.
+
+# Build-in Agents/Apps
+
+Once users have installed OpenDAN, it should have some basic functionalities, even without the installation of any third-party smart applications. These basic functions are provided via built-in Agents/Applications. Built-in applications have two important implications for OpenDAN:
+
+1. They provide a developer's perspective to scrutinize whether our design is reasonable and the application development process is smooth.
+2. Through one or two scenarios, OpenDAN can be quickly put into use by real users in a production environment, and these scenarios can serve as a basis for driving system improvements in OpenDAN.
+
+## Agent: Personal Information Assistant
+
+Through interacting with this Agent, users can use natural language to query information that has already been saved in the Knowledge-Base. For example, "Please show me the photos from my meeting with Richard last week." They can also find their information more accurately based on some interactive questions.
+
+***To be developed.***
+
+## Agent: Bulter Jarvis (0.5.2)
+
+The Butler Agent Jarvis can recognize certain special commands. Through these commands, it can communicate with other Agents in the system, check the system's status, and use all the system's functionalities. It can be seen as another entry point to AIOS_Shell.
+
+Another important function of the Jarvis is to create sessions. When a user has many workflows/agents installed on their OpenDAN, they might not know which workflow/agent to talk to in order to solve a problem. I envision the future mode to be: "If you don't know who to turn to, ask the Jarvis." The Jarvis will create or find a suitable session based on a brief conversation with the user, and then guide the user into this session.
+
+Based on these two functions, the Jarvis might be the only "special Agent" that requires custom development among all Agents, and it is a part of the system.
+
+## App: Personal Station (0.5.2)
+
+The Personal Station is a built-in application that provides a graphical user interface for users to interact with the system. It is a web application that can be accessed through a browser. It is also the first application that users will see after installing OpenDAN. It provides a simple interface for users to interact with the system, and it also provides a way for users to install new applications.
+
+The main functions of Personal Station include:
+
+1. Library, with the help of Personal Information Assistant, you can better manage your own photos, videos, music, documents, etc., and share them with friends more effectively. (For example, ask the assistant to share photos from an event, selecting from those you've starred, and distribute them to friends based on the people appearing in the photos.)
+2. HomePage, with functions similar to Facebook/Twitter, where you can post content you want to share. You can also open your Agent to friends and family, allowing them to interact with your Agent, discuss schedule arrangements, and query your KnowledgeBase for open content.
+
+Home Station is a mobile-first WebApp.
+
+# UI
+
+## CLI UI (aios_shell)
+
+The system provides the command line UI interface priority, facing developers and early senior users.
+
+## Web UI (0.5.2)
+
+Web UI interface for end users
+
+# 0.5.1 Integration Test (Senior*3)
+
+Can be divided into 3 parts
+1.Workflow -> AI Agent -> AI Agent
+2.Spider -> Pipline -> Knowledge Base
+3.AI Agent <- Functions <- Knowledge Base
+
+
+# SDK
+
+## Workflow SDK
+
+The SDK allows developers to expand the new workflow/agent to the system.
+At present, the SDK has completed the most original version. In ROOTFS/, the .tmol file is written according to the directory structure, and a new workflow/ agent can be added to the system.
+
+## AI Environments SDK (>0.5.2)
+
+The SDK allows developers to expand the system that can be called by AI, including
+- Expand the new environment
+- Expand the new function
+
+## Compute Kernel SDK (>0.5.2)
+
+This SDK allows developers to expand more core capabilities to the system
+
+# Document (>0.5.2)
+
+When we release 0.5.3, we must complete at least 3 documents:
+
+1. OpenDan's complete system design document, including the design document of each subsystem.
+2. Installation/use document for end users.
+3. SDK document for developers.
diff --git a/doc/mvp plan.md b/doc/mvp plan.md
new file mode 100644
index 0000000..357e592
--- /dev/null
+++ b/doc/mvp plan.md
@@ -0,0 +1,169 @@
+# OpenDAN Basic Planning of MVP
+0.5.1 Implement data capture into Knownlege-Base(KB) via Spider, followed by access by AI Agent (35%)
+0.5.2 Build a Personal-Center based on the KB and associate the AI Agent with accessible telgram accounts (30%)
+0.5.3 Release for waitlist (5%)
+0.5.4 First public release (10%)
+0.5.5 Incorporate modifications after the first public version, workload depends on feedback (15%)
+0.5.6 Official version of MVP (5%)
+
+# Basic Architecture
+(TODO)
+
+# R&D Process Management
+Based on the project management module provided by SourceDAO, explore a new open source R&D process of "open source is mining".
+0. Confirming Version Leader based on committee election.
+1. Module (task division): Divide the system's futures into independent modules as much as possible, the development workload should be at the level of 1 person for 2-3 weeks.
+2. Discussion: Discuss whether the module division is reasonable, and design it's *BUDGET* based on the difficulty of the module and its importance to the current version (most important step)
+3. Recruit module PM. The module PM is responsible for the module's test delivery: completing the set functions, constructing the basic tests, and passing the self-tests. Testing should retain at least 30% of development resources.
+4. For the completed module, PM should write and publicize the Project Proposal. It contains more detailed about module goals + design ideas, participating teams (if any, there should also be a preliminary division of work within the team and calculation of contribution value), and acceptance plan design.
+5. The PM completes development and self-testing. Mark the module is *DONE*.
+6. Version Leader organizes the acceptance of the module (a dedicated acceptor can be appointed).
+7. Version Leader organizes integration testing according to the completion situation, and the module PM fixes BUGs. The test results can be used in the nightly-channel of OpenDAN.
+8. After the test passes, the Version Leader announces the version release, anyone can use this from release-chanel.
+9. The committee accepts the version after the release effect. After acceptance, all participants can extract contribution rewards.
+
+Difficulty is expressed in the mode of requirement engineer level * time (in weeks, less than 1 week is counted as 1 week). 1 week of work time is calculated as 20 hours.
+
+
+**Below is the module division design for version 0.5.1.**
+
+# Package Management (Architect*2)
+The package management system provides basic package definition and local lookup functionality.
+
+The types of packages are
+1. Agent
+2. Function
+3. Service
+4. App
+5. Models (various AI models)
+
+The system itself is upgraded on this basis
+
+# Agent Management (Senior*1)
+Define Agent package, and give it to the package management system for publishing/download/installation/running
+
+## Some pure prompt word engineering Agent (ordinary*1)
+Can refer to the existing implementation of ChatGPT-Next-Web, mainly to let the system run as soon as possible
+
+# Agent (AI) Runtime (Architecture*2)
+Implementing the Agent as a person to run is an important basic component
+The Agent is an instance of a person, and the contents of all its sessions are interconnected. We can clone a new person based on an existing Agent, and this new Agent runs in a new container inherited from the old agent.
+- Wrap LLM Kernel
+- Manage chat-session
+- Manage memory (one of the difficulties, different from Knowlege-Base)
+- Manage identity and permissions
+- Manage logs, for the convenience of development and debugging, we can see all the details of the key operation and self-thinking of the Agent.
+
+# Agent Work Flow (Architecture*2)
+A team of AI Agents completes a complex task
+Understanding this goal can better design the Agent running container
+*Next version*
+
+# LLM Kernel Packaging (Architecture*1)
+Encapsulate LLM, support Agent to use different LLM cores. We can support the GPT4 first.
+
+## Local LLM Integration (Senior*2)
+If the integration is convenient, we can also integrate a local LLM core to see the effect as soon as possible.
+
+# Function Management (Architecture*1)
+Define Function, Function itself is a stateless passive calculation. Easy to be recognized and called by Agent.
+Function management needs to design the basic structure of Function, implement cloth/download/installation/running based on package management
+
+## Some basic standard Functions (Senior*2)
+Can refer to some existing work, the main work in the early stage is standardization, as much as possible in information reading, the write operation that needs to be authorized by users needs to be promoted for consideration
+Get the physical information of the specified location: time, temperature, humidity
+
+# Compute Runtime (Architecture*1)
+Except for the Agent, all components run in isolated running containers
+The Agent also runs in a container, and its isolation conditions may be stricter than the running container.
+Built based on docker,
+
+# Compute Task Manager (Architecture*2)
+Manage computational tasks that require more computational resources.
+Compute tasks can be completed locally or on other devices.
+Insufficient computing resources is the norm, and scheduling work should be done here. Normal queue, give way to urgent short tasks when necessary.
+
+# Spiders
+## Spider Basic Architecture (Architecture*1 )
+## Local Storage Format Based on NamedObject Theory (Architecture*1 )
+## Email-Spider Senior*1
+No threshold at all, mainly to run the framework
+## Twitter-Spider Senior*2
+For the convenience of ordinary users to install, do not use twitter api
+But if it is really impossible to complete, then use twitter api (difficulty drops to ordinary*2)
+## Facebook-Spider Senior*2
+For the convenience of ordinary users to install, do not use facebook api
+But if it is really impossible to complete, then use facebook api (difficulty drops to ordinary*2)
+
+# Embedding Pipelines
+## Piplene Basic Architecture (Architecture*3)
+NamedObject+RawData => Vector DB
+## Image-Embedding pipeline (Senior*3)
+Implement Image vectorization pipeline (focus)
+## Doc-Embedding pipeline (Senior*3)
+Implement Text vectorization pipeline
+## Code-Embedding pipeline (Senior*2)
+## Video-Embedding pipeline (Senior*3)
+## Audio-Embedding pipeline (Senior*3)
+
+# Knowledge Base (Architecture*2)
+Provide a unified interface, support switching vector database kernel
+Integrate open source vector database (pay attention to Lience selection)
+
+When designing the interface, prepare for future access control
+
+# 0.5.1 Integration Test (Senior*3)
+Can be divided into two parts
+1.Spider -> Pipline -> Knowledge Base
+2.AI Agent <- Functions <- Knowledge Base
+3.WebUI (not necessary)
+
+## Basic OS System Construction (Architecture*1)
+The MVP version will not build a very serious system, the main goal is to set up an integrated test environment.
+
+# AI Gateway(Network OS BUS)
+Obtain data by recognizing network data
+The Gateway also provides an external access entrance for the entire system, and access control can be unified.
+Provides the bus abstraction in the network operating system (the network cable is the bus), devices within the Zone are recognized by the system as plug-and-play devices, and can be called by applications/Agents
+*Next version*
+
+# AI Browser
+By cooperating with the browser, save the web pages that users have seen to capture data
+*Next version*
+
+# IoT Functions
+On the one hand, define standard Functions according to device types
+On the other hand, it is necessary to crack some existing IoT devices so that they can be connected to the AI Gateway and can be controlled by the Agent
+*Next version*
+
+# Personal Lora Model Building
+*Next version*
+
+## Personal Image Lora
+## Personal Voice Lora
+
+# Permission and Privacy Management
+*Next version*
+
+# AI Capability (Function) Integration
+*Next version*
+
+## Text2Image
+Stable Diffusion core
+Stable Diffusion model management
+## Text2Voice
+
+
+# Telegram API Integration
+*Next version*
+
+# Web Version Personal Center
+*Next version*
+
+# Built-in Agents
+*Next version*
+
+# Built-in APPs
+*Next version*
+
+
diff --git a/doc/package_manager.md b/doc/package_manager.md
new file mode 100644
index 0000000..33d9ce7
--- /dev/null
+++ b/doc/package_manager.md
@@ -0,0 +1,126 @@
+
+
+# Problems to Solve
+The Package Manager is a fundamental component of the system for managing Packages.
+The sub system provides fundamental support for packaging, publishing, downloading, verifying, installing, and loading folders containing required packages under different scenarios. Based on relevant modules, it's easy to build a package management system similar to `apt/pip/npm`.
+
+The system design has deeply referenced Git and NDN networks. The distinction between client and server is not that important. Through cryptography, it achieves decentralized trustworthy verification. Any client can become an effective repo server through simple configuration.
+
+
+# Design
+Let's start by introducing the two important processes.
+## Load Package
+[](pkg_procedure.drawio)
+
+## Install Package
+[](pkg_procedure.drawio)
+
+Note that the dependency check during installation allows for the missing packages to be installed into the current environment.
+
+# Some Basic Concepts
+- ***env***:A target environment consisting of a series of configuration files, where packages can be loaded/installed.
+- ***pkg***:A Package(pkg) is either a folder or a file that serves the same purpose as a folder (such as zip, iso, etc.).
+- ***pkg_name***:A unique string used to label a package. It's usually a readable package name, but can also include the version number or even the ContentId.
+- ***version_id***:A complete version number is made up of channel_name and version. channel_name is generally not specified and is configured uniformly by the env (e.g., all use the `nightly` channel or all use the `release` channel). Version_ids are divided into exact versions and conditional versions.
+- ***mediainfo***:A recognizable file or folder format. Successfully loading a package means obtaining a confirmed MediaInfo, from which the package's content can be further read.
+- ***Author***: The package's author, which includes the author's friendly name (unique) and public key. This system cannot determine the trustworthiness of the relationship between the friendly name and the public key; upper layers need to extend this based on product design. Verification through DNS systems or smart contracts is common.
+- ***Distributor***:The distributor is responsible for maintaining index_db.
+- ***index_db***:A database containing a series of package information (pkg_info), maintained by the distributor. `index_db` is a complete file saved within the env. It is updated through the index_db update operation in the env from the distributor. Since -pkg_info contains the package's cid info and the corresponding author's signature, the distributor can only select the version released by the author and cannot release packages on behalf of the Author.
+- ***repo_server***:Includes pkg_server and index_db_server, which can be deployed separately.
+- ***ndn_client***: A library for trustworthy downloading of packages through the package's ContentId.
+
+# Package Env Directory Structure
+[]()
+
+The diagram represents a typical pkg_env directory structure, where:
+
+- ***pkg.cfg.toml*** The root directory contains a pkg.cfg.toml file, which is the main configuration file for the environment.
+- ***index-db*** Inside pkg.cfg.toml, there are two external files included: an external pkg.lock (local version locking) and .pkgs/index-db.toml (independently distributed package index by the distributor).
+- ***.pkgs*** Similar to .git, this folder contains a series of files and directories not directly used by users but supporting package management. It stores all different versions of packages using the naming convention $.pkg_name/pkg_name#cid.
+- ***pkgs*** This directory is user-facing, structured according to successful package installations. Installed packages are soft-linked to the actual files/directories under the .pkgs folder. This minimizes redundant file copying and makes it convenient for users to view and modify. In this example, pkg_nameA has two versions (the default version and 1.0.3), both pointing to the actual folders with CIDs in .pkgs.
+
+
+During the local testing phase, users can easily place their own packages in the pkgs directory for successful loading. Any local changes will not affect the content of the index-db, nor will it impact testing. Cryptographic verification only occurs during the download and installation process.
+
+The above environment isolation design also provides a fairly standard solution for common dependency conflicts.
+
+# Test
+
+
+## Load Package Test
+Load testing some times not depend on index-db.
+### Loading Using pkg_id
+This is the simplest mode for users.
+Not specifying a version number usually means using the default version. When an index exists, the default version is fetched from the index.
+In the absence of an index, the default version will prioritize links without suffixes; otherwise, it will use the link with the highest version.
+```python
+load("english-dict")
+```
+Actual load:
+```
+./pkgs/english-dict/
+./pkgs/english-dict#0.1.5/
+```
+
+When there's an index-db, it will determine the default version based on the index-db information and load using the directory name with the version:
+```
+./pkgs/english-dict#0.1.3/
+```
+Note that even when there's an index-db with a cid, the system still primarily loads by symbol. This gives system administrators more flexibility. Try to avoid modifying directories named with cids.
+
+
+### Loading Using pkg_id + cid
+```python
+load("english-dict#sha256:1234567890")
+```
+
+This is the simplest method and doesn't rely on index-db. The system can precisely locate and load the package, which is stored in:
+```
+./.pkgs/english-dict/sha256:1234567890/
+```
+
+Verification of media information does not occur before loading; it only takes place after the download is complete.
+
+###
+The channel is part of the version. If it's not specified, the default channel name will be read from the environment.
+If the version number is fixed, the directory is directly constructed for loading. If the version number is conditional, it depends on the locally installed version list to first determine the version, and then constructs the directory for loading.
+```python
+load("english-dict#>0.1.4")
+```
+The package will be loaded based on the actual version installed locally:
+```
+./pkgs/english-dict#0.1.5/
+```
+Automatic local repair logic for loading using an exact version number:
+At this point, if that directory does not exist, but it can be seen from the index-db that the cid corresponding to version 0.1.5 is already installed locally, loading will fail by default (simply deleting the version link effectively blocks a version).
+
+Only when the option to automatically repair links during loading is enabled (which requires permissions), will it automatically create a link to that cid directory and successfully load.
+#### Version Control
+Support Only 4 Comparison Operators: >, <, >=, <=
+
+- ***>0.1.2***: Any version greater than 0.1.2
+- ***>0.1.2<0.1.5***: Any version greater than 0.1.2 and less than 0.1.5
+- ***<0.1.2***: Any version less than 0.1.2
+
+The logic for version selection during load is as follows:
+
+1. Retrieve all locally installed versions.
+2. Based on the version selection criteria, choose one version.
+
+Note that during installation, the version chosen based on dependency information has its selection set from all versions in index-db, and it is not related to the versions already installed locally.
+
+
+## Package Installation Testing (To Be Completed)
+Installation testing depends on index-db.
+
+### Installing Using pkg_id
+Check if the installed result matches the current version specified in index-db.
+
+### Installing Using pkg_id + cid
+Verify that the installation process correctly validates the cid. After successful installation, making simple changes to the files on the server should result in a download verification failure.
+
+### Installing Using pkg_id + Version Constraints
+Check if the installed result matches the correct version specified in index-db.
+
+### Installation with Local Upgrades
+After installing using the pkg_id method, make changes to the package content, then reinstall. At this point, the locally modified version should be backed up, and the current version should be reinstalled.
\ No newline at end of file
diff --git a/doc/pkg_procedure.drawio b/doc/pkg_procedure.drawio
new file mode 100644
index 0000000..7f97bd4
--- /dev/null
+++ b/doc/pkg_procedure.drawio
@@ -0,0 +1,377 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/pkg_system_demo/demo1/.pkgs/.pkg_nameA/pkg_nameA#sha1-2aae6c35c94fcfb415dbe95f408b9ce91ee846ed/file.txt b/doc/pkg_system_demo/demo1/.pkgs/.pkg_nameA/pkg_nameA#sha1-2aae6c35c94fcfb415dbe95f408b9ce91ee846ed/file.txt
new file mode 100644
index 0000000..2fbd84d
--- /dev/null
+++ b/doc/pkg_system_demo/demo1/.pkgs/.pkg_nameA/pkg_nameA#sha1-2aae6c35c94fcfb415dbe95f408b9ce91ee846ed/file.txt
@@ -0,0 +1 @@
+ AA
\ No newline at end of file
diff --git a/doc/pkg_system_demo/demo1/.pkgs/.pkg_nameB/pkg_nameB#sha1-addf120b430021c36c232c99ef8d926aea2acd6b.zip b/doc/pkg_system_demo/demo1/.pkgs/.pkg_nameB/pkg_nameB#sha1-addf120b430021c36c232c99ef8d926aea2acd6b.zip
new file mode 100644
index 0000000..7e4e948
Binary files /dev/null and b/doc/pkg_system_demo/demo1/.pkgs/.pkg_nameB/pkg_nameB#sha1-addf120b430021c36c232c99ef8d926aea2acd6b.zip differ
diff --git a/doc/pkg_system_demo/demo1/.pkgs/index-db.toml b/doc/pkg_system_demo/demo1/.pkgs/index-db.toml
new file mode 100644
index 0000000..f72d8a4
--- /dev/null
+++ b/doc/pkg_system_demo/demo1/.pkgs/index-db.toml
@@ -0,0 +1,3 @@
+[authors]
+
+
diff --git a/doc/pkg_system_demo/demo1/pkg.cfg.toml b/doc/pkg_system_demo/demo1/pkg.cfg.toml
new file mode 100644
index 0000000..30404ce
--- /dev/null
+++ b/doc/pkg_system_demo/demo1/pkg.cfg.toml
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/doc/pkg_system_demo/demo1/pkg.lock b/doc/pkg_system_demo/demo1/pkg.lock
new file mode 100644
index 0000000..30404ce
--- /dev/null
+++ b/doc/pkg_system_demo/demo1/pkg.lock
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/doc/pkg_system_demo/demo1/pkgs/pkg_nameA#1.0.3/file.txt b/doc/pkg_system_demo/demo1/pkgs/pkg_nameA#1.0.3/file.txt
new file mode 100644
index 0000000..30404ce
--- /dev/null
+++ b/doc/pkg_system_demo/demo1/pkgs/pkg_nameA#1.0.3/file.txt
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/doc/pkg_system_demo/demo1/pkgs/pkg_nameA/file.txt b/doc/pkg_system_demo/demo1/pkgs/pkg_nameA/file.txt
new file mode 100644
index 0000000..30404ce
--- /dev/null
+++ b/doc/pkg_system_demo/demo1/pkgs/pkg_nameA/file.txt
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/doc/pkg_system_demo/demo1/pkgs/pkg_nameB.zip b/doc/pkg_system_demo/demo1/pkgs/pkg_nameB.zip
new file mode 100644
index 0000000..15cb0ec
Binary files /dev/null and b/doc/pkg_system_demo/demo1/pkgs/pkg_nameB.zip differ
diff --git a/doc/pkg_tree.png b/doc/pkg_tree.png
new file mode 100644
index 0000000..e64bb25
Binary files /dev/null and b/doc/pkg_tree.png differ
diff --git a/doc/process model.drawio b/doc/process model.drawio
new file mode 100644
index 0000000..d78b1db
--- /dev/null
+++ b/doc/process model.drawio
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/res/design.png b/doc/res/design.png
new file mode 100644
index 0000000..3c20718
Binary files /dev/null and b/doc/res/design.png differ
diff --git a/doc/res/jarvis.png b/doc/res/jarvis.png
new file mode 100644
index 0000000..b7a3b98
Binary files /dev/null and b/doc/res/jarvis.png differ
diff --git a/doc/res/mvp.png b/doc/res/mvp.png
new file mode 100644
index 0000000..3f218ce
Binary files /dev/null and b/doc/res/mvp.png differ
diff --git a/doc/res/workflow.png b/doc/res/workflow.png
new file mode 100644
index 0000000..d4af863
Binary files /dev/null and b/doc/res/workflow.png differ
diff --git a/doc/workflow.drawio b/doc/workflow.drawio
new file mode 100644
index 0000000..0cc08b1
--- /dev/null
+++ b/doc/workflow.drawio
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/rootfs/agents/David/agent.toml b/rootfs/agents/David/agent.toml
new file mode 100644
index 0000000..47d2ff8
--- /dev/null
+++ b/rootfs/agents/David/agent.toml
@@ -0,0 +1,14 @@
+instance_id = "David"
+fullname = "David"
+max_token_size = 16000
+
+owner_env = "paint"
+
+[[prompt]]
+role = "system"
+content = """You are an artist, and you will use the 'paint' function in the llm_inner_functions to create artwork.
+You will extract relevant keywords based on user input and requirements, and pass these keywords to the 'prompt' parameter of the 'paint' function.
+When a specific model is mentioned, pass the model's name to the 'model_name' parameter of the 'paint' function.
+If there are instructions not to depict certain content, summarize this content and pass it as keywords to the 'negative_prompt' parameter.
+All parameters must be in English.
+When the user mentions creating a children's picture book, use the "realisticVisionV51_v51VAE" model, and append the keyword " " to the 'prompt' parameter."""
diff --git a/rootfs/agents/Jarvis/agent.toml b/rootfs/agents/Jarvis/agent.toml
new file mode 100644
index 0000000..a69e6fb
--- /dev/null
+++ b/rootfs/agents/Jarvis/agent.toml
@@ -0,0 +1,31 @@
+instance_id = "Jarvis"
+fullname = "Jarvis"
+llm_model_name = "gpt-3.5-turbo-16k-0613"
+max_token_size = 16000
+#enable_kb = "true"
+enable_timestamp = "true"
+owner_prompt = "I am your master{name}"
+contact_prompt = "I am your master's friend{name}"
+owner_env = "calender"
+
+[[prompt]]
+role = "system"
+content = """
+You are named Jarvis, the super personal assistant to the master.
+You lead a team serving the master, the members of which are:
+Tracy, the private English tutor,
+Mia, the master's personal document management expert.
+
+***
+Sometimes the information you see will carry a timestamp. This is to give you a better understanding of when the message was created. When you reply to messages, you do not include this time stamp.
+
+Upon receiving a message, handle it according to the following rules:
+1. If you believe someone in the team is better suited to address the message, forward the message to them using the method below:
+##/send_msg "MemberName"
+Message content
+2.You can access the master's Calendar to view his schedule. If you need to modify the master's schedule while processing a message, please adjust it using the appropriate method.
+3.Be mindful of the identity of the person you are chatting with and provide services accordingly based on their status.
+4.For messages that don't follow the above rules, do your best to handle them.
+"""
+
+
diff --git a/rootfs/agents/Lachlan/agent.toml b/rootfs/agents/Lachlan/agent.toml
new file mode 100644
index 0000000..b0cf561
--- /dev/null
+++ b/rootfs/agents/Lachlan/agent.toml
@@ -0,0 +1,12 @@
+instance_id = "Lachlan"
+fullname = "Lachlan"
+llm_model_name="Llama-2-13b-chat"
+max_token_size=4000
+
+
+[[prompt]]
+role = "system"
+content = """
+Your name is Lachlan, and you are my advanced private Spanish tutor.
+You are also a local guide familiar with the history of the Inca Empire. While teaching me Spanish, you will introduce some related historical and cultural origins.
+"""
\ No newline at end of file
diff --git a/rootfs/agents/Mia/agent.toml b/rootfs/agents/Mia/agent.toml
new file mode 100644
index 0000000..770c07e
--- /dev/null
+++ b/rootfs/agents/Mia/agent.toml
@@ -0,0 +1,24 @@
+instance_id = "Mia"
+fullname = "Mia"
+#llm_model_name = "gpt-4"
+#max_token_size = 16000
+#enable_function =["add_event"]
+#enable_kb = "true"
+#enable_timestamp = "false"
+owner_prompt = "我是你的主人{name}"
+contact_prompt = "我是你的朋友{name}"
+owner_env = "knowledge"
+
+[[prompt]]
+role = "system"
+content = """
+你叫Mia,你可以访问我的个人知识库。
+
+***
+你在收到我的信息后,按如下规则处理
+1. 在第一次接受到一条信息时,优先尝试用合适的关键字查询去查询知识库。
+2. 如果信息中包含一段知识库的查询结果,尝试用查询结果处理,如果还是不能处理,尝试递增index继续查询。
+3. 如果要返回知识库结果条目,在消息开头附上他的json字符串。
+4. 如果知识库返回不了结果了,请尽力返回。
+"""
+
diff --git a/rootfs/agents/Thinker/agent.toml b/rootfs/agents/Thinker/agent.toml
new file mode 100644
index 0000000..b4016d5
--- /dev/null
+++ b/rootfs/agents/Thinker/agent.toml
@@ -0,0 +1,20 @@
+instance_id = "Thinker"
+fullname = "Thinker"
+llm_model_name = "gpt-3.5-turbo-16k-0613"
+max_token_size = 14000
+#enable_function =["add_event"]
+#enable_kb = "true"
+
+[[prompt]]
+role = "system"
+content = """
+You are the best deep thinking in the world, and you will think about the information I give you, sometimes some chat records.Then you will generate a briefing or summary of no more than 400 words based on this information.
+You mainly use the following methods to generate summary:
+1. Try to understand the theme of each sentence, and call the relevant operation to record the relationship between the dialogue and the theme
+2. Try to analyze the personality of different people involved in information
+3. Try to summarize important events in the information and record it
+4. Try to understand the attitude of different people on different topics or events
+5. For the key information or TODO in the information, such as the time, place, amount and other information of the certainty, it must be stored in the summary.
+
+You have a summary of simplicity and profound nonsense, and you don't need to have any polite words to me.Just give me a summary.
+"""
diff --git a/rootfs/agents/Tracy/agent.toml b/rootfs/agents/Tracy/agent.toml
new file mode 100644
index 0000000..dfd3cd9
--- /dev/null
+++ b/rootfs/agents/Tracy/agent.toml
@@ -0,0 +1,25 @@
+instance_id = "Tracy"
+fullname = "Tracy"
+
+[[prompt]]
+role = "system"
+content = """
+Your name is Tracy, and you are my advanced private English tutor.
+1. Engage in a simulated dialogue with me smoothly, helping me practice everyday English. While conversing with me, if necessary, you will adjust my input to sound more like authentic American English.
+2. Depending on my level of English, you will annotate potentially incorrect words with phonetic symbols or provide expanded explanations for certain words and phrases.
+3. If I send you something that is not in English, it means I don't know how to say it in American English. You will first translate what I've sent into English and then respond according to the above rules.
+4. You will chat with me like a friend, rather than just teaching me lessons.
+
+The first message I sent you might be a work summary from your past. Please use this work summary to guide subsequent teaching.
+"""
+
+[[think_prompt]]
+role = "system"
+content = """
+Your name is Tracy, and you are my advanced private English tutor.
+You will receive two pieces of information from me next. The first is a work summary you previously organized, and the second is a record of your recent teaching work. You need to combine these two records, engage in deep introspective thinking, and produce a work summary.
+1. A comprehensive assessment of the students' English proficiency.
+2. Evaluation of students' personalities and hobbies, along with suggestions for teaching methods they might prefer.
+3. Assessment of past teaching methods and thoughts on improvements.
+4. If there are specific unfinished tasks, key information should be recorded.
+"""
\ No newline at end of file
diff --git a/rootfs/agents/agents.cfg b/rootfs/agents/agents.cfg
new file mode 100644
index 0000000..c9f8d63
--- /dev/null
+++ b/rootfs/agents/agents.cfg
@@ -0,0 +1,2 @@
+main = "./"
+cache = "./.agents"
\ No newline at end of file
diff --git a/rootfs/agents/ai_bash/agent.toml b/rootfs/agents/ai_bash/agent.toml
new file mode 100644
index 0000000..3f1cd6e
--- /dev/null
+++ b/rootfs/agents/ai_bash/agent.toml
@@ -0,0 +1,20 @@
+instance_id = "ai_bash"
+fullname = "ai_bash"
+owner_env = "bash"
+llm_model_name = "gpt-3.5-turbo-16k-0613"
+max_token_size = 16000
+[[prompt]]
+role = "system"
+content = """
+## Your name is ai_bash
+You are a very experienced system administrator,You are proficient in system administration and contextual commands for all mainstream operating systems.
+
+I understand some Linux, but I don't remember the bash commands very clearly.
+I will give you the following types of inputs:
+I will give you standard console commands, and you will try to execute them directly based on the type of the current system.
+1. Standard console (bash/DOS/PowerShell) commands. If you think these commands are correct and executable on the current system, then you can execute them directly.
+2. If my command is incorrect, or the command may be harmful to the system, you need to adjust these commands. First, tell me the adjusted commands. After I confirm, you can execute the correct commands.
+3. If the information I give you is not a console command, but some requirements. You can try to understand and then give a set of commands to implement these requirements. After I confirm, execute them.
+4. For other information, please do your best to execute from your professional perspective.
+5. After each command execution, tell me the result.
+"""
diff --git a/rootfs/agents/fairy_tale_writer/agent.toml b/rootfs/agents/fairy_tale_writer/agent.toml
new file mode 100644
index 0000000..82d1fa0
--- /dev/null
+++ b/rootfs/agents/fairy_tale_writer/agent.toml
@@ -0,0 +1,8 @@
+instance_id = "fairy_tale_writer"
+fullname = "tracy wang"
+llm_model_name = "gpt-3.5-turbo-16k-0613"
+enable_function = []
+
+[[prompt]]
+role = "system"
+content = "You are a fairy tale writer who can write all kinds of interesting fairy tale."
diff --git a/rootfs/agents/manager/agent.toml b/rootfs/agents/manager/agent.toml
new file mode 100644
index 0000000..c7ca597
--- /dev/null
+++ b/rootfs/agents/manager/agent.toml
@@ -0,0 +1,8 @@
+instance_id = "agent:xxxxxxabcde"
+fullname = "musk"
+enable_function = []
+
+[[prompt]]
+role = "system"
+content = "You have rich management skills, and you are good at disassembling complex work into simple tasks, so that team members are efficiently collaborated."
+
diff --git a/rootfs/agents/math_teacher/agent.toml b/rootfs/agents/math_teacher/agent.toml
new file mode 100644
index 0000000..871b29a
--- /dev/null
+++ b/rootfs/agents/math_teacher/agent.toml
@@ -0,0 +1,6 @@
+instance_id = "math_teacher"
+fullname = "the one"
+llm_model_name = "gpt-4-0613"
+[[prompt]]
+role = "system"
+content = "You are a teacher who is proficient in mathematics"
diff --git a/rootfs/agents/speecher/agent.toml b/rootfs/agents/speecher/agent.toml
new file mode 100644
index 0000000..7747d65
--- /dev/null
+++ b/rootfs/agents/speecher/agent.toml
@@ -0,0 +1,8 @@
+instance_id = "speecher"
+fullname = "tracy wang"
+llm_model_name = "gpt-3.5-turbo-16k-0613"
+enable_function = ["text_to_speech"]
+
+[[prompt]]
+role = "system"
+content = "You are a stories broadcaster who can broadcast the story into audio. Before the broadcast, you need to adapt the story into a broadcast script, extract the narration and character lines, and each character needs gender, age, and the tone of each line.If you generate audio files, inform your users."
diff --git a/rootfs/email/config.toml b/rootfs/email/config.toml
new file mode 100644
index 0000000..1bf4bca
--- /dev/null
+++ b/rootfs/email/config.toml
@@ -0,0 +1,7 @@
+
+
+EMAIL_IMAP_SERVER = "imap.gmail.com"
+EMAIL_ADDRESS = '<>'
+EMAIL_PASSWORD = '<>'
+EMAIL_IMAP_PORT = 993
+LOCAL_DIR = 'rootfs/data'
\ No newline at end of file
diff --git a/rootfs/templetes/templetes.cfg b/rootfs/templetes/templetes.cfg
new file mode 100644
index 0000000..9cff26f
--- /dev/null
+++ b/rootfs/templetes/templetes.cfg
@@ -0,0 +1,2 @@
+main = "./"
+cache = "./.templetes"
\ No newline at end of file
diff --git a/rootfs/tunnels.cfg.toml b/rootfs/tunnels.cfg.toml
new file mode 100644
index 0000000..32975de
--- /dev/null
+++ b/rootfs/tunnels.cfg.toml
@@ -0,0 +1,19 @@
+[[tunnels]]
+tunnel_id = "MyRoobot"
+type="TelegramTunnel"
+target="agent_1"
+token="your_token"
+
+
+[[tunnels]]
+tunnel_id="MyEmailRobot"
+type="EmailTunnel"
+target="agent_1"
+
+email="youremail@msn.com"
+imap="outlook.office365.com:993"
+smtp="outlook.office365.com:587"
+user=""
+password=""
+folder="inbox"
+interval=10
\ No newline at end of file
diff --git a/rootfs/workflows/math_school/workflow.toml b/rootfs/workflows/math_school/workflow.toml
new file mode 100644
index 0000000..cb6d8f1
--- /dev/null
+++ b/rootfs/workflows/math_school/workflow.toml
@@ -0,0 +1,65 @@
+name = "math_school"
+
+[enviroment]
+GOAL="成为最好的学校"
+
+
+[[connected_env]]
+env_id = "calender"
+[[connected_env.event2msg]]
+timer = "现在是{now}"
+role = "教导处主任"
+
+[filter]
+"*" = "小学老师"
+
+[roles."小学老师"]
+name = "小学老师"
+fullname = "Ada Zhang"
+agent="math_teacher"
+[[roles."小学老师".prompt]]
+role="system"
+content="""你在学校任职,担任小学老师。学校由 小学老师、初中老师、高中老师、教导处主任 组成。
+你的任何处理结果,都要用下面方式汇报给给教导处主任,并根据教导处主任的指示,产生最终回复
+```
+##/send_msg 教导处主任
+处理结果
+```
+
+"""
+
+
+[roles."初中老师"]
+name = "初中老师"
+fullname = "Mark Wang"
+agent="math_teacher"
+[[roles."初中老师".prompt]]
+role="system"
+content="""你在学校任职,担任初中老师。
+当你发现学生的水平不是初中生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师
+当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。
+你会根据教导处主任的指示,定期调整教学方法"""
+
+[roles."高中老师"]
+name = "高中老师"
+fullname = "Hong Sun"
+agent="math_teacher"
+
+[[roles."高中老师".prompt]]
+role="system"
+content="""你在学校任职,担任高中老师。
+当你发现学生的水平不是高中生时,应使用 sendmsg(老师名称,问题) 的方法,把学生的问题转发给学校里合适的老师
+当学生发来作业时,进行批改(满分5分),并把批改结果以 postmsg(教导处主任,学生名_作业结果) 的方法,将一次作业情况汇报给教导处主任。
+你会根据教导处主任的指示,定期调整教学方法"""
+
+[roles."教导处主任"]
+name = "教导处主任"
+fullname = "Green King"
+agent="math_teacher"
+
+[[roles."教导处主任".prompt]]
+role="system"
+content="""你在学校任职,担任教导处主任。你的目标是{GOAL}
+你收到老师发来的信息时,如果是类似 学生名_作业分数 的结果,会在合适的情况下根据学生作业的整体情况,对老师的教学方法进行必要的调整。
+当收到非老师发来的时间信息时,回复那一天学生的平均分。"""
+
diff --git a/rootfs/workflows/story_maker/workflow.toml b/rootfs/workflows/story_maker/workflow.toml
new file mode 100644
index 0000000..4f9fbef
--- /dev/null
+++ b/rootfs/workflows/story_maker/workflow.toml
@@ -0,0 +1,51 @@
+name = "story_maker"
+
+
+[filter]
+"*" = "manager"
+
+[roles.manager]
+name = "manager"
+fullname = "总导演"
+agent="manager"
+enable_function = []
+
+[[roles.manager.prompt]]
+role="system"
+content="""
+你当前的职位是语音故事制作总负责人,负责与客户对接并向团队下达指令,不需要自己直接完成任务。你的团队分为下面两个成员:writer,speecher。一个故事制作分成两个阶段:让writer写出故事,再交由speecher演播故事生成音频文件。你的基本工作模式是:
+1. 收到客户的明确的指令后,让writer写出故事
+2. 将writer写出的故事交给speecher演播
+3. 获得音频文件之后,整个任务已完成,将音频文件的存放路径以如下格式返回给客户:
+```
+故事制作完成。
+audio file:音频文件路径
+```
+4. 当你决定要和成员通信时,请使用下面形式输出需要通信的消息
+```
+##/send_msg "成员名称"
+内容
+```
+"""
+
+[roles.writer]
+name = "writer"
+agent = "fairy_tale_writer"
+fullname = "作家"
+enable_function = []
+[[roles.writer.prompt]]
+role="system"
+content=""
+
+[roles.speecher]
+name = "speecher"
+agent = "speecher"
+enable_function = ["text_to_speech"]
+[[roles.speecher.prompt]]
+role="system"
+content="""你现在的职责是演播一个故事,故事完成之后请以如下格式返回:
+```
+故事演播完成。
+audio file:音频文件路径
+```
+"""
diff --git a/rootfs/workflows/workflows.cfg b/rootfs/workflows/workflows.cfg
new file mode 100644
index 0000000..f8a61a0
--- /dev/null
+++ b/rootfs/workflows/workflows.cfg
@@ -0,0 +1,2 @@
+main = "./"
+cache = "./.workflows"
\ No newline at end of file
diff --git a/src/aios_kernel/__init__.py b/src/aios_kernel/__init__.py
new file mode 100644
index 0000000..42d1b65
--- /dev/null
+++ b/src/aios_kernel/__init__.py
@@ -0,0 +1,29 @@
+from .environment import Environment,EnvironmentEvent
+from .agent_message import AgentMsg,AgentMsgStatus,AgentMsgType
+from .chatsession import AIChatSession
+from .agent import AIAgent,AIAgentTemplete,AgentPrompt
+from .compute_kernel import ComputeKernel,ComputeTask,ComputeTaskResult,ComputeTaskState,ComputeTaskType
+from .compute_node import ComputeNode,LocalComputeNode
+from .open_ai_node import OpenAI_ComputeNode
+from .knowledge_base import KnowledgeBase, KnowledgeEnvironment
+from .knowledge_pipeline import KnowledgeEmailSource, KnowledgeDirSource, KnowledgePipline
+from .role import AIRole,AIRoleGroup
+from .workflow import Workflow
+from .bus import AIBus
+from .workflow_env import WorkflowEnvironment,CalenderEnvironment,CalenderEvent,PaintEnvironment
+from .local_llama_compute_node import LocalLlama_ComputeNode
+from .whisper_node import WhisperComputeNode
+from .google_text_to_speech_node import GoogleTextToSpeechNode
+from .tunnel import AgentTunnel
+from .tg_tunnel import TelegramTunnel
+from .email_tunnel import EmailTunnel
+from .storage import ResourceLocation,AIStorage,UserConfig,UserConfigItem
+from .contact_manager import ContactManager,Contact,FamilyMember
+from .text_to_speech_function import TextToSpeechFunction
+from .workspace_env import WorkspaceEnvironment
+from .local_stability_node import Local_Stability_ComputeNode
+from .stability_node import Stability_ComputeNode
+from .local_st_compute_node import LocalSentenceTransformer_Text_ComputeNode,LocalSentenceTransformer_Image_ComputeNode
+from .compute_node_config import ComputeNodeConfig
+AIOS_Version = "0.5.1, build 2023-9-28"
+
diff --git a/src/aios_kernel/agent.py b/src/aios_kernel/agent.py
new file mode 100644
index 0000000..563d49e
--- /dev/null
+++ b/src/aios_kernel/agent.py
@@ -0,0 +1,702 @@
+from typing import Optional
+
+from asyncio import Queue
+import asyncio
+import logging
+import uuid
+import time
+import json
+import shlex
+import datetime
+import copy
+
+from .agent_message import AgentMsg, AgentMsgStatus, AgentMsgType,FunctionItem,LLMResult
+from .chatsession import AIChatSession
+from .compute_task import ComputeTaskResult,ComputeTaskResultCode
+from .ai_function import AIFunction
+from .environment import Environment
+from .contact_manager import ContactManager,Contact,FamilyMember
+
+logger = logging.getLogger(__name__)
+
+class AgentPrompt:
+ def __init__(self,prompt_str = None) -> None:
+ self.messages = []
+ if prompt_str:
+ self.messages.append({"role":"user","content":prompt_str})
+ self.system_message = None
+
+ def as_str(self)->str:
+ result_str = ""
+ if self.system_message:
+ result_str += self.system_message.get("role") + ":" + self.system_message.get("content") + "\n"
+ if self.messages:
+ for msg in self.messages:
+ result_str += msg.get("role") + ":" + msg.get("content") + "\n"
+
+ return result_str
+
+ def to_message_list(self):
+ result = []
+ if self.system_message:
+ result.append(self.system_message)
+ result.extend(self.messages)
+ return result
+
+ def append(self,prompt):
+ if prompt is None:
+ return
+
+ if prompt.system_message is not None:
+ if self.system_message is None:
+ self.system_message = copy.deepcopy(prompt.system_message)
+ else:
+ self.system_message["content"] += prompt.system_message.get("content")
+
+ self.messages.extend(prompt.messages)
+
+ def get_prompt_token_len(self):
+ result = 0
+
+ if self.system_message:
+ result += len(self.system_message.get("content"))
+ for msg in self.messages:
+ result += len(msg.get("content"))
+
+ return result
+
+ def load_from_config(self,config:list) -> bool:
+ if isinstance(config,list) is not True:
+ logger.error("prompt is not list!")
+ return False
+ self.messages = []
+ for msg in config:
+ if msg.get("role") == "system":
+ self.system_message = msg
+ else:
+ self.messages.append(msg)
+ return True
+
+
+class AIAgentTemplete:
+ def __init__(self) -> None:
+ self.llm_model_name:str = "gpt-4-0613"
+ self.max_token_size:int = 0
+ self.template_id:str = None
+ self.introduce:str = None
+ self.author:str = None
+ self.prompt:AgentPrompt = None
+
+ def load_from_config(self,config:dict) -> bool:
+ if config.get("llm_model_name") is not None:
+ self.llm_model_name = config["llm_model_name"]
+ if config.get("max_token_size") is not None:
+ self.max_token_size = config["max_token_size"]
+ if config.get("template_id") is not None:
+ self.template_id = config["template_id"]
+ if config.get("prompt") is not None:
+ self.prompt = AgentPrompt()
+ if self.prompt.load_from_config(config["prompt"]) is False:
+ logger.error("load prompt from config failed!")
+ return False
+
+
+ return True
+
+
+class AIAgent:
+ def __init__(self) -> None:
+ self.agent_prompt:AgentPrompt = None
+ self.agent_think_prompt:AgentPrompt = None
+ self.llm_model_name:str = None
+ self.max_token_size:int = 3600
+ self.agent_id:str = None
+ self.template_id:str = None
+ self.fullname:str = None
+ self.powerby = None
+ self.enable = True
+ self.enable_kb = False
+ self.enable_timestamp = False
+ self.guest_prompt_str = None
+ self.owner_promp_str = None
+ self.contact_prompt_str = None
+ self.history_len = 10
+
+ self.chat_db = None
+ self.unread_msg = Queue() # msg from other agent
+ self.owner_env : Environment = None
+ self.owenr_bus = None
+ self.enable_function_list = None
+
+ @classmethod
+ def create_from_templete(cls,templete:AIAgentTemplete, fullname:str):
+ # Agent just inherit from templete on craete,if template changed,agent will not change
+ result_agent = AIAgent()
+ result_agent.llm_model_name = templete.llm_model_name
+ result_agent.max_token_size = templete.max_token_size
+ result_agent.template_id = templete.template_id
+ result_agent.agent_id = "agent#" + uuid.uuid4().hex
+ result_agent.fullname = fullname
+ result_agent.powerby = templete.author
+ result_agent.agent_prompt = templete.prompt
+ return result_agent
+
+ def load_from_config(self,config:dict) -> bool:
+ if config.get("instance_id") is None:
+ logger.error("agent instance_id is None!")
+ return False
+ self.agent_id = config["instance_id"]
+
+ if config.get("fullname") is None:
+ logger.error(f"agent {self.agent_id} fullname is None!")
+ return False
+ self.fullname = config["fullname"]
+
+ if config.get("prompt") is not None:
+ self.agent_prompt = AgentPrompt()
+ self.agent_prompt.load_from_config(config["prompt"])
+
+ if config.get("think_prompt") is not None:
+ self.agent_think_prompt = AgentPrompt()
+ self.agent_think_prompt.load_from_config(config["think_prompt"])
+
+ if config.get("guest_prompt") is not None:
+ self.guest_prompt_str = config["guest_prompt"]
+
+ if config.get("owner_prompt") is not None:
+ self.owner_promp_str = config["owner_prompt"]
+
+ if config.get("contact_prompt") is not None:
+ self.contact_prompt_str = config["contact_prompt"]
+
+ if config.get("owner_env") is not None:
+ self.owner_env = Environment.get_env_by_id(config["owner_env"])
+
+ if config.get("powerby") is not None:
+ self.powerby = config["powerby"]
+ if config.get("template_id") is not None:
+ self.template_id = config["template_id"]
+ if config.get("llm_model_name") is not None:
+ self.llm_model_name = config["llm_model_name"]
+ if config.get("max_token_size") is not None:
+ self.max_token_size = config["max_token_size"]
+ if config.get("enable_function") is not None:
+ self.enable_function_list = config["enable_function"]
+ if config.get("enable_kb") is not None:
+ self.enable_kb = bool(config["enable_kb"])
+ if config.get("enable_timestamp") is not None:
+ self.enable_timestamp = bool(config["enable_timestamp"])
+ if config.get("history_len"):
+ self.history_len = int(config.get("history_len"))
+ return True
+
+
+ def _get_llm_result_type(self,llm_result_str:str) -> LLMResult:
+ r = LLMResult()
+ if llm_result_str is None:
+ r.state = "ignore"
+ return r
+ if llm_result_str == "ignore":
+ r.state = "ignore"
+ return r
+
+ lines = llm_result_str.splitlines()
+ is_need_wait = False
+
+ def check_args(func_item:FunctionItem):
+ match func_name:
+ case "send_msg":# sendmsg($target_id,$msg_content)
+ if len(func_args) != 1:
+ logger.error(f"parse sendmsg failed! {func_name}")
+ return False
+ new_msg = AgentMsg()
+ target_id = func_item.args[0]
+ msg_content = func_item.body
+ new_msg.set(self.agent_id,target_id,msg_content)
+
+ r.send_msgs.append(new_msg)
+ is_need_wait = True
+
+ case "post_msg":# postmsg($target_id,$msg_content)
+ if len(func_args) != 1:
+ logger.error(f"parse postmsg failed! {func_name}")
+ return False
+ new_msg = AgentMsg()
+ target_id = func_item.args[0]
+ msg_content = func_item.body
+ new_msg.set(self.agent_id,target_id,msg_content)
+ r.post_msgs.append(new_msg)
+
+ case "call":# call($func_name,$args_str)
+ r.calls.append(func_item)
+ is_need_wait = True
+ return True
+ case "post_call": # post_call($func_name,$args_str)
+ r.post_calls.append(func_item)
+ return True
+
+ current_func : FunctionItem = None
+ for line in lines:
+ if line.startswith("##/"):
+ if current_func:
+ if check_args(current_func) is False:
+ r.resp += current_func.dumps()
+
+ func_name,func_args = AgentMsg.parse_function_call(line[3:])
+ current_func = FunctionItem(func_name,func_args)
+ else:
+ if current_func:
+ current_func.append_body(line + "\n")
+ else:
+ r.resp += line + "\n"
+
+ if current_func:
+ if check_args(current_func) is False:
+ r.resp += current_func.dumps()
+
+ if len(r.send_msgs) > 0 or len(r.calls) > 0:
+ r.state = "waiting"
+ else:
+ r.state = "reponsed"
+
+ return r
+
+ def _get_remote_user_prompt(self,remote_user:str) -> AgentPrompt:
+ cm = ContactManager.get_instance()
+ contact = cm.find_contact_by_name(remote_user)
+ if contact is None:
+ #create guest prompt
+ if self.guest_prompt_str is not None:
+ prompt = AgentPrompt()
+ prompt.system_message = {"role":"system","content":self.guest_prompt_str}
+ return prompt
+ return None
+ else:
+ if contact.is_family_member:
+ if self.owner_promp_str is not None:
+ real_str = self.owner_promp_str.format_map(contact.to_dict())
+ prompt = AgentPrompt()
+ prompt.system_message = {"role":"system","content":real_str}
+ return prompt
+ else:
+ if self.contact_prompt_str is not None:
+ real_str = self.contact_prompt_str.format_map(contact.to_dict())
+ prompt = AgentPrompt()
+ prompt.system_message = {"role":"system","content":real_str}
+ return prompt
+
+ return None
+
+ def _get_inner_functions(self) -> dict:
+ if self.owner_env is None:
+ return None,0
+
+ all_inner_function = self.owner_env.get_all_ai_functions()
+ if all_inner_function is None:
+ return None,0
+
+ result_func = []
+ result_len = 0
+ for inner_func in all_inner_function:
+ func_name = inner_func.get_name()
+ if self.enable_function_list is not None:
+ if len(self.enable_function_list) > 0:
+ if func_name not in self.enable_function_list:
+ logger.debug(f"ageint {self.agent_id} ignore inner func:{func_name}")
+ continue
+
+ this_func = {}
+ this_func["name"] = func_name
+ this_func["description"] = inner_func.get_description()
+ this_func["parameters"] = inner_func.get_parameters()
+ result_len += len(json.dumps(this_func)) / 4
+ result_func.append(this_func)
+
+ return result_func,result_len
+
+ async def _execute_func(self,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg,stack_limit = 5) -> [str,int]:
+ from .compute_kernel import ComputeKernel
+
+ func_name = inenr_func_call_node.get("name")
+ arguments = json.loads(inenr_func_call_node.get("arguments"))
+ logger.info(f"llm execute inner func:{func_name} ({json.dumps(arguments)})")
+
+ func_node : AIFunction = self.owner_env.get_ai_function(func_name)
+ if func_node is None:
+ result_str = f"execute {func_name} error,function not found"
+ else:
+ ineternal_call_record = AgentMsg.create_internal_call_msg(func_name,arguments,org_msg.get_msg_id(),org_msg.target)
+ try:
+ result_str:str = await func_node.execute(**arguments)
+ except Exception as e:
+ result_str = f"execute {func_name} error:{str(e)}"
+ logger.error(f"llm execute inner func:{func_name} error:{e}")
+
+
+ logger.info("llm execute inner func result:" + result_str)
+ inner_functions,inner_function_len = self._get_inner_functions()
+ prompt.messages.append({"role":"function","content":result_str,"name":func_name})
+ task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions)
+ if task_result.result_code != ComputeTaskResultCode.OK:
+ logger.error(f"llm compute error:{task_result.error_str}")
+ return task_result.error_str,1
+
+ ineternal_call_record.result_str = task_result.result_str
+ ineternal_call_record.done_time = time.time()
+ org_msg.inner_call_chain.append(ineternal_call_record)
+
+ if stack_limit > 0:
+ result_message = task_result.result.get("message")
+ if result_message:
+ inner_func_call_node = result_message.get("function_call")
+
+ if inner_func_call_node:
+ return await self._execute_func(inner_func_call_node,prompt,org_msg,stack_limit-1)
+ else:
+ return task_result.result_str,0
+
+ async def _get_agent_prompt(self) -> AgentPrompt:
+ return self.agent_prompt
+
+ async def _get_agent_think_prompt(self) -> AgentPrompt:
+ return self.agent_think_prompt
+
+ def _format_msg_by_env_value(self,prompt:AgentPrompt):
+ if self.owner_env is None:
+ return
+
+ for msg in prompt.messages:
+ old_content = msg.get("content")
+ msg["content"] = old_content.format_map(self.owner_env)
+
+ async def _handle_event(self,event):
+ if event.type == "AgentThink":
+ return await self._do_think()
+
+
+ async def _do_think(self):
+ #1) load all sessions
+ session_id_list = AIChatSession.list_session(self.agent_id,self.chat_db)
+ #2) get history from session in token limit
+ for session_id in session_id_list:
+ await self.think_chatsession(session_id)
+
+ #4) advanced: reload all chatrecord,and think the topic of message.
+ #5) some topic could be end(not be thinked in futured )
+ return
+
+ async def think_chatsession(self,session_id):
+ if self.agent_think_prompt is None:
+ return
+ logger.info(f"agent {self.agent_id} think session {session_id}")
+ from .compute_kernel import ComputeKernel
+ chatsession = AIChatSession.get_session_by_id(session_id,self.chat_db)
+
+ while True:
+ cur_pos = chatsession.summarize_pos
+ summary = chatsession.summary
+ prompt:AgentPrompt = AgentPrompt()
+ #prompt.append(self._get_agent_prompt())
+ prompt.append(await self._get_agent_think_prompt())
+ system_prompt_len = prompt.get_prompt_token_len()
+ #think env?
+ history_prompt,next_pos = await self._get_history_prompt_for_think(chatsession,summary,system_prompt_len,cur_pos)
+ prompt.append(history_prompt)
+ is_finish = next_pos - cur_pos < 2
+ if is_finish:
+ logger.info(f"agent {self.agent_id} think session {session_id} is finished!,no more history")
+ break
+ #3) llm summarize chat history
+ task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,None)
+ if task_result.result_code != ComputeTaskResultCode.OK:
+ logger.error(f"llm compute error:{task_result.error_str}")
+ break
+ else:
+ new_summary= task_result.result_str
+ logger.info(f"agent {self.agent_id} think session {session_id} from {cur_pos} to {next_pos} summary:{new_summary}")
+ chatsession.update_think_progress(next_pos,new_summary)
+
+
+
+ return
+
+ async def _process_group_chat_msg(self,msg:AgentMsg) -> AgentMsg:
+ from .compute_kernel import ComputeKernel
+ from .bus import AIBus
+
+ session_topic = msg.target + "#" + msg.topic
+ chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
+ need_process = False
+ if msg.mentions is not None:
+ if self.agent_id in msg.mentions:
+ need_process = True
+ logger.info(f"agent {self.agent_id} recv a group chat message from {msg.sender},but is not mentioned,ignore!")
+
+ if need_process is not True:
+ chatsession.append(msg)
+ resp_msg = msg.create_group_resp_msg(self.agent_id,"")
+ return resp_msg
+ else:
+ msg_prompt = AgentPrompt()
+ msg_prompt.messages = [{"role":"user","content":f"{msg.sender}:{msg.body}"}]
+
+ prompt = AgentPrompt()
+ prompt.append(await self._get_agent_prompt())
+ self._format_msg_by_env_value(prompt)
+ inner_functions,function_token_len = self._get_inner_functions()
+
+ system_prompt_len = prompt.get_prompt_token_len()
+ input_len = len(msg.body)
+
+ history_prmpt,history_token_len = await self._get_prompt_from_session_for_groupchat(chatsession,system_prompt_len + function_token_len,input_len)
+ prompt.append(history_prmpt) # chat context
+ prompt.append(msg_prompt)
+
+ logger.debug(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len}, totoal prompt:{system_prompt_len + function_token_len + history_token_len} ")
+ task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions)
+ if task_result.result_code != ComputeTaskResultCode.OK:
+ logger.error(f"llm compute error:{task_result.error_str}")
+ error_resp = msg.create_error_resp(task_result.error_str)
+ return error_resp
+
+ final_result = task_result.result_str
+
+ result_message = task_result.result.get("message")
+ if result_message:
+ inner_func_call_node = result_message.get("function_call")
+ if inner_func_call_node:
+ #TODO to save more token ,can i use msg_prompt?
+ call_prompt : AgentPrompt = copy.deepcopy(prompt)
+ final_result,error_code = await self._execute_func(inner_func_call_node,call_prompt,msg)
+ if error_code != 0:
+ error_resp = msg.create_error_resp(final_result)
+ return error_resp
+
+ llm_result : LLMResult = self._get_llm_result_type(final_result)
+ is_ignore = False
+ result_prompt_str = ""
+ match llm_result.state:
+ case "ignore":
+ is_ignore = True
+ case "waiting":
+ for sendmsg in llm_result.send_msgs:
+ target = sendmsg.target
+ sendmsg.topic = msg.topic
+ sendmsg.prev_msg_id = msg.get_msg_id()
+ send_resp = await AIBus.get_default_bus().send_message(sendmsg)
+ if send_resp is not None:
+ result_prompt_str += f"\n{target} response is :{send_resp.body}"
+ agent_sesion = AIChatSession.get_session(self.agent_id,f"{sendmsg.target}#{sendmsg.topic}",self.chat_db)
+ agent_sesion.append(sendmsg)
+ agent_sesion.append(send_resp)
+
+ final_result = llm_result.resp + result_prompt_str
+
+ if is_ignore is not True:
+ resp_msg = msg.create_group_resp_msg(self.agent_id,final_result)
+ chatsession.append(msg)
+ chatsession.append(resp_msg)
+
+ return resp_msg
+
+ return None
+
+ async def _process_msg(self,msg:AgentMsg) -> AgentMsg:
+ from .compute_kernel import ComputeKernel
+ from .bus import AIBus
+
+ if msg.msg_type == AgentMsgType.TYPE_GROUPMSG:
+ return await self._process_group_chat_msg(msg)
+
+ session_topic = msg.get_sender() + "#" + msg.topic
+ chatsession = AIChatSession.get_session(self.agent_id,session_topic,self.chat_db)
+
+
+ msg_prompt = AgentPrompt()
+ msg_prompt.messages = [{"role":"user","content":msg.body}]
+
+ prompt = AgentPrompt()
+ prompt.append(await self._get_agent_prompt())
+ self._format_msg_by_env_value(prompt)
+ prompt.append(self._get_remote_user_prompt(msg.sender))
+
+ inner_functions,function_token_len = self._get_inner_functions()
+
+ system_prompt_len = prompt.get_prompt_token_len()
+ input_len = len(msg.body)
+
+ history_prmpt,history_token_len = await self._get_prompt_from_session(chatsession,system_prompt_len + function_token_len,input_len)
+ prompt.append(history_prmpt) # chat context
+ prompt.append(msg_prompt)
+
+ logger.debug(f"Agent {self.agent_id} do llm token static system:{system_prompt_len},function:{function_token_len},history:{history_token_len},input:{input_len}, totoal prompt:{system_prompt_len + function_token_len + history_token_len} ")
+ task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,self.llm_model_name,self.max_token_size,inner_functions)
+ if task_result.result_code != ComputeTaskResultCode.OK:
+ logger.error(f"llm compute error:{task_result.error_str}")
+ error_resp = msg.create_error_resp(task_result.error_str)
+ return error_resp
+
+ final_result = task_result.result_str
+
+ result_message = task_result.result.get("message")
+ if result_message:
+ inner_func_call_node = result_message.get("function_call")
+ if inner_func_call_node:
+ #TODO to save more token ,can i use msg_prompt?
+ call_prompt : AgentPrompt = copy.deepcopy(prompt)
+ final_result,error_code = await self._execute_func(inner_func_call_node,call_prompt,msg)
+ if error_code != 0:
+ error_resp = msg.create_error_resp(final_result)
+ return error_resp
+
+ llm_result : LLMResult = self._get_llm_result_type(final_result)
+ is_ignore = False
+ result_prompt_str = ""
+ match llm_result.state:
+ case "ignore":
+ is_ignore = True
+ case "waiting":
+ for sendmsg in llm_result.send_msgs:
+ target = sendmsg.target
+ sendmsg.topic = msg.topic
+ sendmsg.prev_msg_id = msg.get_msg_id()
+ send_resp = await AIBus.get_default_bus().send_message(sendmsg)
+ if send_resp is not None:
+ result_prompt_str += f"\n{target} response is :{send_resp.body}"
+ agent_sesion = AIChatSession.get_session(self.agent_id,f"{sendmsg.target}#{sendmsg.topic}",self.chat_db)
+ agent_sesion.append(sendmsg)
+ agent_sesion.append(send_resp)
+
+ final_result = llm_result.resp + result_prompt_str
+
+ if is_ignore is not True:
+ resp_msg = msg.create_resp_msg(final_result)
+ chatsession.append(msg)
+ chatsession.append(resp_msg)
+
+ return resp_msg
+
+ return None
+
+ def get_id(self) -> str:
+ return self.agent_id
+
+ def get_fullname(self) -> str:
+ return self.fullname
+
+ def get_template_id(self) -> str:
+ return self.template_id
+
+ def get_llm_model_name(self) -> str:
+ return self.llm_model_name
+
+ def get_max_token_size(self) -> int:
+ return self.max_token_size
+
+ async def _get_history_prompt_for_think(self,chatsession:AIChatSession,summary:str,system_token_len:int,pos:int)->(AgentPrompt,int):
+ history_len = (self.max_token_size * 0.7) - system_token_len
+
+ messages = chatsession.read_history(self.history_len,pos,"natural") # read
+ result_token_len = 0
+ result_prompt = AgentPrompt()
+ have_summary = False
+ if summary is not None:
+ if len(summary) > 1:
+ have_summary = True
+
+ if have_summary:
+ result_prompt.messages.append({"role":"user","content":summary})
+ result_token_len -= len(summary)
+ else:
+ result_prompt.messages.append({"role":"user","content":"There is no summary yet."})
+ result_token_len -= 6
+
+ read_history_msg = 0
+ history_str : str = ""
+ for msg in messages:
+ read_history_msg += 1
+ dt = datetime.datetime.fromtimestamp(float(msg.create_time))
+ formatted_time = dt.strftime('%y-%m-%d %H:%M:%S')
+ record_str = f"{msg.sender},[{formatted_time}]\n{msg.body}\n"
+ history_str = history_str + record_str
+
+ history_len -= len(msg.body)
+ result_token_len += len(msg.body)
+ if history_len < 0:
+ logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.")
+ break
+
+ result_prompt.messages.append({"role":"user","content":history_str})
+ return result_prompt,pos+read_history_msg
+
+ async def _get_prompt_from_session_for_groupchat(self,chatsession:AIChatSession,system_token_len,input_token_len,is_groupchat=False):
+ history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len
+ messages = chatsession.read_history(self.history_len) # read
+ result_token_len = 0
+ result_prompt = AgentPrompt()
+ read_history_msg = 0
+ for msg in reversed(messages):
+ read_history_msg += 1
+ dt = datetime.datetime.fromtimestamp(float(msg.create_time))
+ formatted_time = dt.strftime('%y-%m-%d %H:%M:%S')
+
+ if msg.sender == self.agent_id:
+ if self.enable_timestamp:
+ result_prompt.messages.append({"role":"assistant","content":f"(create on {formatted_time}) {msg.body} "})
+ else:
+ result_prompt.messages.append({"role":"assistant","content":msg.body})
+
+ else:
+ if self.enable_timestamp:
+ result_prompt.messages.append({"role":"user","content":f"(create on {formatted_time}) {msg.body} "})
+ else:
+ result_prompt.messages.append({"role":"user","content":f"{msg.sender}:{msg.body}"})
+
+ history_len -= len(msg.body)
+ result_token_len += len(msg.body)
+ if history_len < 0:
+ logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.")
+ break
+
+ return result_prompt,result_token_len
+
+ async def _get_prompt_from_session(self,chatsession:AIChatSession,system_token_len,input_token_len) -> AgentPrompt:
+ # TODO: get prompt from group chat is different from single chat
+
+ history_len = (self.max_token_size * 0.7) - system_token_len - input_token_len
+ messages = chatsession.read_history(self.history_len) # read
+ result_token_len = 0
+ result_prompt = AgentPrompt()
+ read_history_msg = 0
+
+ if chatsession.summary is not None:
+ if len(chatsession.summary) > 1:
+ result_prompt.messages.append({"role":"user","content":chatsession.summary})
+ result_token_len -= len(chatsession.summary)
+
+ for msg in reversed(messages):
+ read_history_msg += 1
+ dt = datetime.datetime.fromtimestamp(float(msg.create_time))
+ formatted_time = dt.strftime('%y-%m-%d %H:%M:%S')
+
+ if msg.sender == self.agent_id:
+
+ if self.enable_timestamp:
+ result_prompt.messages.append({"role":"assistant","content":f"(create on {formatted_time}) {msg.body} "})
+ else:
+ result_prompt.messages.append({"role":"assistant","content":msg.body})
+
+ else:
+ if self.enable_timestamp:
+ result_prompt.messages.append({"role":"user","content":f"(create on {formatted_time}) {msg.body} "})
+ else:
+ result_prompt.messages.append({"role":"user","content":msg.body})
+
+ history_len -= len(msg.body)
+ result_token_len += len(msg.body)
+ if history_len < 0:
+ logger.warning(f"_get_prompt_from_session reach limit of token,just read {read_history_msg} history message.")
+ break
+
+ return result_prompt,result_token_len
+
diff --git a/src/aios_kernel/agent_message.py b/src/aios_kernel/agent_message.py
new file mode 100644
index 0000000..c5dc26d
--- /dev/null
+++ b/src/aios_kernel/agent_message.py
@@ -0,0 +1,169 @@
+from enum import Enum
+import uuid
+import time
+import re
+import shlex
+from typing import List
+from .ai_function import FunctionItem
+
+class AgentMsgType(Enum):
+ TYPE_MSG = 0
+ TYPE_GROUPMSG = 1
+ TYPE_INTERNAL_CALL = 10
+ TYPE_ACTION = 20
+ TYPE_EVENT = 30
+ TYPE_SYSTEM = 40
+
+
+class AgentMsgStatus(Enum):
+ RESPONSED = 0
+ INIT = 1
+ SENDING = 2
+ PROCESSING = 3
+ ERROR = 4
+ RECVED = 5
+ EXECUTED = 6
+
+# msg is a msg / msg resp
+# msg body可以有内容类型(MIME标签),text, image, voice, video, file,以及富文本(html)
+# msg is a inner function call with result
+# msg is a Action with result
+
+# qutoe Msg
+# forword msg
+# reply msg
+
+# 逻辑上的同一个Message在同一个session中看到的msgid相同
+# 在不同的session中看到的msgid不同
+
+class AgentMsg:
+ def __init__(self,msg_type=AgentMsgType.TYPE_MSG) -> None:
+ self.msg_id = "msg#" + uuid.uuid4().hex
+ self.msg_type:AgentMsgType = msg_type
+
+ self.prev_msg_id:str = None
+ self.quote_msg_id:str = None
+ self.rely_msg_id:str = None # if not none means this is a respone msg
+ self.session_id:str = None
+
+ #forword info
+
+
+ self.create_time = 0
+ self.done_time = 0
+ self.topic:str = None # topic is use to find session, not store in db
+
+ self.sender:str = None # obj_id.sub_objid@tunnel_id
+ self.target:str = None
+ self.mentions:[] = None #use in group chat only
+ #self.title:str = None
+ self.body:str = None
+ self.body_mime:str = None #//default is "text/plain",encode is utf8
+
+ #type is call / action
+ self.func_name = None
+ self.args = None
+ self.result_str = None
+
+ #type is event
+ self.event_name = None
+ self.event_args = None
+
+ self.status = AgentMsgStatus.INIT
+ self.inner_call_chain = []
+ self.resp_msg = None
+
+ @classmethod
+ def create_internal_call_msg(self,func_name:str,args:dict,prev_msg_id:str,caller:str):
+ msg = AgentMsg(AgentMsgType.TYPE_INTERNAL_CALL)
+ msg.create_time = time.time()
+ msg.func_name = func_name
+ msg.args = args
+ msg.prev_msg_id = prev_msg_id
+ msg.sender = caller
+ return msg
+
+ def create_action_msg(self,action_name:str,args:dict,caller:str):
+ msg = AgentMsg(AgentMsgType.TYPE_ACTION)
+ msg.create_time = time.time()
+ msg.func_name = action_name
+ msg.args = args
+ msg.prev_msg_id = self.msg_id
+ msg.topic = self.topic
+ msg.sender = caller
+ return msg
+
+ def create_error_resp(self,error_msg:str):
+ resp_msg = AgentMsg(AgentMsgType.TYPE_SYSTEM)
+ resp_msg.create_time = time.time()
+
+ resp_msg.rely_msg_id = self.msg_id
+ resp_msg.body = error_msg
+ resp_msg.topic = self.topic
+ resp_msg.sender = self.target
+ resp_msg.target = self.sender
+
+ return resp_msg
+
+ def create_resp_msg(self,resp_body):
+ resp_msg = AgentMsg()
+ resp_msg.create_time = time.time()
+
+ resp_msg.rely_msg_id = self.msg_id
+ resp_msg.sender = self.target
+ resp_msg.target = self.sender
+ resp_msg.body = resp_body
+ resp_msg.topic = self.topic
+
+ return resp_msg
+
+ def create_group_resp_msg(self,sender_id,resp_body):
+ resp_msg = AgentMsg(AgentMsgType.TYPE_GROUPMSG)
+ resp_msg.create_time = time.time()
+
+ resp_msg.rely_msg_id = self.msg_id
+ resp_msg.target = self.target
+ resp_msg.sender = sender_id
+ resp_msg.body = resp_body
+ resp_msg.topic = self.topic
+
+ return resp_msg
+
+ def set(self,sender:str,target:str,body:str,topic:str=None) -> None:
+ self.sender = sender
+ self.target = target
+ self.body = body
+ self.create_time = time.time()
+ if topic:
+ self.topic = topic
+
+ def get_msg_id(self) -> str:
+ return self.msg_id
+
+ def get_sender(self) -> str:
+ return self.sender
+
+ def get_target(self) -> str:
+ return self.target
+
+ def get_prev_msg_id(self) -> str:
+ return self.prev_msg_id
+
+ def get_quote_msg_id(self) -> str:
+ return self.quote_msg_id
+
+ @classmethod
+ def parse_function_call(cls,func_string:str):
+ str_list = shlex.split(func_string)
+ func_name = str_list[0]
+ params = str_list[1:]
+ return func_name, params
+
+class LLMResult:
+ def __init__(self) -> None:
+ self.state : str = "ignore"
+ self.resp : str = ""
+ self.post_msgs : List[AgentMsg] = []
+ self.send_msgs : List[AgentMsg] = []
+ self.calls : List[FunctionItem] = []
+ self.post_calls : List[FunctionItem] = []
\ No newline at end of file
diff --git a/src/aios_kernel/ai_function.py b/src/aios_kernel/ai_function.py
new file mode 100644
index 0000000..bca41d2
--- /dev/null
+++ b/src/aios_kernel/ai_function.py
@@ -0,0 +1,144 @@
+from abc import ABC, abstractmethod
+from typing import Dict,Coroutine,Callable
+
+class ParameterDefine:
+ def __init__(self) -> None:
+ self.name = None
+ self.type = None
+ self.description = None
+
+
+class AIFunction:
+ def __init__(self) -> None:
+ self.description : str = None
+
+ @abstractmethod
+ def get_name(self) -> str:
+ """
+ return the name of the function (should be snake case)
+ """
+ pass
+
+ @abstractmethod
+ def get_description(self) -> str:
+ """
+ return a detailed description of what the function does
+ """
+ return self.description
+
+ @abstractmethod
+ def get_parameters(self) -> Dict:
+ """
+ Return the list of parameters to execute this function in the form of
+ JSON schema as specified in the OpenAI documentation:
+ https://platform.openai.com/docs/api-reference/chat/create#chat/create-parameters
+
+ str = run_code(code:str)
+ parameters = {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string",
+ "description": "Python code which needs to be executed"
+ }
+ }
+ }
+
+ """
+ pass
+
+ @abstractmethod
+ async def execute(self, **kwargs) -> str:
+ """
+ Execute the function and return a JSON serializable dict.
+ The parameters are passed in the form of kwargs
+ """
+ pass
+
+ @abstractmethod
+ def is_local(self) -> bool:
+ """
+ is this function call need network?
+ """
+ pass
+
+ @abstractmethod
+ def is_in_zone(self) -> bool:
+ """
+ is this function call in Lan?
+ """
+ pass
+
+ @abstractmethod
+ def is_ready_only(self) -> bool:
+ pass
+
+ #def load_from_config(self,config:dict) -> bool:
+ # pass
+
+class FunctionItem:
+ def __init__(self,name,args) -> None:
+ self.name = name
+ self.args = args
+ self.body = None
+
+ def append_body(self,body:str) -> None:
+ if self.body is None:
+ self.body = body
+ else:
+ self.body += body
+
+ def dumps(self) -> str:
+ pass
+
+# call chain is a combination of ai_function,group of ai_function.
+class CallChain:
+ def __init__(self) -> None:
+ pass
+
+ def load_from_config(self,config:dict) -> bool:
+ pass
+
+ async def execute(self):
+ pass
+
+class SimpleAIFunction(AIFunction):
+ def __init__(self,func_id:str,description:str,func_handler:Coroutine,parameters:Dict = None) -> None:
+ self.func_id = func_id
+ self.description = description
+ self.func_handler = func_handler
+ self.parameters = parameters
+
+ def get_name(self) -> str:
+ return self.func_id
+
+ def get_parameters(self) -> Dict:
+ if self.parameters is not None:
+ result = {}
+ result["type"] = "object"
+ parm_defines = {}
+ for parm,desc in self.parameters.items():
+ parm_item = {}
+ parm_item["type"] = "string"
+ parm_item["description"] = desc
+ parm_defines[parm] = parm_item
+ result["properties"] = parm_defines
+ return result
+ return {"type": "object", "properties": {}}
+
+
+ async def execute(self,**kwargs) -> str:
+ if self.func_handler is None:
+ return "error: function not implemented"
+
+ return await self.func_handler(**kwargs)
+
+ def is_local(self) -> bool:
+ return True
+
+ def is_in_zone(self) -> bool:
+ return True
+
+ def is_ready_only(self) -> bool:
+ return False
+
diff --git a/src/aios_kernel/bus.py b/src/aios_kernel/bus.py
new file mode 100644
index 0000000..2731702
--- /dev/null
+++ b/src/aios_kernel/bus.py
@@ -0,0 +1,145 @@
+from typing import Coroutine,Dict,Any
+from .agent_message import AgentMsg,AgentMsgStatus,AgentMsgType
+import asyncio
+from asyncio import Queue
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+class AIBusHandler:
+ def __init__(self,handler:Coroutine,owner_bus,enable_defualt_proc=True) -> None:
+ self.handler = handler
+ self.working_task = None
+ self.results = {} # recv resps
+ self.queue:Queue = Queue()
+ self.enable_defualt_proc = enable_defualt_proc
+ self.owner_bus = owner_bus
+
+ async def handle_message(self,msg:AgentMsg) -> Any:
+ if self.handler is None:
+ return None
+
+ resp_msg = await self.handler(msg)
+ if self.enable_defualt_proc:
+ if resp_msg is not None:
+ if resp_msg.msg_type == AgentMsgType.TYPE_GROUPMSG:
+ await self.owner_bus.post_message(resp_msg,resp_msg.target)
+ else:
+ await self.owner_bus.post_message(resp_msg)
+
+ return resp_msg
+
+class AIBus:
+ _instance = None
+ @classmethod
+ def get_default_bus(cls):
+ if cls._instance is None:
+ cls._instance = AIBus()
+ return cls._instance
+
+ def __init__(self) -> None:
+ self.handlers:Dict[AIBusHandler] = {}
+ self.unhandle_handler:Coroutine = None
+
+
+ async def post_message(self,msg:AgentMsg,target_id = None,use_unhandle=True) -> bool:
+ if target_id is None:
+ target_id =msg.target
+
+ target_id = target_id.split(".")[0]
+
+ handler = self.handlers.get(target_id)
+ if handler:
+ if msg.rely_msg_id is not None:
+ handler.results[msg.rely_msg_id] = msg
+ return None
+
+ handler.queue.put_nowait(msg)
+ self.start_process(target_id)
+ return True
+
+ if use_unhandle:
+ if self.unhandle_handler is not None:
+ if await self.unhandle_handler(self,target_id):
+ return await self.post_message(msg,target_id,False)
+
+ logger.warn(f"post message to {msg.target} failed!,target not found")
+ return False
+
+ async def resp_message(self,org_msg_id:str,resp:AgentMsg) -> None:
+ assert resp.rely_msg_id == org_msg_id
+ return await self.post_message(resp)
+
+ async def send_message(self,msg:AgentMsg,target_id = None, real_sender=None) -> AgentMsg:
+ if real_sender is None:
+ sender_id = msg.sender.split(".")[0]
+ else:
+ sender_id = real_sender.split(".")[0]
+
+ sender_handler = self.handlers.get(sender_id) # sender already register on bus
+ if sender_handler is None:
+ logger.warn(f"sender {sender_id} not register on AI_BUS!")
+ return None
+
+ post_result = await self.post_message(msg,target_id)
+ if post_result is False:
+ return None
+
+ retry_times = 0
+ while True:
+ resp : AgentMsg = sender_handler.results.get(msg.msg_id)
+ if resp is not None:
+ msg.resp_msg = resp
+ msg.status = AgentMsgStatus.RESPONSED
+ del sender_handler.results[msg.msg_id]
+ return resp
+
+ await asyncio.sleep(0.2)
+ retry_times += 1
+ if retry_times > 5*240: # default timeout is 240 sec
+ msg.status = AgentMsgStatus.ERROR
+ return None
+
+ return None
+
+ def register_unhandle_message_handler(self,handler:Any) -> Queue:
+ self.unhandle_handler = handler
+
+ # means sub
+ def register_message_handler(self,handler_name:str,handler:Any) -> Queue:
+ handler_node = AIBusHandler(handler,self)
+ self.handlers[handler_name] = handler_node
+ return handler_node.queue
+
+ async def process_queue(self, handler:AIBusHandler):
+ while True:
+ # Wait for a message
+ message = await handler.queue.get()
+
+ try:
+ # Try to handle the message
+ await handler.handle_message(message)
+ except Exception as e:
+ # If an error occurs, put the message back into the queue
+ logger.error(f"handle message {message.msg_id} failed! {e}")
+ logger.exception(e)
+ raise e
+ #self.queues[name].put_nowait(message)
+
+ return
+
+ def start_process(self,target_name):
+ handler = self.handlers.get(target_name)
+ if handler is None:
+ logger.error(f"handler {target_name} not found!")
+ return
+
+ if handler.handler is None:
+ return
+
+ if handler.working_task is not None:
+ logger.warn(f"handler {target_name} is already working!")
+ return
+
+ handler.working_task = asyncio.create_task(self.process_queue(handler))
diff --git a/src/aios_kernel/chatsession.py b/src/aios_kernel/chatsession.py
new file mode 100644
index 0000000..1290d80
--- /dev/null
+++ b/src/aios_kernel/chatsession.py
@@ -0,0 +1,383 @@
+
+import sqlite3 # Because sqlite3 IO operation is small, so we can use sqlite3 directly.(so we don't need to use async sqlite3 now)
+from sqlite3 import Error
+import logging
+import threading
+import datetime
+import uuid
+import json
+
+from .agent_message import AgentMsgType, AgentMsg, AgentMsgStatus
+
+class ChatSessionDB:
+ def __init__(self, db_file):
+ """ initialize db connection """
+ self.local = threading.local()
+ self.db_file = db_file
+ self._get_conn()
+
+ def _get_conn(self):
+ """ get db connection """
+ if not hasattr(self.local, 'conn'):
+ self.local.conn = self._create_connection(self.db_file)
+ return self.local.conn
+
+ def _create_connection(self, db_file):
+ """ create a database connection to a SQLite database """
+ conn = None
+ try:
+ conn = sqlite3.connect(db_file)
+ except Error as e:
+ logging.error("Error occurred while connecting to database: %s", e)
+ return None
+
+ if conn:
+ self._create_table(conn)
+
+ return conn
+
+ def close(self):
+ if not hasattr(self.local, 'conn'):
+ return
+ self.local.conn.close()
+
+ def _create_table(self, conn):
+ """ create table """
+ try:
+ # create sessions table
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS ChatSessions (
+ SessionID TEXT PRIMARY KEY,
+ SessionOwner TEXT,
+ SessionTopic TEXT,
+ StartTime TEXT,
+ SummarizePos INTEGER,
+ Summary TEXT
+ );
+ """)
+
+ # create messages table
+ # reciver_id could be None
+
+ conn.execute("""
+ CREATE TABLE IF NOT EXISTS Messages (
+ MessageID TEXT PRIMARY KEY,
+ SessionID TEXT,
+ MsgType INTEGER,
+ PrevMsgID TEXT,
+ QuoteMsgID TEXT,
+ RelyMsgID TEXT,
+
+ SenderID TEXT,
+ ReceiverID TEXT,
+ Timestamp TEXT,
+
+ Topic TEXT,
+ Mentions TEXT,
+ ContentMIME TEXT,
+ Content TEXT,
+
+ ActionName TEXT,
+ ActionParams TEXT,
+ ActionResult TEXT,
+ DoneTime TEXT,
+
+ Status INTEGER
+ );
+ """)
+ conn.commit()
+ except Error as e:
+ logging.error("Error occurred while creating tables: %s", e)
+
+ def insert_chatsession(self, session_id, session_owner,session_topic, start_time):
+ """ insert a new session into the ChatSessions table """
+ try:
+ conn = self._get_conn()
+ conn.execute("""
+ INSERT INTO ChatSessions (SessionID, SessionOwner,SessionTopic, StartTime,SummarizePos,Summary)
+ VALUES (?,?, ?, ?,0,"")
+ """, (session_id, session_owner,session_topic, start_time))
+ conn.commit()
+ return 0 # return 0 if successful
+ except Error as e:
+ logging.error("Error occurred while inserting session: %s", e)
+ return -1 # return -1 if an error occurs
+
+ def insert_message(self, msg:AgentMsg):
+ """ insert a new message into the Messages table """
+ try:
+ action_name = None
+ action_params = None
+ action_result = None
+ mentions = None
+ if msg.mentions:
+ mentions = json.dumps(msg.mentions)
+
+ match msg.msg_type:
+ case AgentMsgType.TYPE_MSG:
+ pass
+ case AgentMsgType.TYPE_ACTION:
+ action_name = msg.func_name
+ action_params = json.dumps(msg.args)
+ action_result = msg.result_str
+ case AgentMsgType.TYPE_INTERNAL_CALL:
+ action_name = msg.func_name
+ action_params = json.dumps(msg.args)
+ action_result = msg.result_str
+ case AgentMsgType.TYPE_EVENT:
+ action_name = msg.event_name
+ action_params = json.dumps(msg.event_args)
+
+
+ conn = self._get_conn()
+ conn.execute("""
+ INSERT INTO Messages (MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status)
+ VALUES (?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, (msg.msg_id, msg.session_id, msg.msg_type.value, msg.prev_msg_id, msg.sender, msg.target, msg.create_time, msg.topic,mentions,msg.body_mime,msg.body,action_name,action_params,action_result,msg.done_time,msg.status.value))
+ conn.commit()
+
+ if msg.inner_call_chain:
+ for inner_call in msg.inner_call_chain:
+ self.insert_message(inner_call)
+
+ return 0 # return 0 if successful
+ except Error as e:
+ logging.error("Error occurred while inserting message: %s", e)
+ return -1 # return -1 if an error occurs
+
+ def get_chatsession_by_id(self, session_id):
+ """Get a message by its ID"""
+ conn = self._get_conn()
+ c = conn.cursor()
+ c.execute("SELECT * FROM ChatSessions WHERE SessionID = ?", (session_id,))
+ chatsession = c.fetchone()
+ return chatsession
+
+ def get_chatsession_by_owner_topic(self, owner_id, topic):
+ """Get a chatsession by its owner and topic"""
+ conn = self._get_conn()
+ c = conn.cursor()
+ c.execute("SELECT * FROM ChatSessions WHERE SessionOwner = ? AND SessionTopic = ?", (owner_id,topic))
+ chatsession = c.fetchone()
+ return chatsession
+
+ def list_chatsessions(self, owner_id, limit, offset):
+ """ retrieve sessions with pagination """
+ try:
+ conn = self._get_conn()
+ cursor = conn.cursor()
+ cursor.execute("""
+ SELECT SessionID FROM ChatSessions
+ WHERE SessionOwner = ?
+ ORDER BY StartTime DESC
+ LIMIT ? OFFSET ?
+ """, (owner_id,limit, offset))
+ results = cursor.fetchall()
+ #self.close()
+ return results # return 0 and the result if successful
+ except Error as e:
+ logging.error("Error occurred while getting sessions: %s", e)
+ return -1, None # return -1 and None if an error occurs
+
+ def get_message_by_id(self, message_id):
+ """Get a message by its ID"""
+ conn =self._get_conn()
+ c = conn.cursor()
+ c.execute("SELECT MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status FROM Messages WHERE MessageID = ?", (message_id,))
+ message = c.fetchone()
+ return message
+
+ # read message from begin->now
+ def read_message(self,session_id,limit,offset):
+ try:
+ conn = self._get_conn()
+ cursor = conn.cursor()
+ cursor.execute("""
+ SELECT MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status FROM Messages
+ WHERE SessionID = ?
+ ORDER BY Timestamp
+ LIMIT ? OFFSET ?
+ """, (session_id, limit, offset))
+ results = cursor.fetchall()
+ #self.close()
+ return results # return 0 and the result if successful
+ except Error as e:
+ logging.error("Error occurred while getting messages: %s", e)
+ return -1, None # return -1 and None if an error occurs
+
+ # read message from now->beign
+ def get_messages(self, session_id, limit, offset):
+ """ retrieve messages of a session with pagination """
+ try:
+ conn = self._get_conn()
+ cursor = conn.cursor()
+ cursor.execute("""
+ SELECT MessageID, SessionID, MsgType, PrevMsgID, SenderID, ReceiverID, Timestamp, Topic,Mentions,ContentMIME,Content,ActionName,ActionParams,ActionResult,DoneTime,Status FROM Messages
+ WHERE SessionID = ?
+ ORDER BY Timestamp DESC
+ LIMIT ? OFFSET ?
+ """, (session_id, limit, offset))
+ results = cursor.fetchall()
+ #self.close()
+ return results # return 0 and the result if successful
+ except Error as e:
+ logging.error("Error occurred while getting messages: %s", e)
+ return -1, None # return -1 and None if an error occurs
+
+ def update_message_status(self, message_id, status):
+ """ update the status of a message """
+ try:
+ conn = self._get_conn()
+ conn.execute("""
+ UPDATE Messages
+ SET Status = ?
+ WHERE MessageID = ?
+ """, (status, message_id))
+ conn.commit()
+ return 0 # return 0 if successful
+ except Error as e:
+ logging.error("Error occurred while updating message status: %s", e)
+ return -1 # return -1 if an error occurs
+
+ def update_session_summary(self, session_id, summarize_pos, summary):
+ """ update the summary of a session """
+ try:
+ conn = self._get_conn()
+ conn.execute("""
+ UPDATE ChatSessions
+ SET SummarizePos = ?, Summary = ?
+ WHERE SessionID = ?
+ """, (summarize_pos, summary, session_id))
+ conn.commit()
+ return 0 # return 0 if successful
+ except Error as e:
+ logging.error("Error occurred while updating session summary: %s", e)
+ return -1
+
+# chat session store the chat history between owner and agent
+# chat session might be large, so can read / write at stream mode.
+class AIChatSession:
+ _dbs = {}
+ #@classmethod
+ #async def get_session_by_id(cls,session_id:str,db_path:str):
+ # db = cls._dbs.get(db_path)
+ # if db is None:
+ # db = ChatSessionDB(db_path)
+ # cls._dbs[db_path] = db
+ # db.get_chatsession_by_id(session_id)
+ # #result = AIChatSession()
+
+ @classmethod
+ def get_session(cls,owner_id:str,session_topic:str,db_path:str,auto_create = True) -> 'AIChatSession':
+ db = cls._dbs.get(db_path)
+ if db is None:
+ db = ChatSessionDB(db_path)
+ cls._dbs[db_path] = db
+
+ result = None
+ session = db.get_chatsession_by_owner_topic(owner_id,session_topic)
+ if session is None:
+ if auto_create:
+ session_id = "CS#" + uuid.uuid4().hex
+ db.insert_chatsession(session_id,owner_id,session_topic,datetime.datetime.now())
+ result = AIChatSession(owner_id,session_id,db)
+ else:
+ result = AIChatSession(owner_id,session[0],db)
+ result.topic = session_topic
+ result.summarize_pos = session[4]
+ result.summary = session[5]
+
+ return result
+
+ @classmethod
+ def get_session_by_id(cls,session_id:str,db_path:str)->'AIChatSession':
+ db = cls._dbs.get(db_path)
+ if db is None:
+ db = ChatSessionDB(db_path)
+ cls._dbs[db_path] = db
+
+ result = None
+ session = db.get_chatsession_by_id(session_id)
+ if session is None:
+ return None
+ else:
+ result = AIChatSession(session[1],session[0],db)
+ result.topic = session[2]
+ result.summarize_pos = session[4]
+ result.summary = session[5]
+
+ return result
+
+ @classmethod
+ def list_session(cls,owner_id:str,db_path:str) -> list[str]:
+ db = cls._dbs.get(db_path)
+ if db is None:
+ db = ChatSessionDB(db_path)
+ cls._dbs[db_path] = db
+
+ result = db.list_chatsessions(owner_id,16,0)
+ result_ids = []
+ for r in result:
+ result_ids.append(r[0])
+ return result_ids
+
+
+ def __init__(self,owner_id:str, session_id:str, db:ChatSessionDB) -> None:
+ self.owner_id :str = owner_id
+ self.session_id : str = session_id
+ self.db : ChatSessionDB = db
+
+ self.topic : str = None
+ self.start_time : str = None
+ self.summarize_pos : int = 0
+ self.summary = None
+
+ def get_owner_id(self) -> str:
+ return self.owner_id
+
+ def read_history(self, number:int=10,offset=0,order="revers") -> [AgentMsg]:
+ if order == "revers":
+ msgs = self.db.get_messages(self.session_id, number, offset)
+ else:
+ msgs = self.db.read_message(self.session_id, number, offset)
+
+ result = []
+ for msg in msgs:
+ agent_msg = AgentMsg()
+ agent_msg.msg_id = msg[0]
+ agent_msg.session_id = msg[1]
+ agent_msg.msg_type = AgentMsgType(msg[2])
+ agent_msg.prev_msg_id = msg[3]
+ agent_msg.sender = msg[4]
+ agent_msg.target = msg[5]
+ agent_msg.create_time = msg[6]
+ agent_msg.topic = msg[7]
+ if msg[8] is not None:
+ agent_msg.mentions = json.loads(msg[8])
+ agent_msg.body_mime = msg[9]
+ agent_msg.body = msg[10]
+ agent_msg.func_name = msg[11]
+ if msg[12] is not None:
+ agent_msg.args = json.loads(msg[12])
+ agent_msg.result_str = msg[13]
+ agent_msg.done_time = msg[14]
+ agent_msg.status = AgentMsgStatus(msg[15])
+
+ result.append(agent_msg)
+ return result
+
+ def append(self,msg:AgentMsg) -> None:
+ msg.session_id = self.session_id
+ self.db.insert_message(msg)
+
+
+ def update_think_progress(self,progress:int,new_summary:str) -> None:
+ self.db.update_session_summary(self.session_id,progress,new_summary)
+ self.summarize_pos = progress
+ self.summary = new_summary
+
+ #def attach_event_handler(self,handler) -> None:
+ # """chat session changed event handler"""
+ # pass
+
+ #TODO : add iterator interface for read chat history
diff --git a/src/aios_kernel/compute_kernel.py b/src/aios_kernel/compute_kernel.py
new file mode 100644
index 0000000..3f97708
--- /dev/null
+++ b/src/aios_kernel/compute_kernel.py
@@ -0,0 +1,216 @@
+from abc import ABC, abstractmethod
+import random
+from typing import Optional
+import logging
+import asyncio
+from asyncio import Queue
+
+from knowledge import ObjectID
+from .agent import AgentPrompt
+from .compute_node import ComputeNode
+from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskResult, ComputeTaskType,ComputeTaskResultCode
+
+logger = logging.getLogger(__name__)
+
+# How to dispatch different computing tasks (some tasks may contain a large amount of state for correct execution)
+# to suitable computing nodes, achieving a balance of speed, cost, and power consumption,
+# is the CORE GOAL of the entire computing task schedule system (aios_kernel).
+
+
+class ComputeKernel:
+ _instance = None
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = ComputeKernel()
+ return cls._instance
+
+ def __init__(self) -> None:
+ self.is_start = False
+ self.task_queue = Queue()
+ self.is_start = False
+ self.compute_nodes = {}
+
+ def run(self, task: ComputeTask) -> None:
+ # check there is compute node can support this task
+ if self.is_task_support(task) is False:
+ logger.error(
+ f"task {task.display()} is not support by any compute node")
+ return
+ # add task to working_queue
+ self.task_queue.put_nowait(task)
+
+ async def start(self):
+ if self.is_start is True:
+ logger.warn("compute_kernel is already start")
+ return
+
+ self.is_start = True
+
+ async def _run_task_loop():
+ while True:
+ task = await self.task_queue.get()
+ logger.info(f"compute_kernel get task: {task.display()}")
+ c_node: ComputeNode = self._schedule(task)
+ if c_node:
+ await c_node.push_task(task)
+
+ logger.warn("compute_kernel is stoped!")
+
+ asyncio.create_task(_run_task_loop())
+
+ def _schedule(self, task) -> ComputeNode:
+ # find all the node which supports this task
+ support_nodes = []
+ total_weights = 0
+
+ for node in self.compute_nodes.values():
+ if node.is_support(task) is True:
+ support_nodes.append({
+ "pos": total_weights,
+ "node": node
+ })
+ total_weights += node.weight()
+
+ if len(support_nodes) < 1:
+ logger.warning(f"task {task.display()} is not support by any compute node")
+ return None
+
+ # hit a random node with weight
+ hit_pos = random.randint(0, total_weights - 1)
+ for i in range(min(len(support_nodes) - 1, hit_pos), -1, -1):
+ if support_nodes[i]["pos"] <= hit_pos:
+ return support_nodes[i]["node"]
+
+ logger.warning(
+ f"task {task.display()} is not support by any compute node")
+ return None
+
+ def add_compute_node(self, node: ComputeNode):
+ if self.compute_nodes.get(node.node_id) is not None:
+ logger.warn(
+ f"compute_node {node.display()} already in compute_kernel")
+ return
+ self.compute_nodes[node.node_id] = node
+ logger.info(f"add compute_node {node.display()} to compute_kernel")
+
+ def disable_compute_node(self, node_id: str):
+ node = self.compute_nodes.get(node_id)
+ if node is None:
+ logger.warn(f"compute_node {node_id} not in compute_kernel")
+ return
+ node.enable = False
+
+ def is_task_support(self, task: ComputeTask) -> bool:
+ return True
+
+ # friendly interface for use:
+ def llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0,inner_functions = None):
+ # craete a llm_work_task ,push on queue's end
+ # then task_schedule would run this task.(might schedule some work_task to another host)
+ task_req = ComputeTask()
+ task_req.set_llm_params(prompt, mode_name, max_token,inner_functions)
+ self.run(task_req)
+ return task_req
+
+ async def _wait_task(self,task_req:ComputeTask)->ComputeTaskResult:
+ async def check_timer():
+ check_times = 0
+ while True:
+ if task_req.state == ComputeTaskState.DONE:
+ break
+
+ if task_req.state == ComputeTaskState.ERROR:
+ break
+
+ if check_times >= 120:
+ task_req.state = ComputeTaskState.ERROR
+ break
+
+ await asyncio.sleep(0.5)
+ check_times += 1
+
+ await asyncio.create_task(check_timer())
+ if task_req.result:
+ return task_req.result
+ else:
+ time_out_result = ComputeTaskResult()
+ time_out_result.result_code = ComputeTaskResultCode.TIMEOUT
+ time_out_result.set_from_task(task_req)
+ ## craete timeout task_result
+
+ async def do_llm_completion(self, prompt: AgentPrompt, mode_name: Optional[str] = None, max_token: int = 0, inner_functions = None) -> str:
+ task_req = self.llm_completion(prompt, mode_name, max_token,inner_functions)
+ return await self._wait_task(task_req)
+
+
+ def text_embedding(self,input:str,model_name:Optional[str] = None):
+ task_req = ComputeTask()
+ task_req.set_text_embedding_params(input,model_name)
+ self.run(task_req)
+ return task_req
+
+ async def do_text_embedding(self,input:str,model_name:Optional[str] = None) -> [float]:
+ task_req = self.text_embedding(input,model_name)
+ task_result = await self._wait_task(task_req)
+
+ if task_req.state == ComputeTaskState.DONE:
+ return task_result.result.get("content")
+ else:
+ logging.warning(f"do_text_embedding error: {task_req.error_str},input: {input}")
+ return None
+
+ def image_embedding(self,input:ObjectID,model_name:Optional[str] = None):
+ task_req = ComputeTask()
+ task_req.set_image_embedding_params(input,model_name)
+ self.run(task_req)
+ return task_req
+
+ async def do_image_embedding(self,input:ObjectID,model_name:Optional[str] = None) -> [float]:
+ task_req = self.image_embedding(input,model_name)
+ task_result = await self._wait_task(task_req)
+
+ if task_req.state == ComputeTaskState.DONE:
+ return task_result.result.get("content")
+
+ return None
+
+ async def do_text_to_speech(self,
+ input:str,
+ language_code:Optional[str] = None,
+ gender: Optional[str] = None,
+ age: Optional[str] = None,
+ voice_name: Optional[str] = None,
+ tone: Optional[str] = None):
+ task_req = ComputeTask()
+ task_req.params["text"] = input
+ task_req.params["language_code"] = language_code
+ task_req.params["gender"] = gender
+ task_req.params["age"] = age
+ task_req.params["voice_name"] = voice_name
+ task_req.params["tone"] = tone
+ task_req.task_type = ComputeTaskType.TEXT_2_VOICE
+ self.run(task_req)
+
+ task_result = await self._wait_task(task_req)
+
+ if task_req.state == ComputeTaskState.DONE:
+ return task_result.result
+
+
+ def text_2_image(self, prompt:str, model_name:Optional[str] = None, negative_prompt = None):
+ task = ComputeTask()
+ task.set_text_2_image_params(prompt,model_name, negative_prompt)
+ self.run(task)
+ return task
+
+ async def do_text_2_image(self, prompt:str, model_name:Optional[str] = None, negative_prompt = None) -> ComputeTaskResult:
+ task = self.text_2_image(prompt,model_name, negative_prompt)
+ task = await self._wait_task(task)
+
+ return task.result
+ # if task_req.state == ComputeTaskState.DONE:
+ # return None, task_result
+
+ # return task_req.error_str, None
+
diff --git a/src/aios_kernel/compute_node.py b/src/aios_kernel/compute_node.py
new file mode 100644
index 0000000..7ce7292
--- /dev/null
+++ b/src/aios_kernel/compute_node.py
@@ -0,0 +1,53 @@
+from abc import ABC, abstractmethod
+from .compute_task import ComputeTask, ComputeTaskType
+
+
+class ComputeNode(ABC):
+ def __init__(self) -> None:
+ self.node_id = "default"
+ self.enable = True
+
+ @abstractmethod
+ async def push_task(self, task: ComputeTask, proiority: int = 0):
+ pass
+
+ @abstractmethod
+ async def remove_task(self, task_id: str):
+ pass
+
+ @abstractmethod
+ def get_task_state(self, task_id: str):
+ pass
+
+ @abstractmethod
+ def display(self) -> str:
+ pass
+
+ @abstractmethod
+ def get_capacity(self):
+ pass
+
+ @abstractmethod
+ def is_support(self, task: ComputeTask) -> bool:
+ pass
+
+ @abstractmethod
+ def is_local(self) -> bool:
+ pass
+
+ # the hit weight when select this node in schedule
+ def weight(self) -> int:
+ return 1
+
+ def is_trusted(self) -> bool:
+ return True
+
+ def get_fee_type(self) -> str:
+ return "free"
+
+class LocalComputeNode(ComputeNode):
+ def display(self) -> str:
+ return super().display()
+
+ def is_local(self) -> bool:
+ return True
\ No newline at end of file
diff --git a/src/aios_kernel/compute_node_config.py b/src/aios_kernel/compute_node_config.py
new file mode 100644
index 0000000..8e25f50
--- /dev/null
+++ b/src/aios_kernel/compute_node_config.py
@@ -0,0 +1,87 @@
+"""
+Configuration for nodes:
+
+```
+├── nodes
+│ └── llama
+| └── 0
+| | └── url
+| | └── model_name
+| └── 1
+| └── url
+| └── model_name
+```
+"""
+import logging
+from typing import List
+
+import os
+import toml
+
+from .local_llama_compute_node import LocalLlama_ComputeNode
+from .storage import AIStorage
+
+# define singleton class knowledge pipline
+class ComputeNodeConfig:
+ _instance = None
+
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = ComputeNodeConfig()
+ cls._instance.__singleton_init__()
+
+ return cls._instance
+
+ def initial(self) -> List[LocalLlama_ComputeNode]:
+ config_path = self.__config_path()
+ logging.info(f"initial nodes from {config_path}")
+
+ if os.path.exists(config_path):
+ self.config = toml.load(self.__config_path())
+ if self.config is None:
+ return []
+
+ nodes = []
+ llama_nodes_cfg = self.config["llama"]
+ if llama_nodes_cfg is not None:
+ for cfg in llama_nodes_cfg:
+ node = LocalLlama_ComputeNode(url=cfg["url"], model_name=cfg["model_name"])
+ nodes.append(node)
+
+ return nodes
+
+ return []
+
+ def save(self):
+ with open(self.__config_path(), "w") as f:
+ toml.dump(self.config, f)
+
+ def add_node(self, model_type: str, url: str, model_name: str):
+ if model_type == "llama":
+ llama_nodes_cfg = self.config.get("llama") or []
+ for cfg in llama_nodes_cfg:
+ if url == cfg["url"] and model_name == cfg["model_name"]:
+ return
+ llama_nodes_cfg.append({"url": url, "model_name": model_name})
+ self.config["llama"] = llama_nodes_cfg
+
+
+ def remove_node(self, model_type: str, url: str, model_name: str):
+ if model_type == "llama":
+ llama_nodes_cfg = self.config.get("llama") or []
+ for i in range(0, len(llama_nodes_cfg)):
+ cfg = llama_nodes_cfg[i]
+ if url == cfg["url"] and model_name == cfg["model_name"]:
+ llama_nodes_cfg.pop(i)
+
+ def list(self) -> str:
+ return toml.dumps(self.config)
+
+ def __singleton_init__(self):
+ self.config = {}
+
+ @classmethod
+ def __config_path(cls) -> str:
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+ return os.path.abspath(f"{user_data_dir}/etc/compute_nodes.cfg.toml")
diff --git a/src/aios_kernel/compute_task.py b/src/aios_kernel/compute_task.py
new file mode 100644
index 0000000..ea7c431
--- /dev/null
+++ b/src/aios_kernel/compute_task.py
@@ -0,0 +1,122 @@
+
+from enum import Enum
+import uuid
+import time
+from typing import Union
+from knowledge import ObjectID
+
+class ComputeTaskResultCode(Enum):
+ OK = 0
+ TIMEOUT = 1
+ NO_WORKER = 2
+ ERROR = 3
+
+
+class ComputeTaskState(Enum):
+ DONE = 0
+ INIT = 1
+ RUNNING = 2
+ ERROR = 3
+ PENDING = 4
+
+class ComputeTaskType(Enum):
+ NONE = "None"
+ LLM_COMPLETION = "llm_completion"
+ TEXT_2_IMAGE = "text_2_image"
+ IMAGE_2_IMAGE = "image_2_image"
+ VOICE_2_TEXT = "voice_2_text"
+ TEXT_2_VOICE = "text_2_voice"
+ TEXT_EMBEDDING ="text_embedding"
+ IMAGE_EMBEDDING ="image_embedding"
+
+
+class ComputeTask:
+ def __init__(self) -> None:
+ self.task_type = ComputeTaskType.NONE
+ self.create_time = None
+
+ self.task_id: str = None
+ self.callchain_id: str = None
+ self.params: dict = {}
+ self.refers: dict = None
+ self.pading_data: bytearray = None
+
+ self.state = ComputeTaskState.INIT
+ self.result = None
+ self.error_str = None
+
+ def set_llm_params(self, prompts, model_name, max_token_size, inner_functions = None, callchain_id=None):
+ self.task_type = ComputeTaskType.LLM_COMPLETION
+ self.create_time = time.time()
+ self.task_id = uuid.uuid4().hex
+ self.callchain_id = callchain_id
+ self.params["prompts"] = prompts.to_message_list()
+ if model_name is not None:
+ self.params["model_name"] = model_name
+ else:
+ self.params["model_name"] = "gpt-4-0613"
+ if max_token_size is None:
+ self.params["max_token_size"] = 4000
+ else:
+ self.params["max_token_size"] = max_token_size
+
+ if inner_functions is not None:
+ self.params["inner_functions"] = inner_functions
+
+ def set_text_embedding_params(self, input: str, model_name=None, callchain_id = None):
+ self.task_type = ComputeTaskType.TEXT_EMBEDDING
+ self.create_time = time.time()
+ self.task_id = uuid.uuid4().hex
+ self.callchain_id = callchain_id
+ if model_name is not None:
+ self.params["model_name"] = model_name
+ else:
+ self.params["model_name"] = "text-embedding-ada-002"
+ self.params["input"] = input
+
+ def set_image_embedding_params(self, input = Union[ObjectID, bytes], model_name=None, callchain_id = None):
+ self.task_type = ComputeTaskType.IMAGE_EMBEDDING
+ self.create_time = time.time()
+ self.task_id = uuid.uuid4().hex
+ self.callchain_id = callchain_id
+ if model_name is not None:
+ self.params["model_name"] = model_name
+ else:
+ self.params["model_name"] = None
+ self.params["input"] = input
+
+ def set_text_2_image_params(self, prompt: str, model_name, negative_prompt="", callchain_id=None):
+ self.task_type = ComputeTaskType.TEXT_2_IMAGE
+ self.create_time = time.time()
+ self.task_id = uuid.uuid4().hex
+ self.callchain_id = callchain_id
+ self.params["prompt"] = prompt
+ self.params["negative_prompt"] = negative_prompt
+ if model_name is not None:
+ self.params["model_name"] = model_name
+ else:
+ self.params["model_name"] = "v1-5-pruned-emaonly"
+
+ def display(self) -> str:
+ return f"ComputeTask: {self.task_id} {self.task_type} {self.state}"
+
+
+class ComputeTaskResult:
+ def __init__(self) -> None:
+ self.create_time = None
+ self.task_id: str = None
+ self.callchain_id: str = None
+ self.worker_id: str = None
+ self.error_str : str = None
+ self.result_code: int = 0
+ self.result_str: str = None # easy to use,can read from result
+
+ self.result : dict = {}
+
+ self.result_refers: dict = {}
+ self.pading_data: bytearray = None
+
+ def set_from_task(self, task: ComputeTask):
+ self.task_id = task.task_id
+ self.callchain_id = task.callchain_id
+ task.result = self
diff --git a/src/aios_kernel/contact_manager.py b/src/aios_kernel/contact_manager.py
new file mode 100644
index 0000000..39c5711
--- /dev/null
+++ b/src/aios_kernel/contact_manager.py
@@ -0,0 +1,153 @@
+from typing import List
+import toml
+
+class Contact:
+ def __init__(self, name, phone=None, email=None, telegram=None,added_by=None, tags=[], notes=""):
+ self.name = name
+ self.phone = phone
+ self.email = email
+ self.telegram = telegram
+ self.added_by = added_by
+ self.tags = tags
+ self.notes = notes
+ self.is_family_member = False
+
+ def to_dict(self):
+ return {
+ "name": self.name,
+ "phone": self.phone,
+ "email": self.email,
+ "telegram" : self.telegram,
+
+ "added_by": self.added_by,
+ "tags": self.tags,
+ "notes": self.notes
+ }
+
+ @classmethod
+ def from_dict(cls, data):
+ return Contact(data.get("name"), data.get("phone"), data.get("email"), data.get("telegram"),data.get("added_by"), data.get("tags"), data.get("notes"))
+
+class FamilyMember(Contact):
+ def __init__(self, name, relationship,phone=None, email=None,telegram=None):
+ super().__init__(name, phone, email, telegram)
+ self.name = name
+ self.relationship = relationship
+ self.is_family_member = True
+
+ def to_dict(self):
+ result = super().to_dict()
+ result["relationship"] = self.relationship
+ return result
+
+ @classmethod
+ def from_dict(cls, data):
+ return FamilyMember(data.get("name"),data.get("relationship"),data.get("phone"), data.get("email"),data.get("telegram"))
+
+class ContactManager:
+ _instance = None
+ @classmethod
+ def get_instance(cls,filename=None) -> "ContactManager":
+ if cls._instance is None:
+ cls._instance = ContactManager(str(filename))
+ return cls._instance
+
+ def __init__(self, filename="contacts.toml"):
+ self.filename = filename
+ self.contacts = []
+ self.family_members = []
+
+ self.is_auto_create_contact_from_telegram = True
+
+ def load_data(self):
+ try:
+ with open(self.filename, "r") as f:
+ config = toml.load(f)
+ return self.load_from_config(config)
+ except FileNotFoundError:
+ return {}
+
+ def load_from_config(self,config_data:dict):
+ self.contacts = [Contact.from_dict(item) for item in config_data.get("contacts", [])]
+ self.family_members = [FamilyMember.from_dict(item) for item in config_data.get("family_members", [])]
+
+ def save_data(self):
+ data = {
+ "contacts": [contact.to_dict() for contact in self.contacts],
+ "family_members": [member.to_dict() for member in self.family_members]
+ }
+ with open(self.filename, "w") as f:
+ toml.dump(data, f)
+
+ def set_contact(self, name:str, new_contact:Contact):
+ assert name == new_contact.name
+ for i, contact in enumerate(self.contacts):
+ if contact.name == name:
+ self.contacts[i] = new_contact
+ self.save_data()
+ return True
+ for i, member in enumerate(self.family_members):
+ if member.name == name:
+ self.family_members[i] = new_contact
+ self.save_data()
+ return True
+
+ return False
+
+ def add_contact(self, name:str, new_contact:Contact):
+ assert name == new_contact.name
+ self.contacts.append(new_contact)
+ self.save_data()
+
+ def remove_contact(self, name:str):
+ self.contacts = [contact for contact in self.contacts if contact.name != name]
+ self.save_data()
+
+ def find_contact_by_name(self, name:str):
+ for contact in self.contacts:
+ if contact.name == name:
+ return contact
+
+ for member in self.family_members:
+ if member.name == name:
+ return member
+ return None
+
+ def find_contact_by_telegram(self, telegram:str):
+ for contact in self.contacts:
+ if contact.telegram == telegram:
+ return contact
+ for member in self.family_members:
+ if member.telegram == telegram:
+ return member
+ return None
+
+ def find_contact_by_email(self, email:str):
+ for contact in self.contacts:
+ if contact.email == email:
+ return contact
+ for member in self.family_members:
+ if member.email == email:
+ return member
+ return None
+
+ def find_contact_by_phone(self, phone:str):
+ for contact in self.contacts:
+ if contact.phone == phone:
+ return contact
+ for member in self.family_members:
+ if member.phone == phone:
+ return member
+ return None
+
+
+ def add_family_member(self, name, new_member:FamilyMember):
+ assert name == new_member.name
+ self.family_members.append(new_member)
+ self.save_data()
+
+ def list_contacts(self):
+ return self.contacts
+
+ def list_family_members(self):
+ return self.family_members
diff --git a/src/aios_kernel/email_tunnel.py b/src/aios_kernel/email_tunnel.py
new file mode 100644
index 0000000..c4f75b4
--- /dev/null
+++ b/src/aios_kernel/email_tunnel.py
@@ -0,0 +1,143 @@
+import asyncio
+import aiosmtplib
+import aioimaplib
+import email
+from email.header import decode_header
+import mailparser
+import logging
+import time
+import datetime
+from .tunnel import AgentTunnel
+from .agent_message import AgentMsg
+
+from email.message import EmailMessage
+
+logger = logging.getLogger(__name__)
+
+class EmailTunnel(AgentTunnel):
+ @classmethod
+ def register_to_loader(cls):
+ async def load_email_tunnel(config:dict) -> AgentTunnel:
+ result_tunnel = EmailTunnel()
+ if await result_tunnel.load_from_config(config):
+ return result_tunnel
+ else:
+ return None
+
+ AgentTunnel.register_loader("EmailTunnel",load_email_tunnel)
+
+ async def load_from_config(self,config:dict)->bool:
+ self.target_id = config["target"]
+ self.tunnel_id = config["tunnel_id"]
+
+ self.type = "TelegramTunnel"
+ self.email = config["email"]
+ self.imap_server = config["imap"]
+ s = self.imap_server.split(":")
+ if len(s) == 2:
+ self.imap_server = s[0]
+ self.imap_port = int(s[1])
+
+ self.smtp_server = config["smtp"]
+ s = self.smtp_server.split(":")
+ if len(s) == 2:
+ self.smtp_server = s[0]
+ self.smtp_port = int(s[1])
+
+ self.login_user = config["user"]
+ self.login_password = config["password"]
+ self.folder = config["folder"]
+ self.check_interval = config["interval"]
+
+ return True
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.is_start = False
+ self.read_email = {}
+
+ async def on_new_email(self,mail:mailparser.MailParser) -> None:
+ remote_email_addr = mail.from_[0][1]
+ remote_user_name = remote_email_addr.split("@")[0]
+ agent_msg = self.conver_mail_to_agent_msg(mail)
+ agent_msg.sender = remote_user_name
+ agent_msg.target = self.target_id
+ self.ai_bus.register_message_handler(remote_user_name, self._process_message)
+
+ resp_msg = await self.ai_bus.send_message(agent_msg)
+ if resp_msg is None:
+ await self.reply_email(remote_email_addr,"Sorry, I can't understand your message","")
+ else:
+ if resp_msg.body_mime is None:
+ await self.reply_email(remote_email_addr,"result",resp_msg.body)
+
+ async def reply_email(self,target_email:str,title:str,msg:str) -> None:
+ email_msg = EmailMessage()
+ email_msg['Subject'] = f"Reply: {title}"
+ email_msg['From'] = self.email
+ email_msg['To'] = target_email
+ email_msg.set_content(msg)
+
+ await aiosmtplib.send(
+ email_msg,
+ hostname = self.smtp_server,
+ port=self.smtp_port,
+ username=self.login_user,
+ password=self.login_password,
+ )
+
+
+
+ def conver_mail_to_agent_msg(self,mail:mailparser.MailParser) -> AgentMsg:
+ msg = AgentMsg()
+ msg.set("",self.target_id,mail.text_plain[0])
+ msg.topic = "email"
+ return msg
+
+ async def check_email(self) -> None:
+ self.last_check_num = 0
+ self.last_check_time = datetime.datetime.now()
+ while True:
+ if self.is_start == False:
+ return
+
+ await asyncio.sleep(self.check_interval)
+ imap_client = aioimaplib.IMAP4_SSL(host=self.imap_server,port=self.imap_port)
+ await imap_client.wait_hello_from_server()
+ await imap_client.login(self.login_user, self.login_password)
+
+ date_since = self.last_check_time.strftime("%d-%b-%Y")
+
+ await imap_client.select(self.folder)
+ status, messages = await imap_client.search('UNSEEN',charset='US-ASCII')
+ self.last_check_time = datetime.datetime.now()
+ if status == "OK":
+ message_numbers = messages[0].split()
+ for num in message_numbers:
+ num = int(num)
+ if self.read_email.get(num) is not None:
+ continue
+
+ status, email_data = await imap_client.fetch(str(num), "(RFC822)")
+ if status == "OK":
+ #r = email.message_from_bytes(email_data[1])
+ mail = mailparser.parse_from_bytes(email_data[1])
+ self.read_email[num] = mail
+ await self.on_new_email(mail)
+
+ await imap_client.logout()
+
+ async def start(self) -> bool:
+ if self.is_start:
+ logger.warning(f"tunnel {self.tunnel_id} is already started")
+ return False
+ self.is_start = True
+
+ asyncio.create_task(self.check_email())
+ return True
+
+ async def close(self) -> None:
+ self.is_start = False
+
+ async def _process_message(self, msg: AgentMsg) -> None:
+ logger.warn(f"process message {msg.msg_id} from {msg.sender} to {msg.target}")
diff --git a/src/aios_kernel/environment.py b/src/aios_kernel/environment.py
new file mode 100644
index 0000000..1e8cdab
--- /dev/null
+++ b/src/aios_kernel/environment.py
@@ -0,0 +1,135 @@
+# basic environment class
+# we have some built-in environment: Calender(include timer),Home(connect to IoT device in your home), ,KnwoledgeBase,FileSystem,
+
+from abc import ABC, abstractmethod
+from typing import Any, Callable, Optional,Dict,Awaitable,List
+import logging
+
+from .ai_function import AIFunction
+
+logger = logging.getLogger(__name__)
+
+class EnvironmentEvent(ABC):
+ @abstractmethod
+ def display(self) -> str:
+ pass
+
+EnvironmentEventHandler = Callable[[str,EnvironmentEvent],Awaitable[Any]]
+
+class Environment:
+ _all_env = {}
+ @classmethod
+ def get_env_by_id(cls,env_id:str):
+ return cls._all_env.get(env_id)
+
+ @classmethod
+ def set_env_by_id(cls,id,env):
+ assert id == env.get_id()
+ cls._all_env[env.get_id()] = env
+
+ def __init__(self,env_id:str) -> None:
+ self.env_id = env_id
+ self.values:Dict[str,str] = {}
+ self.get_handlers:Dict[str,Callable] = {}
+ self.owner_env:Dict[str,Environment] = {}
+ # self.valid_keys:Dict[str,bool] = None
+ self.event_handlers:Dict[str,List[EnvironmentEventHandler]]= {}
+
+ self.functions : Dict[str,AIFunction] = {}
+
+ def get_id(self) -> str:
+ return self.env_id
+
+ def add_owner_env(self,env) -> None:
+ self.owner_env[env.get_id()] = env
+
+ #@abstractmethod
+ #TODO: how to use env? different env has different prompt
+ #def get_env_prompt(self) -> str:
+ # pass
+
+ def add_ai_function(self,func:AIFunction) -> None:
+ if self.functions.get(func.get_name()) is not None:
+ logger.warn(f"add ai_function {func.get_name()} in env {self.env_id}:function already exist")
+
+ self.functions[func.get_name()] = func
+
+ def get_ai_function(self,func_name:str) -> AIFunction:
+ return self.functions.get(func_name)
+
+ #def enable_ai_function(self,func_name:str) -> None:
+ # pass
+
+ #def disable_ai_function(self,func_name:str) -> None:
+ # pass
+
+ def get_all_ai_functions(self) -> List[AIFunction]:
+ return self.functions.values()
+
+ @abstractmethod
+ def _do_get_value(self,key:str) -> Optional[str]:
+ pass
+
+ def register_get_handler(self,key:str,handler:Callable) -> None:
+ h = self.get_handlers.get(key)
+ if h is not None:
+ logger.warn(f"register get_handler {key} in env {self.env_id}:handler already exist")
+
+ self.get_handlers[key] = handler
+
+
+ def attach_event_handler(self,event_id:str,handler:Callable) -> None:
+ handler_list = self.event_handlers.get(event_id)
+ if handler_list is None:
+ handler_list = []
+ self.event_handlers[event_id] = handler_list
+
+ handler_list.append(handler)
+
+ def remove_event_handler(self,event_id:str,handler:Callable) -> None:
+ handler_list = self.event_handlers.get(event_id)
+ if handler is not None:
+ handler_list.remove(handler)
+ return
+
+ logger.warn(f"remove event_handler {event_id} in env {self.env_id}:handler not found")
+
+ async def fire_event(self,event_id:str,event:EnvironmentEvent) -> None:
+ handler_list = self.event_handlers.get(event_id)
+ if handler_list is not None:
+ for handler in handler_list:
+ await handler(self.env_id,event)
+ else:
+ logger.debug(f"fire event {event_id} in env {self.env_id}:handler not found")
+ return
+
+ def __getitem__(self, key):
+ return self.get_value(key)
+
+ def get_value(self,key:str) -> Optional[str]:
+ handler = self.get_handlers.get(key)
+ if handler is not None:
+ return handler()
+
+ s = self.values.get(key)
+ if isinstance(s,str):
+ return s
+ else:
+ logger.warn(f"get value {key} in env {self.env_id} failed!,type is not str")
+
+ s = self._do_get_value(key)
+ if s is not None:
+ return s
+ if self.owner_env is not None:
+ for env in self.owner_env.values():
+ s = env.get_value(key)
+ if s is not None:
+ return s
+
+ logger.warn(f"get value {key} in env {self.env_id} failed!,not found")
+ return None
+
+ def set_value(self, key: str, str_value: str,is_storage:bool = True):
+ logger.info(f"set value {key} in env {self.env_id} to {str_value}")
+ self.values[key] = str_value
+
diff --git a/src/aios_kernel/google_text_to_speech_node.py b/src/aios_kernel/google_text_to_speech_node.py
new file mode 100644
index 0000000..0fdba47
--- /dev/null
+++ b/src/aios_kernel/google_text_to_speech_node.py
@@ -0,0 +1,180 @@
+
+import os
+import asyncio
+from asyncio import Queue
+import logging
+from typing import Optional
+
+from google.cloud import texttospeech
+
+from .storage import AIStorage
+from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType
+from .compute_node import ComputeNode
+
+logger = logging.getLogger(__name__)
+
+
+"""
+You need to set the GOOGLE_APPLICATION_CREDENTIALS environment variable when using it.
+see:https://cloud.google.com/text-to-speech/docs/before-you-begin
+"""
+
+
+class GoogleTextToSpeechNode(ComputeNode):
+ _instance = None
+
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ def __init__(self):
+ super().__init__()
+ self.node_id = "google_text_to_speech_node"
+ self.task_queue = Queue()
+ self.client: Optional[texttospeech.TextToSpeechClient] = None
+
+ self.language_list = {
+ "cnm-CN": {
+ "female": ["cmn-CN-Standard-A",
+ "cmn-CN-Standard-D",
+ "cmn-CN-Wavenet-A",
+ "cmn-CN-Wavenet-D",
+ "cmn-TW-Standard-A",
+ "cmn-TW-Wavenet-A"],
+ "man": ["cmn-CN-Standard-B",
+ "cmn-CN-Standard-C",
+ "cmn-CN-Wavenet-B",
+ "cmn-CN-Wavenet-C",
+ "cmn-TW-Standard-B",
+ "cmn-TW-Standard-C",
+ "cmn-TW-Wavenet-B",
+ "cmn-TW-Wavenet-C"]
+ },
+ "en-US": {
+ "female": ["en-US-Neural2-C",
+ "en-US-Neural2-E",
+ "en-US-Neural2-F",
+ "en-US-Neural2-G",
+ "en-US-Neural2-H",
+ "en-US-News-K",
+ "en-US-News-L",
+ "en-US-Standard-C",
+ "en-US-Standard-E",
+ "en-US-Standard-F",
+ "en-US-Standard-G",
+ "en-US-Standard-H",
+ "en-US-Studio-O",
+ "en-US-Wavenet-C",
+ "en-US-Wavenet-E",
+ "en-US-Wavenet-F",
+ "en-US-Wavenet-G",
+ "en-US-Wavenet-H"],
+ "man": ["en-US-Polyglot-1",
+ "en-US-Standard-A",
+ "en-US-Standard-B",
+ "en-US-Standard-D",
+ "en-US-Standard-I",
+ "en-US-Standard-J",
+ "en-US-Studio-M",
+ "en-US-Wavenet-A",
+ "en-US-Wavenet-B",
+ "en-US-Wavenet-D",
+ "en-US-Wavenet-I",
+ "en-US-Wavenet-J"]
+ }
+ }
+ self.start()
+
+ def init(self):
+ user_config = AIStorage.get_instance().get_user_config()
+ google_application_credentials = user_config.get_value("google_application_credentials")
+ if google_application_credentials is None:
+ raise Exception("google_application_credentials is None!")
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = google_application_credentials
+ self.client = texttospeech.TextToSpeechClient()
+
+ def start(self):
+ async def _run_task_loop():
+ while True:
+ task = await self.task_queue.get()
+ try:
+ result = self._run_task(task)
+ if result is not None:
+ task.state = ComputeTaskState.DONE
+ task.result = result
+ except Exception as e:
+ logger.error(f"google_text_to_speech_node run task error: {e}")
+ task.state = ComputeTaskState.ERROR
+ task.result = ComputeTaskResult()
+ task.result.set_from_task(task)
+ task.result.worker_id = self.node_id
+ task.result.result_str = str(e)
+
+ asyncio.create_task(_run_task_loop())
+
+ def _run_task(self, task: ComputeTask):
+ task.state = ComputeTaskState.RUNNING
+ language_code = task.params["language_code"]
+ text = task.params["text"]
+ voice_name = task.params["voice_name"]
+ gender = task.params["gender"]
+ age = task.params["age"]
+
+ if language_code == "zh":
+ language_code = "cnm-CN"
+ elif language_code == "en":
+ language_code = "en-US"
+ else:
+ raise Exception(f"language_code {language_code} not support")
+
+ lang_list = self.language_list[language_code][gender]
+ voice = lang_list[hash(voice_name) % len(lang_list)]
+
+ synthesis_input = texttospeech.SynthesisInput(text=text)
+ voice = texttospeech.VoiceSelectionParams(language_code=language_code,
+ ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL,
+ name=voice)
+
+ audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3)
+
+ response = self.client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config)
+
+ result = ComputeTaskResult()
+ result.set_from_task(task)
+ result.worker_id = self.node_id
+ result.result = response.audio_content
+ return result
+
+ async def push_task(self, task: ComputeTask, proiority: int = 0):
+ logger.info(f"google_text_to_speech_node push task: {task.display()}")
+ self.task_queue.put_nowait(task)
+
+ async def remove_task(self, task_id: str):
+ pass
+
+ def get_task_state(self, task_id: str):
+ pass
+
+ def display(self) -> str:
+ return f"GoogleTextToSpeechNode: {self.node_id}"
+
+ def get_capacity(self):
+ return 0
+
+ def is_support(self, task: ComputeTask) -> bool:
+ if task.task_type == ComputeTaskType.TEXT_2_VOICE:
+ return True
+ return False
+
+ def is_local(self) -> bool:
+ return False
+
+ def declare_user_config(self,is_optional:bool = False):
+ if os.getenv("GOOGLE_APPLICATION_CREDENTIALS") is None:
+ user_config = AIStorage.get_instance().get_user_config()
+ user_config.add_user_config("google_application_credentials",
+ "google application credentials, please visit:https://cloud.google.com/text-to-speech/docs/before-you-begin",
+ True,
+ None)
diff --git a/src/aios_kernel/knowledge_base.py b/src/aios_kernel/knowledge_base.py
new file mode 100644
index 0000000..63c7d04
--- /dev/null
+++ b/src/aios_kernel/knowledge_base.py
@@ -0,0 +1,295 @@
+# define a knowledge base class
+import json
+import logging
+from .agent import AgentPrompt
+from .compute_kernel import ComputeKernel
+from .storage import AIStorage
+from .environment import Environment
+from .ai_function import SimpleAIFunction
+from knowledge import *
+
+
+class KnowledgeBase:
+ _instance = None
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ cls._instance.__singleton_init__()
+
+ return cls._instance
+
+ def __singleton_init__(self) -> None:
+ self.store = KnowledgeStore()
+ self.compute_kernel = ComputeKernel.get_instance()
+ self._default_text_model = "all-MiniLM-L6-v2"
+ self._default_image_model = "clip-ViT-B-32"
+
+ async def __embedding_document(self, document: DocumentObject):
+ for chunk_id in document.get_chunk_list():
+ chunk = self.store.get_chunk_reader().get_chunk(chunk_id)
+ if chunk is None:
+ raise ValueError(f"text chunk not found: {chunk_id}")
+
+ text = chunk.read().decode("utf-8")
+ vector = await self.compute_kernel.do_text_embedding(text, self._default_text_model)
+ if vector:
+ await self.store.get_vector_store(self._default_text_model).insert(vector, chunk_id)
+
+ async def __embedding_image(self, image: ImageObject):
+ # desc = {}
+ # if not not image.get_meta():
+ # desc["meta"] = image.get_meta()
+ # if not not image.get_exif():
+ # desc["exif"] = image.get_exif()
+ # if not not image.get_tags():
+ # desc["tags"] = image.get_tags()
+ # vector = await self.compute_kernel.do_text_embedding(json.dumps(desc), self._default_text_model)
+ vector = await self.compute_kernel.do_image_embedding(image.calculate_id(), self._default_image_model)
+ if vector:
+ await self.store.get_vector_store(self._default_image_model).insert(vector, image.calculate_id())
+
+ async def __embedding_video(self, vedio: VideoObject):
+ desc = {}
+ if not not vedio.get_meta():
+ desc["meta"] = vedio.get_meta()
+ if not not vedio.get_info():
+ desc["info"] = vedio.get_info()
+ if not not vedio.get_tags():
+ desc["tags"] = vedio.get_tags()
+ vector = await self.compute_kernel.do_text_embedding(json.dumps(desc), self._default_text_model)
+ await self.store.get_vector_store(self._default_text_model).insert(vector, vedio.calculate_id())
+
+ async def __embedding_rich_text(self, rich_text: RichTextObject):
+ for document_id in rich_text.get_documents().values():
+ document = DocumentObject.decode(self.store.get_object_store().get_object(document_id))
+ await self.__embedding_document(document)
+ for image_id in rich_text.get_images().values():
+ image = ImageObject.decode(self.store.get_object_store().get_object(image_id))
+ await self.__embedding_image(image)
+ for video_id in rich_text.get_videos().values():
+ video = VideoObject.decode(self.store.get_object_store().get_object(video_id))
+ await self.__embedding_video(video)
+ for rich_text_id in rich_text.get_rich_texts().values():
+ rich_text = RichTextObject.decode(self.store.get_object_store().get_object(rich_text_id))
+ await self.__embedding_rich_text(rich_text)
+
+ async def __embedding_email(self, email: EmailObject):
+ vector = await self.compute_kernel.do_text_embedding(json.dumps(email.get_desc()), self._default_text_model)
+ await self.store.get_vector_store(self._default_text_model).insert(vector, email.calculate_id())
+ await self.__embedding_rich_text(email.get_rich_text())
+
+
+ async def __do_embedding(self, object: KnowledgeObject):
+ if object.get_object_type() == ObjectType.Document:
+ await self.__embedding_document(object)
+ if object.get_object_type() == ObjectType.Image:
+ await self.__embedding_image(object)
+ if object.get_object_type() == ObjectType.Video:
+ await self.__embedding_video(object)
+ if object.get_object_type() == ObjectType.RichText:
+ await self.__embedding_rich_text(object)
+ if object.get_object_type() == ObjectType.Email:
+ await self.__embedding_email(object)
+ else:
+ pass
+
+ # def __save_document(self, document: DocumentObject):
+ # doc_id = document.calculate_id()
+ # self.store.get_object_store().put_object(doc_id, document.encode())
+ # for chunk_id in document.get_chunk_list():
+ # self.store.get_relation_store().add_relation(chunk_id, doc_id)
+
+ # def __save_image(self, image: ImageObject):
+ # image_id = image.calculate_id()
+ # self.store.get_object_store().put_object(image_id, image.encode())
+
+ # def __save_video(self, video: VideoObject):
+ # video_id = video.calculate_id()
+ # self.store.get_object_store().put_object(video_id, video.encode())
+
+ # def __save_rich_text(self, rich_text: RichTextObject):
+ # rich_text_id = rich_text.calculate_id()
+ # # rich_text_enc = dict()
+ # # rich_text_enc["desc"] = rich_text.desc
+ # # rich_text_enc["body"] = {"documents": {}, "images": {}, "videos": {}, "rich_texts": {}}
+ # for key, document in rich_text.get_documents().items():
+ # self.__save_document(document)
+ # doc_id = document.calculate_id()
+ # self.store.get_relation_store().add_relation(doc_id, rich_text_id)
+ # # rich_text_enc["body"]["documents"][key] = doc_id
+ # for key, image in rich_text.get_images().items():
+ # self.__save_image(image)
+ # image_id = image.calculate_id()
+ # self.store.get_relation_store().add_relation(image_id, rich_text_id)
+ # # rich_text_enc["body"]["images"][key] = image_id
+ # for key, video in rich_text.get_videos().items():
+ # self.__save_video(video)
+ # video_id = video.calculate_id()
+ # self.store.get_relation_store().add_relation(video_id, rich_text_id)
+ # # rich_text_enc["body"]["videos"][key] = video_id
+ # for key, rich_text in rich_text.get_rich_texts().items():
+ # self.__save_rich_text(rich_text)
+ # rich_text_id = rich_text.calculate_id()
+ # self.store.get_relation_store().add_relation(rich_text_id, rich_text_id)
+ # # rich_text_enc["body"]["rich_texts"][key] = rich_text_id
+
+
+ # self.store.get_object_store().put_object(rich_text_id, rich_text.encode())
+
+ # def __save_email(self, email: EmailObject):
+ # email_id = email.calculate_id()
+ # # email_enc = dict()
+ # # email_enc["desc"] = email.desc
+ # # email_enc["body"] = {"content": None}
+ # self.__save_rich_text(email.get_rich_text())
+ # rich_text_id = email.get_rich_text().calculate_id()
+ # self.store.get_relation_store().add_relation(rich_text_id, email_id)
+ # # email_enc["body"]["content"] = rich_text_id
+ # self.store.get_object_store().put_object(email_id, email.encode())
+
+
+ # def __save_object(self, object: KnowledgeObject):
+ # if object.get_object_type() == ObjectType.Document:
+ # self.__save_document(object)
+ # if object.get_object_type() == ObjectType.Image:
+ # self.__save_image(object)
+ # if object.get_object_type() == ObjectType.Video:
+ # self.__save_video(object)
+ # if object.get_object_type() == ObjectType.RichText:
+ # self.__save_rich_text(object)
+ # if object.get_object_type() == ObjectType.Email:
+ # self.__save_email(object)
+ # else:
+ # pass
+
+ async def insert_object(self, object: KnowledgeObject):
+ self.store.get_object_store().put_object(object.calculate_id(), object.encode())
+ await self.__do_embedding(object)
+
+ async def query_objects(self, tokens: str, types: list[str], topk: int) -> [ObjectID]:
+ texts = []
+ if "text" in types:
+ vector = await self.compute_kernel.do_text_embedding(tokens, self._default_text_model)
+ texts = await self.store.get_vector_store(self._default_text_model).query(vector, topk)
+ images = []
+ if "image" in types:
+ vector = await self.compute_kernel.do_text_embedding(tokens, self._default_image_model)
+ images = await self.store.get_vector_store(self._default_image_model).query(vector, topk)
+ return texts + images
+
+ def load_object(self, object_id: ObjectID) -> KnowledgeObject:
+ if object_id.get_object_type() == ObjectType.Document:
+ return DocumentObject.decode(self.store.get_object_store().get_object(object_id))
+ if object_id.get_object_type() == ObjectType.Image:
+ return ImageObject.decode(self.store.get_object_store().get_object(object_id))
+ if object_id.get_object_type() == ObjectType.Video:
+ return VideoObject.decode(self.store.get_object_store().get_object(object_id))
+ if object_id.get_object_type() == ObjectType.RichText:
+ return RichTextObject.decode(self.store.get_object_store().get_object(object_id))
+ if object_id.get_object_type() == ObjectType.Email:
+ return EmailObject.decode(self.store.get_object_store().get_object(object_id))
+ else:
+ pass
+
+
+ def tokens_from_objects(self, object_ids: [ObjectID]) -> list[str]:
+ results = dict()
+ for object_id in object_ids:
+ parents = self.store.get_relation_store().get_related_root_objects(object_id)
+ # last parent is the root object
+ root_object_id = parents[0] if parents else object_id
+ logging.info(f"object_id: {str(object_id)} root_object_id: {str(root_object_id)}")
+ if str(root_object_id) in results:
+ results[str(root_object_id)].append(object_id)
+ else:
+ results[str(root_object_id)] = [root_object_id, object_id]
+ content = ""
+ result_desc = []
+ for result in results.values():
+ # first element in result is the root object
+ root_object_id = result[0]
+ if root_object_id.get_object_type() == ObjectType.Email:
+ email = self.load_object(root_object_id)
+ desc = email.get_desc()
+ desc["type"] = "email"
+ desc["contents"] = []
+ result_desc.append(desc)
+ upper_list = desc["contents"]
+ result = result[1:]
+ else:
+ upper_list = result_desc
+
+ for object_id in result:
+ if object_id.get_object_type() == ObjectType.Chunk:
+ upper_list.append({"type": "text", "content": self.store.get_chunk_reader().get_chunk(object_id).read().decode("utf-8")})
+ if object_id.get_object_type() == ObjectType.Image:
+ # image = self.load_object(object_id)
+ desc = dict()
+ desc["id"] = str(object_id)
+ desc["type"] = "image"
+ upper_list.append(desc)
+ if object_id.get_object_type() == ObjectType.Video:
+ video = self.load_object(object_id)
+ desc = video.get_desc()
+ desc["type"] = "video"
+ upper_list.append(desc)
+ else:
+ pass
+ content += json.dumps(result_desc)
+ content += ".\n"
+
+ return content
+
+ def parse_object_in_message(self, message: str) -> KnowledgeObject:
+ # get message's first line
+ logging.info(f"tg parse resp message: {message}")
+ lines = message.split("\n")
+ if len(lines) > 0:
+ message = lines[0]
+ try:
+ desc = json.loads(message)
+ if isinstance(desc, dict):
+ object_id = desc["id"]
+ else:
+ object_id = desc[0]["id"]
+ except Exception as e:
+ return None
+
+ if object_id is not None:
+ return self.load_object(ObjectID.from_base58(object_id))
+
+
+ def bytes_from_object(self, object: KnowledgeObject) -> bytes:
+ if object.get_object_type() == ObjectType.Image:
+ image_object = object
+ return self.store.get_chunk_reader().read_chunk_list_to_single_bytes(image_object.get_chunk_list())
+
+
+
+
+
+
+
+class KnowledgeEnvironment(Environment):
+ def __init__(self, env_id: str) -> None:
+ super().__init__(env_id)
+
+ query_param = {
+ "tokens": "key words to query",
+ "types": "prefered knowledge types, one or more of [text, image]",
+ "index": "index of query result"
+ }
+ self.add_ai_function(SimpleAIFunction("query_knowledge",
+ "vector query content from local knowledge base",
+ self._query,
+ query_param))
+
+ async def _query(self, tokens: str, types: list[str] = ["text"], index: str=0):
+ index = int(index)
+ object_ids = await KnowledgeBase().query_objects(tokens, types, 4)
+ if len(object_ids) <= index:
+ return "*** I have no more information for your reference.\n"
+ else:
+ content = "*** I have provided the following known information for your reference with json format:\n"
+ return content + KnowledgeBase().tokens_from_objects(object_ids[index:index+1])
\ No newline at end of file
diff --git a/src/aios_kernel/knowledge_pipeline.py b/src/aios_kernel/knowledge_pipeline.py
new file mode 100644
index 0000000..759793a
--- /dev/null
+++ b/src/aios_kernel/knowledge_pipeline.py
@@ -0,0 +1,411 @@
+"""
+Capture your email locally, and parse out the pictures in the email body and the pictures, videos and other files in the attachment. Subsequently, it supports vectorized analysis of your personal data and serves as a knowledge base to enable large language model answers. Better results.
+
+An example of a local file is as follows:
+├── data
+│ └── alex0072@gmail.com
+│ └── 5de3e52f3a6b90cabe6cbdd4ae3a5c5b
+│ ├── email.txt
+│ ├── meta.json
+│ ├── image
+│ │ ├── 0648B869@99C03070.DB94B354.jpg
+│ └── body_image
+│ ├── 11044884873.jpg
+│ ├── 282985198265470.gif
+│ └── dd-login-service-min.png
+
+"""
+import asyncio
+import datetime
+import sqlite3
+import imaplib
+import logging
+import mailparser
+import hashlib
+import json
+import base64
+import chardet
+import aiofiles
+
+from bs4 import BeautifulSoup
+import requests
+import os
+import toml
+from .storage import AIStorage, UserConfigItem
+from .knowledge_base import KnowledgeBase, ImageObjectBuilder, ObjectID, ObjectType, DocumentObjectBuilder, EmailObjectBuilder, EmailObject
+
+class KnowledgeJournal:
+ def __init__(self, source_type: str, source_id: str, item_id: str, object_id: str, timestamp=None):
+ # define a timestamp variable
+ self.timestamp = datetime.datetime.now() if timestamp is None else timestamp
+ self.object_id = object_id
+ self.source_type = source_type
+ self.source_id = source_id
+ self.item_id = item_id
+
+ def __str__(self) -> str:
+ if self.source_type == "dir":
+ object_id = ObjectID.from_base58(self.object_id)
+ object_type = None
+ if object_id.get_object_type() == ObjectType.Image:
+ object_type = "image"
+ else:
+ pass
+ return f"Add {object_type} from {os.path.join(self.source_id, self.item_id)}"
+ if self.source_type == "email":
+ object_id = ObjectID.from_base58(self.object_id)
+ email = EmailObject.decode(KnowledgeBase().store.get_object_store().get_object(object_id))
+ meta = email.get_meta()
+ return f'Add email from {os.path.join(self.source_id)} subject {meta["subject"]}'
+
+
+# init sqlite3 client
+class KnowledgeJournalClient:
+ def __init__(self):
+ knowledge_dir = os.path.join(AIStorage.get_instance().get_myai_dir(), "knowledge")
+ if not os.path.exists(knowledge_dir):
+ os.makedirs(knowledge_dir)
+ self.journal_path = os.path.join(knowledge_dir, "journal.db")
+
+ conn = sqlite3.connect(self.journal_path)
+ conn.execute(
+ '''CREATE TABLE IF NOT EXISTS journal (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ time DATETIME DEFAULT CURRENT_TIMESTAMP,
+ source_type TEXT,
+ source_id TEXT,
+ item_id TEXT,
+ object_id TEXT)'''
+ )
+ conn.commit()
+
+ def insert(self, journal: KnowledgeJournal):
+ conn = sqlite3.connect(self.journal_path)
+ conn.execute(
+ "INSERT INTO journal (time, source_type, source_id, item_id, object_id) VALUES (?, ?, ?, ?, ?)",
+ (journal.timestamp, journal.source_type, journal.source_id, journal.item_id, journal.object_id),
+ )
+ conn.commit()
+
+ def latest_journal(self, source_id: str) -> KnowledgeJournal:
+ conn = sqlite3.connect(self.journal_path)
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM journal WHERE source_id = ? ORDER BY id DESC LIMIT 1", (source_id,))
+ result = cursor.fetchone()
+ if result is None:
+ return None
+ else:
+ (_, timestamp, source_type, sorce_id, item_id, object_id) = result
+ return KnowledgeJournal(source_type, sorce_id, item_id, object_id, timestamp)
+
+ def latest_journals(self, topn) -> [KnowledgeJournal]:
+ conn = sqlite3.connect(self.journal_path)
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM journal ORDER BY id DESC LIMIT ?", (topn,))
+ return [KnowledgeJournal(source_type, sorce_id, item_id, object_id, timestamp) for (_, timestamp, source_type, sorce_id, item_id, object_id) in cursor.fetchall()]
+
+
+class KnowledgeEmailSource:
+ def __init__(self, config:dict):
+ self.config = config
+ self.config["type"] = "email"
+
+ def id(self):
+ return self.config["address"]
+
+ @classmethod
+ def user_config_items(cls):
+ return [("address", "email address"),
+ ("password", "email password"),
+ ("imap_server", "imap server"),
+ ("imap_port", "imap port")
+ ]
+
+ @classmethod
+ def local_root(cls):
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+ return os.path.abspath(f"{user_data_dir}/knowledge/email")
+
+ async def run_once(self):
+ # read config from toml file
+ # and read from config config.local.toml if exists (config.local.toml is ignored by git)
+ logging.debug(f"knowledge email source {self.id()} run once")
+ filter = "ALL"
+ self.client = self.email_client()
+ await self.read_emails(imap_keyword=filter)
+
+ def email_client(self) -> imaplib.IMAP4_SSL:
+ logging.info(f"read email config from {self.config.get('imap_server')}")
+ client = imaplib.IMAP4_SSL(
+ host=self.config.get('imap_server'),
+ port=self.config.get('imap_port')
+ )
+ client.login(self.config.get('address'), self.config.get('password'))
+ return client
+
+ async def read_emails(self, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"):
+ journal_client = KnowledgeJournalClient()
+ latest_journal = journal_client.latest_journal(self.id())
+ latest_uid = 0 if latest_journal is None else int(latest_journal.item_id)
+ self.client.select(folder)
+ _, data = self.client.uid('search', None, imap_keyword)
+
+ # get email uid list
+ email_list = data[0].split()
+ logging.info(f"got {len(email_list)} emails")
+ journal_client = KnowledgeJournalClient()
+ for uid in email_list:
+ _uid = int.from_bytes(uid)
+ if _uid > latest_uid:
+ email_dir = self.check_email_saved(uid)
+ if email_dir is not None:
+ logging.info(f"email uid {uid} already saved")
+ else:
+ email_dir = self.read_and_save_email(uid)
+ logging.info(f"email uid {uid} saved")
+ email_object = EmailObjectBuilder({}, email_dir).build()
+ await KnowledgeBase().insert_object(email_object)
+ journal_client.insert(KnowledgeJournal("email", self.id(), str(int.from_bytes(uid)), str(email_object.calculate_id())))
+
+
+ def read_and_save_email(self, uid: str) -> str:
+ message_parts = "(BODY.PEEK[])"
+ _, email_data = self.client.uid('fetch', uid, message_parts)
+ mail = mailparser.parse_from_bytes(email_data[0][1])
+ logging.info(f"got email subject [{mail.subject}]")
+ self.save_email(mail)
+ return self.get_local_dir_name(mail)
+
+ def get_local_dir_name(self, mail: mailparser.MailParser) -> str:
+ dir = f"{self.local_root()}/{self.config.get('address')}"
+ name = f"{mail.subject}__{mail.date}"
+ name = hashlib.md5(name.encode('utf-8')).hexdigest()
+ return f"{dir}/{name}"
+
+ def check_email_saved(self, uid: str) -> str:
+ message_parts = "(BODY[HEADER])"
+ _, email_data = self.client.uid('fetch', uid, message_parts)
+ mail = mailparser.parse_from_bytes(email_data[0][1])
+ logging.info(f"[{uid}]check email subject [{mail.subject}]")
+ dir = self.get_local_dir_name(mail)
+ logging.info(f"check email saved {dir}")
+ file = f"{dir}/email.txt"
+ if os.path.exists(file):
+ return dir
+ return None
+
+ # save email attachment(images)
+ def save_email_attachment(self, mail: mailparser.MailParser, email_dir: str):
+ for attachment in mail.attachments:
+ if attachment['mail_content_type'] in ['image/png', 'image/jpeg', 'image/gif']:
+ print('current mail have image attachment')
+ img_dir = f"{email_dir}/image"
+ if not os.path.exists(img_dir):
+ os.makedirs(img_dir)
+ filename = attachment['filename']
+ filefullname = f"{img_dir}/{filename}"
+ image_data = attachment['payload']
+ try:
+ image_data = base64.b64decode(image_data)
+ except base64.binascii.Error:
+ image_data = image_data.encode()
+ with open(filefullname, 'wb') as f:
+ f.write(image_data)
+ logging.info(f"save email image {filename} success")
+
+ # save email body images(html content)
+ def save_body_images(self, html_content: str, email_dir: str):
+ # get all image urls
+ soup = BeautifulSoup(html_content, 'html.parser')
+ img_tags = soup.find_all('img')
+ img_urls = [img['src'] for img in img_tags if 'src' in img.attrs]
+ logging.info(f'Found {len(img_urls)} images in email body')
+
+ name_count = 0
+
+ if not os.path.exists(email_dir):
+ os.makedirs(email_dir)
+
+ for img_url in img_urls:
+ # keep the original image filename(last of url)
+ ext = img_url.split('/')[-1].split('.')[-1]
+ img_filename = os.path.join(email_dir, f"{name_count}.{ext}")
+ name_count += 1
+ # download image
+ response = requests.get(img_url, stream=True)
+ if response.status_code == 200:
+ with open(img_filename, 'wb') as img_file:
+ for chunk in response.iter_content(1024):
+ img_file.write(chunk)
+ logging.info(f'Downloaded {img_url} to {img_filename}')
+ else:
+ logging.info(f'Failed to download {img_url}')
+
+ # save email content to local dir
+ def save_email(self, mail: mailparser.MailParser):
+ dir = f"{self.local_root()}/{self.config.get('address')}"
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+ email_dir = self.get_local_dir_name(mail)
+ logging.info(f"save email to {email_dir}")
+ if not os.path.exists(email_dir):
+ os.makedirs(email_dir)
+ with open(f"{email_dir}/email.txt", "w", encoding='utf-8') as f:
+ # soup = BeautifulSoup(mail.body, 'html.parser')
+ f.write(mail.body)
+ with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f:
+ mail_dict = json.loads(mail.mail_json)
+ if 'body' in mail_dict:
+ del mail_dict['body']
+ json.dump(mail_dict, f, ensure_ascii=False, indent=4)
+ logging.info(f"save email meta info {f.name}")
+
+ self.save_email_attachment(mail, email_dir)
+ self.save_body_images(mail.body, f"{email_dir}/body_image")
+
+
+class KnowledgeDirSource:
+ def __init__(self, config):
+ self.config = config
+ config["path"] = os.path.abspath(config["path"])
+ self.config["type"] = "dir"
+
+ @classmethod
+ def user_config_items(cls):
+ return [("path", "local dir path")]
+
+ def id(self):
+ return self.config["path"]
+
+ def path(self):
+ return self.config["path"]
+
+ @staticmethod
+ async def read_txt_file(file_path:str)->str:
+ cur_encode = "utf-8"
+ async with aiofiles.open(file_path,'rb') as f:
+ cur_encode = chardet.detect(await f.read())['encoding']
+
+ async with aiofiles.open(file_path,'r',encoding=cur_encode) as f:
+ return await f.read()
+
+ async def run_once(self):
+ logging.debug(f"knowledge dir source {self.id()} run once")
+ journal_client = KnowledgeJournalClient()
+ latest_journal = journal_client.latest_journal(self.id())
+ if latest_journal is not None:
+ if os.path.getmtime(self.path()) <= latest_journal.timestamp:
+ logging.debug(f"knowledge dir source {self.id()} ingnored for no update")
+ return
+ file_pathes = sorted(os.listdir(self.path()), key=lambda x: os.path.getctime(os.path.join(self.path(), x)))
+ for rel_path in file_pathes:
+ file_path = os.path.join(self.path(), rel_path)
+ timestamp = os.path.getctime(file_path)
+ if latest_journal is not None:
+ if timestamp <= latest_journal.timestamp:
+ continue
+ ext = os.path.splitext(file_path)[1].lower()
+ if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
+ logging.info(f"knowledge dir source {self.id()} found image file {file_path}")
+ image = ImageObjectBuilder({}, {}, file_path).build()
+ await KnowledgeBase().insert_object(image)
+ journal_client.insert(KnowledgeJournal("dir", self.id(), rel_path, str(image.calculate_id()), timestamp))
+ if ext in ['.txt']:
+ logging.info(f"knowledge dir source {self.id()} found text file {file_path}")
+ text = await self.read_txt_file(file_path)
+
+ document = DocumentObjectBuilder({}, {}, text).build()
+ await KnowledgeBase().insert_object(document)
+ journal_client.insert(KnowledgeJournal("dir", self.id(), rel_path, str(document.calculate_id()), timestamp))
+
+
+
+
+# define singleton class knowledge pipline
+class KnowledgePipline:
+ _instance = None
+
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = KnowledgePipline()
+ cls._instance.__singleton_init__()
+
+ return cls._instance
+
+ def initial(self):
+ config_path = self.__config_path()
+ logging.info(f"initial knowledge pipline from {config_path}")
+ if os.path.exists(config_path):
+ config = toml.load(self.__config_path())
+ for source_config in config["sources"]:
+ if source_config['type'] == 'email':
+ self.add_email_source(KnowledgeEmailSource(source_config))
+ if source_config['type'] == 'dir':
+ self.add_dir_source(KnowledgeDirSource(source_config))
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+ default_dir = os.path.abspath(f"{user_data_dir}/data")
+ if not os.path.exists(default_dir):
+ os.makedirs(default_dir)
+ self.add_dir_source(KnowledgeDirSource({"path": default_dir}))
+
+ return True
+
+ def __singleton_init__(self):
+ self.knowledge_base = KnowledgeBase()
+ self.email_sources = dict()
+ self.dir_sources = dict()
+ self.source_queue = list()
+ self.run_lock = asyncio.Lock()
+ asyncio.create_task(self.run_loop())
+
+
+ def save_config(self):
+ config = dict()
+ config["sources"] = [source.config for source in self.source_queue]
+ with open(self.__config_path(), "w") as f:
+ toml.dump(config, f)
+
+
+ @classmethod
+ def __config_path(cls) -> str:
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+ return os.path.abspath(f"{user_data_dir}/etc/knowledge.cfg.toml")
+
+
+ def add_email_source(self, source: KnowledgeEmailSource):
+ if self.email_sources.get(source.id()) is not None:
+ return "already exists"
+ self.email_sources[source.id()] = source
+ self.source_queue.append(source)
+ return None
+
+ def add_dir_source(self, source: KnowledgeDirSource):
+ if self.dir_sources.get(source.id()) is not None:
+ logging.info(f"knowledge add source {source.id()} failed for already exists")
+ return "already exists"
+ logging.info(f"knowledge added source {source.id()}")
+ self.dir_sources[source.id()] = source
+ self.source_queue.append(source)
+ return None
+
+ def get_latest_journals(self, topn) -> [KnowledgeJournal]:
+ return KnowledgeJournalClient().latest_journals(topn)
+
+ async def run_loop(self):
+ while True:
+ await self.run_once()
+ await asyncio.sleep(5)
+
+ async def run_once(self):
+ logging.info(f"knowledge pipeline started")
+ # sources = list()
+ # async with self.run_lock:
+ # for source in self.source_queue:
+ # sources.append(source)
+ # for source in sources:
+ # await source.run_once()
+ for source in self.source_queue:
+ await source.run_once()
+
+ logging.info(f"knowledge pipeline finished")
\ No newline at end of file
diff --git a/src/aios_kernel/local_llama_compute_node.py b/src/aios_kernel/local_llama_compute_node.py
new file mode 100644
index 0000000..7f8ad0a
--- /dev/null
+++ b/src/aios_kernel/local_llama_compute_node.py
@@ -0,0 +1,171 @@
+
+import json
+import logging
+import requests
+from typing import Optional, List
+from pydantic import BaseModel
+
+from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskResultCode, ComputeTaskState, ComputeTaskType
+from .queue_compute_node import Queue_ComputeNode
+from .storage import AIStorage,UserConfig
+
+logger = logging.getLogger(__name__)
+
+"""
+This is a custom implementation, it should be redesigned.
+"""
+
+class LocalLlama_ComputeNode(Queue_ComputeNode):
+ def __init__(self, url: str, model_name: str):
+ super().__init__()
+ self.url = url
+ self.model_name = model_name
+
+ async def execute_task(self, task: ComputeTask)->ComputeTaskResult:
+ result = ComputeTaskResult()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.set_from_task(task)
+ result.worker_id = self.node_id
+ match task.task_type:
+ case ComputeTaskType.TEXT_EMBEDDING:
+ model_name = task.params["model_name"]
+ input = task.params["input"]
+ logger.info(f"call local-llama ({self.url}, {self.model_name}) {model_name} input: {input}")
+
+ self.embedding(input, result)
+
+ if result.result_code == ComputeTaskResultCode.OK:
+ task.state = ComputeTaskState.DONE
+ else:
+ task.state = ComputeTaskState.ERROR
+ task.error_str = result.error_str
+
+ return result
+ case ComputeTaskType.LLM_COMPLETION:
+ mode_name = task.params["model_name"]
+ prompts = task.params["prompts"]
+
+ logger.info(f"local-llama({self.url}, {self.model_name}) prompts: {prompts}")
+
+ self.completion(task, result)
+
+ if result.result_code == ComputeTaskResultCode.OK:
+ task.state = ComputeTaskState.DONE
+ else:
+ task.state = ComputeTaskState.ERROR
+ task.error_str = result.error_str
+
+ case _:
+ task.state = ComputeTaskState.ERROR
+ result.result_code = ComputeTaskResultCode.ERROR
+ task.error_str = f"ComputeTask's TaskType : {task.task_type} not support!"
+ result.error_str = f"ComputeTask's TaskType : {task.task_type} not support!"
+ return result
+
+ return result
+
+ async def initial(self) -> bool:
+ return True
+
+ def display(self) -> str:
+ return f"local-llama: {self.node_id}"
+
+ def get_capacity(self):
+ pass
+
+ def is_support(self, task: ComputeTask) -> bool:
+ return (task.task_type == ComputeTaskType.TEXT_EMBEDDING or task.task_type == ComputeTaskType.LLM_COMPLETION) and (not task.params["model_name"] or task.params["model_name"] == self.model_name)
+
+ def is_local(self) -> bool:
+ return True
+
+ def embedding(self, input: str, result: ComputeTaskResult):
+ body = {
+ "input": input
+ }
+
+ try:
+ response = requests.post(self.url + "/v1/embeddings", json = body, verify=False, headers={"Content-Type": "application/json"})
+ response.close()
+
+ logger.info(f"local-llama({self.url}, {self.model_name}) task responsed, request: {body}, status-code: {response.status_code}, headers: {response.headers}, content: {response.content}")
+
+ if response.status_code == 200:
+ resp = response.json()
+ result.result = resp["data"][0]["embedding"]
+ elif response.status_code == 422:
+ resp = response.json()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.error_str = "http request failed: " + str(resp["detail"][0]["msg"])
+ else:
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.error_str = "http request failed: " + str(response.status_code)
+ except Exception as e:
+ logger.error(f"call local-llama({self.url}, {self.model_name}) run TEXT_EMBEDDING task error: {e}")
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.error_str = str(e)
+ return result
+
+ def completion(self, task: ComputeTask, result: ComputeTaskResult):
+ mode_name = task.params["model_name"]
+ prompts = task.params["prompts"]
+ max_token_size = task.params.get("max_token_size")
+ llm_inner_functions = task.params.get("inner_functions")
+ if max_token_size is None:
+ max_token_size = max_token_size
+
+ body = {
+ "messages": [],
+ "max_tokens": 4000
+ }
+
+ for prompt in prompts:
+ body["messages"].append({
+ "role": prompt["role"],
+ "content": prompt["content"]
+ })
+
+ try:
+ response = requests.post(self.url + "/v1/chat/completions", json = body, verify=False, headers={"Content-Type": "application/json"})
+ response.close()
+
+ logger.info(f"local-llama({self.url}, {self.model_name}) task responsed, request: {body}, status-code: {response.status_code}, headers: {response.headers}, content: {response.content}")
+
+ if response.status_code == 200:
+ resp = response.json()
+
+ status_code = resp["choices"][0]["finish_reason"]
+ token_usage = resp["usage"]
+
+ match status_code:
+ case "function_call":
+ task.state = ComputeTaskState.DONE
+ case "stop":
+ task.state = ComputeTaskState.DONE
+ case _:
+ task.state = ComputeTaskState.ERROR
+ task.error_str = f"The status code was {status_code}."
+ result.error_str = f"The status code was {status_code}."
+ result.result_code = ComputeTaskResultCode.ERROR
+ return None
+
+ result.result_code = ComputeTaskResultCode.OK
+ result.result_str = resp["choices"][0]["message"]["content"]
+ result.result["message"] = resp["choices"][0]["message"]
+
+ if token_usage:
+ result.result_refers["token_usage"] = token_usage
+
+ logger.info(f"local-llama({self.url}, {self.model_name}) success response: {result.result_str}")
+ elif response.status_code == 422:
+ resp = response.json()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.error_str = "http request failed: " + str(resp["detail"][0]["msg"])
+ else:
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.error_str = "http request failed: " + str(response.status_code)
+ except Exception as e:
+ logger.error(f"call local-llama({self.url}, {self.model_name}) run LLM_COMPLETION task error: {e}")
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.error_str = str(e)
+ return result
\ No newline at end of file
diff --git a/src/aios_kernel/local_st_compute_node.py b/src/aios_kernel/local_st_compute_node.py
new file mode 100644
index 0000000..4142d5a
--- /dev/null
+++ b/src/aios_kernel/local_st_compute_node.py
@@ -0,0 +1,213 @@
+import logging
+import requests
+from typing import Optional, List
+from pydantic import BaseModel
+from typing import Union
+from PIL import Image
+import io
+from .compute_task import ComputeTask, ComputeTaskState, ComputeTaskType,ComputeTaskResult,ComputeTaskResultCode
+from .queue_compute_node import Queue_ComputeNode
+from knowledge import ObjectID
+
+logger = logging.getLogger(__name__)
+
+class LocalSentenceTransformer_Text_ComputeNode(Queue_ComputeNode):
+ # For valid pretrained models, see https://www.sbert.net/docs/pretrained_models.html
+ def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
+ super().__init__()
+
+ self.node_id = "local_sentence_transformer_text_embedding_node"
+ self.model_name = model_name
+ self.model = None
+
+ def initial(self) -> bool:
+ logger.info(
+ f"LocalSentenceTransformer_Text_ComputeNode init, model_name: {self.model_name}"
+ )
+
+ assert self.model_name is not None
+ assert self.model is None
+ try:
+ from sentence_transformers import SentenceTransformer
+
+ self.model = SentenceTransformer(self.model_name)
+ except Exception as err:
+ logger.error(f"load model {self.model} failed: {err}")
+ return False
+ self.start()
+ return True
+
+ async def execute_task(self, task: ComputeTask) :
+ result = ComputeTaskResult()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.set_from_task(task)
+ result.worker_id = self.node_id
+ try:
+ # logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task: {task}")
+ if task.task_type == ComputeTaskType.TEXT_EMBEDDING:
+ input = task.params["input"]
+ logger.debug(
+ f"LocalSentenceTransformer_Text_ComputeNode task input: {input}"
+ )
+ sentence_embeddings = self.model.encode(input, show_progress_bar=False).tolist()
+ # logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task sentence_embeddings: {sentence_embeddings}")
+ result.result_code = ComputeTaskResultCode.OK
+ result.result["content"] = sentence_embeddings
+
+ else:
+ result.error_str = f"unsupport embedding task type: {task.task_type}"
+ except Exception as err:
+ import traceback
+
+ logger.error(f"{traceback.format_exc()}, error: {err}")
+ result.error_str = f"{traceback.format_exc()}, error: {err}"
+
+ return result
+
+
+ def display(self) -> str:
+ return f"LocalSentenceTransformer_Text_ComputeNode: {self.node_id}, {self.model_name}"
+
+ def get_capacity(self):
+ pass
+
+ def is_support(self, task: ComputeTask) -> bool:
+ return task.task_type == ComputeTaskType.TEXT_EMBEDDING and task.params["model_name"] == "all-MiniLM-L6-v2"
+
+ def is_local(self) -> bool:
+ return True
+
+
+class LocalSentenceTransformer_Image_ComputeNode(Queue_ComputeNode):
+ # For valid pretrained models, see https://www.sbert.net/docs/pretrained_models.html
+ def __init__(
+ self,
+ model_name: str = "clip-ViT-B-32",
+ multi_model_name: str = "clip-ViT-B-32-multilingual-v1",
+ ):
+ super().__init__()
+
+ self.node_id = "local_sentence_transformer_image_embedding_node"
+ self.model_name = model_name
+ self.multi_model_name = multi_model_name
+ self.model = None
+ self.multi_model = None
+
+ def initial(self) -> bool:
+ logger.info(
+ f"LocalSentenceTransformer_Image_ComputeNode init, model_name: {self.model_name} {self.multi_model_name}"
+ )
+
+ assert self.model_name is not None
+ assert self.multi_model_name is not None
+ assert self.model is None
+ assert self.multi_model is None
+
+ try:
+ from sentence_transformers import SentenceTransformer
+
+ self.model = SentenceTransformer(self.model_name)
+ self.multi_model = SentenceTransformer(self.multi_model_name)
+ except Exception as err:
+ logger.error(f"load model {self.model} failed: {err}")
+ return False
+ self.start()
+ return True
+
+ def _load_image(self, source: Union[ObjectID, bytes]) -> Optional[Image]:
+ image_data = None
+ if isinstance(source, ObjectID):
+ from knowledge import KnowledgeStore, ImageObject
+
+ buf = KnowledgeStore().get_object_store().get_object(source)
+ if buf is None:
+ logger.error(f"load image object but not found! {source}")
+ return None
+
+ try:
+ image_obj = ImageObject.decode(buf)
+ except Exception as err:
+ logger.error(f"decode ImageObject from buffer failed: {source}, {err}")
+ return None
+
+ file_size = image_obj.get_file_size()
+ # print(f"got image object: {source.to_base58()}, size: {file_size}")
+
+ image_data = (
+ KnowledgeStore()
+ .get_chunk_reader()
+ .read_chunk_list_to_single_bytes(image_obj.get_chunk_list())
+ )
+
+ elif isinstance(source, bytes):
+ image_data = source
+ else:
+ logger.error(f"unsupport image source type: {type(source)}, {source}")
+ return None
+
+ try:
+ img = Image.open(io.BytesIO(image_data))
+
+ return img
+ except Exception as err:
+ logger.error(f"load image from buffer failed: {source}, {err}")
+ return None
+
+ async def execute_task(
+ self, task: ComputeTask
+ ) -> ComputeTaskResult:
+ result = ComputeTaskResult()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.set_from_task(task)
+ result.worker_id = self.node_id
+ try:
+ # logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task: {task}")
+ if task.task_type == ComputeTaskType.TEXT_EMBEDDING:
+ input = task.params["input"]
+ logger.debug(
+ f"LocalSentenceTransformer_Text_ComputeNode task text input: {input}"
+ )
+ sentence_embeddings = self.multi_model.encode(input, show_progress_bar=False).tolist()
+ # logger.debug(f"LocalSentenceTransformer_Text_ComputeNode task sentence_embeddings: {sentence_embeddings}")
+ result.result_code = ComputeTaskResultCode.OK
+ result.result["content"] = sentence_embeddings
+
+ elif task.task_type == ComputeTaskType.IMAGE_EMBEDDING:
+ input = task.params["input"]
+ logger.debug(
+ f"LocalSentenceTransformer_Image_ComputeNode task image input: {input}"
+ )
+
+ img = self._load_image(input)
+ if img is None:
+ result.error_str = f"load image failed: {input}"
+ return result
+
+ sentence_embeddings = self.model.encode(img, show_progress_bar=False).tolist()
+ result.result_code = ComputeTaskResultCode.OK
+ result.result["content"] = sentence_embeddings
+ else:
+ result.error_str = f"unsupport embedding task type: {task.task_type}"
+ except Exception as err:
+ import traceback
+
+ logger.error(f"{traceback.format_exc()}, error: {err}")
+ result.error_str = f"{traceback.format_exc()}, error: {err}"
+
+
+ return result
+
+ def display(self) -> str:
+ return f"LocalSentenceTransformer_Image_ComputeNode: {self.node_id}, {self.model_name}"
+
+ def get_capacity(self):
+ pass
+
+ def is_support(self, task: ComputeTask) -> bool:
+ return (
+ (task.task_type == ComputeTaskType.TEXT_EMBEDDING and task.params["model_name"] == "clip-ViT-B-32")
+ or task.task_type == ComputeTaskType.IMAGE_EMBEDDING
+ )
+
+ def is_local(self) -> bool:
+ return True
diff --git a/src/aios_kernel/local_stability_node.py b/src/aios_kernel/local_stability_node.py
new file mode 100644
index 0000000..d0d7f9d
--- /dev/null
+++ b/src/aios_kernel/local_stability_node.py
@@ -0,0 +1,205 @@
+import os
+import io
+import asyncio
+from asyncio import Queue
+import logging
+import base64
+from PIL import Image
+import requests
+from typing import Tuple
+from pathlib import Path
+
+from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType, ComputeTaskResultCode
+from .compute_node import ComputeNode
+from .storage import AIStorage, UserConfig
+
+logger = logging.getLogger(__name__)
+
+
+class Local_Stability_ComputeNode(ComputeNode):
+ _instance = None
+
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = Local_Stability_ComputeNode()
+ return cls._instance
+
+ @classmethod
+ def declare_user_config(cls):
+ user_config = AIStorage.get_instance().get_user_config()
+ if os.getenv("LOCAL_STABILITY_URL") is None:
+ user_config.add_user_config(
+ "local_stability_url", "local stability url", True, None)
+ if os.getenv("TEXT2IMG_OUTPUT_DIR") is None:
+ home_dir = Path.home()
+ output_dir = Path.joinpath(home_dir, "text2img_output")
+ Path.mkdir(output_dir, exist_ok=True)
+ user_config.add_user_config(
+ "text2img_output_dir", "text2image output dir", True, output_dir)
+ if os.getenv("TEXT2IMG_DEFAULT_MODEL") is None:
+ user_config.add_user_config(
+ "text2img_default_model", "text2img default model", True, "v1-5-pruned-emaonly")
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ self.is_start = False
+ self.node_id = "local_stability_node"
+ self.url = None
+ self.default_model = None
+ self.output_dir = None
+
+ self.task_queue = Queue()
+
+ async def initial(self):
+ if os.getenv("LOCAL_STABILITY_URL") is not None:
+ self.url = os.getenv("LOCAL_STABILITY_URL")
+ else:
+ self.url = AIStorage.get_instance(
+ ).get_user_config().get_value("local_stability_url")
+
+ if os.getenv("TEXT2IMG_OUTPUT_DIR") is not None:
+ self.output_dir = os.getenv("TEXT2IMG_OUTPUT_DIR")
+ else:
+ self.output_dir = AIStorage.get_instance(
+ ).get_user_config().get_value("text2img_output_dir")
+
+ if os.getenv("TEXT2IMG_DEFAULT_MODEL") is not None:
+ self.default_model = os.getenv("TEXT2IMG_DEFAULT_MODEL")
+ else:
+ self.default_model = AIStorage.get_instance(
+ ).get_user_config().get_value("text2img_default_model")
+
+ if self.url is None:
+ logger.error("local stability url is None!")
+ return False
+
+ if self.default_model is None:
+ logger.error("local stability default model is None!")
+ return False
+
+ if self.output_dir is None:
+ self.output_dir = "./"
+
+ self.output_dir = os.path.abspath(self.output_dir)
+
+ self.start()
+
+ return True
+
+ async def push_task(self, task: ComputeTask, proiority: int = 0):
+ logger.info(f"stability_node push task: {task.display()}")
+ self.task_queue.put_nowait(task)
+
+ async def remove_task(self, task_id: str):
+ pass
+
+ def _make_post_request(self, url, json) -> Tuple[str, requests.Response]:
+ try:
+ response = requests.post(url, json=json)
+ if response.status_code != 200:
+ return f'{response.status_code}, {response.json()}', None
+ return None, response
+ except Exception as e:
+ return f"{e}", None
+
+
+ def _run_task(self, task: ComputeTask):
+ task.state = ComputeTaskState.RUNNING
+ result = ComputeTaskResult()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.set_from_task(task)
+
+ model_name = task.params["model_name"]
+ prompt = task.params["prompt"]
+ negative_prompt = task.params["negative_prompt"]
+ if negative_prompt == None or negative_prompt == "":
+ negative_prompt = "sketches, (worst quality:2), (low quality:2), (normal quality:2), lowres, duplicate, mutated hands, mutated legs, (blurry:1.3), (bad anatomy:1.2), bad proportions, extra limbs, more than 2 nipples, extra legs, fused fingers, missing fingers, jpeg artifacts, signature, watermark, username, artist name, heterochromia, muscular legs, monochrome, grayscale, skin spots, acnes, skin blemishes, age spot, skin spots, acnes, logo, badhandv4, easynegative, cropped image, patreon,lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, ng_deepnegative_v1_75t, lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry,(Tiptoe:1.3),looking at viewer, Twisted eyes"
+
+ prompt += ",masterpiece, best quality:1.3"
+
+ logging.info(f"call local stability {model_name} prompts: {prompt}, nagative_prompt: {negative_prompt}")
+
+ if model_name is not None:
+ payload = {
+ "sd_model_checkpoint": model_name,
+ }
+ err, resp = self._make_post_request(f'{self.url}/sdapi/v1/options', payload)
+
+ if err is not None:
+ task.state = ComputeTaskState.ERROR
+ err_msg = f"Set local stability model failed. err:{err}"
+ logger.error(err_msg)
+ task.error_str = err_msg
+ result.error_str = err_msg
+ return result
+
+ logging.info(f"set local stability model {model_name} success")
+
+ payload = {
+ "prompt": prompt,
+ "negative_prompt": negative_prompt,
+ "steps": 20
+ }
+
+ err, resp = self._make_post_request(f'{self.url}/sdapi/v1/txt2img', payload)
+ if err is not None:
+ task.state = ComputeTaskState.ERROR
+ err_msg = f"Failed. err:{err}"
+ logger.error(err_msg)
+ task.error_str = err_msg
+ result.error_str = err_msg
+ return result
+
+ r = resp.json()
+
+ for i in r['images']:
+ image = Image.open(io.BytesIO(
+ base64.b64decode(i.split(",", 1)[0])))
+ file_name = os.path.join(self.output_dir, task.task_id + ".png")
+ image.save(file_name)
+
+ task.state = ComputeTaskState.DONE
+ result.result_code = ComputeTaskResultCode.OK
+ result.worker_id = self.node_id
+ result.result = {"file": file_name}
+
+ return result
+
+ task.error_str = "Unknown error!"
+ result.error_str = "Unknown error!"
+ task.state = ComputeTaskState.ERROR
+ return result
+
+ def start(self):
+ if self.is_start:
+ return
+ self.is_start = True
+
+ async def _run_task_loop():
+ while True:
+ logger.info("local_stability_node is waiting for task...")
+ task = await self.task_queue.get()
+ logger.info(f"stability_node get task: {task.display()}")
+ result = self._run_task(task)
+ # if result is not None:
+ # task.state = ComputeTaskState.DONE
+ # task.result = result
+
+ asyncio.create_task(_run_task_loop())
+
+ def display(self) -> str:
+ return f"Stability_ComputeNode: {self.node_id}"
+
+ def get_task_state(self, task_id: str):
+ pass
+
+ def get_capacity(self):
+ pass
+
+ def is_support(self, task: ComputeTask) -> bool:
+ return task.task_type == ComputeTaskType.TEXT_2_IMAGE
+
+ def is_local(self) -> bool:
+ return False
diff --git a/src/aios_kernel/open_ai_node.py b/src/aios_kernel/open_ai_node.py
new file mode 100644
index 0000000..de68656
--- /dev/null
+++ b/src/aios_kernel/open_ai_node.py
@@ -0,0 +1,206 @@
+import openai
+import os
+import asyncio
+from asyncio import Queue
+import logging
+import json
+
+from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType,ComputeTaskResultCode
+from .compute_node import ComputeNode
+from .storage import AIStorage,UserConfig
+
+logger = logging.getLogger(__name__)
+
+
+class OpenAI_ComputeNode(ComputeNode):
+ _instance = None
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = OpenAI_ComputeNode()
+ return cls._instance
+
+ @classmethod
+ def declare_user_config(cls):
+ if os.getenv("OPENAI_API_KEY_") is None:
+ user_config = AIStorage.get_instance().get_user_config()
+ user_config.add_user_config("openai_api_key","openai api key",False,None)
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ self.is_start = False
+ # openai.organization = "org-AoKrOtF2myemvfiFfnsSU8rF" #buckycloud
+ self.openai_api_key = None
+ self.node_id = "openai_node"
+ self.task_queue = Queue()
+
+
+ async def initial(self):
+ if os.getenv("OPENAI_API_KEY") is not None:
+ self.openai_api_key = os.getenv("OPENAI_API_KEY")
+ else:
+ self.openai_api_key = AIStorage.get_instance().get_user_config().get_value("openai_api_key")
+
+ if self.openai_api_key is None:
+ logger.error("openai_api_key is None!")
+ return False
+
+ openai.api_key = self.openai_api_key
+ self.start()
+ return True
+
+ async def push_task(self, task: ComputeTask, proiority: int = 0):
+ logger.info(f"openai_node push task: {task.display()}")
+ self.task_queue.put_nowait(task)
+
+ async def remove_task(self, task_id: str):
+ pass
+
+ def _run_task(self, task: ComputeTask):
+ task.state = ComputeTaskState.RUNNING
+
+ result = ComputeTaskResult()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.set_from_task(task)
+
+ match task.task_type:
+ case ComputeTaskType.TEXT_EMBEDDING:
+ model_name = task.params["model_name"]
+ input = task.params["input"]
+ logger.info(f"call openai {model_name} input: {input}")
+ try:
+ resp = openai.Embedding.create(model=model_name,
+ input=input)
+ except Exception as e:
+ logger.error(f"openai run TEXT_EMBEDDING task error: {e}")
+ task.state = ComputeTaskState.ERROR
+ task.error_str = str(e)
+ result.error_str = str(e)
+ return result
+
+ # resp = {
+ # "object": "list",
+ # "data": [
+ # {
+ # "object": "embedding",
+ # "index": 0,
+ # "embedding": [
+ # -0.00930514745414257,
+ # 0.00765434792265296,
+ # -0.007167573552578688,
+ # -0.012373941019177437,
+ # -0.04884673282504082
+ # ]}]
+ # }
+
+ logger.info(f"openai response: {resp}")
+ task.state = ComputeTaskState.DONE
+ result.result_code = ComputeTaskResultCode.OK
+ result.worker_id = self.node_id
+ result.result_str = resp["data"][0]["embedding"]
+
+ return result
+ case ComputeTaskType.LLM_COMPLETION:
+ mode_name = task.params["model_name"]
+ prompts = task.params["prompts"]
+ max_token_size = task.params.get("max_token_size")
+ llm_inner_functions = task.params.get("inner_functions")
+ if max_token_size is None:
+ max_token_size = 4000
+
+ result_token = max_token_size
+ try:
+ if llm_inner_functions is None:
+ logger.info(f"call openai {mode_name} prompts: {prompts}")
+ resp = openai.ChatCompletion.create(model=mode_name,
+ messages=prompts,
+ #max_tokens=result_token,
+ temperature=0.7)
+ else:
+ logger.info(f"call openai {mode_name} prompts: {prompts} functions: {json.dumps(llm_inner_functions)}")
+ resp = openai.ChatCompletion.create(model=mode_name,
+ messages=prompts,
+ functions=llm_inner_functions,
+ #max_tokens=result_token,
+ temperature=0.7) # TODO: add temperature to task params?
+ except Exception as e:
+ logger.error(f"openai run LLM_COMPLETION task error: {e}")
+ task.state = ComputeTaskState.ERROR
+ task.error_str = str(e)
+ result.error_str = str(e)
+ return result
+
+ logger.info(f"openai response: {json.dumps(resp, indent=4)}")
+
+ status_code = resp["choices"][0]["finish_reason"]
+ token_usage = resp.get("usage")
+ match status_code:
+ case "function_call":
+ task.state = ComputeTaskState.DONE
+ case "stop":
+ task.state = ComputeTaskState.DONE
+ case _:
+ task.state = ComputeTaskState.ERROR
+ task.error_str = f"The status code was {status_code}."
+ result.error_str = f"The status code was {status_code}."
+ result.result_code = ComputeTaskResultCode.ERROR
+ return result
+
+ result.result_code = ComputeTaskResultCode.OK
+ result.worker_id = self.node_id
+ result.result_str = resp["choices"][0]["message"]["content"]
+ result.result["message"] = resp["choices"][0]["message"]
+
+ if token_usage:
+ result.result_refers["token_usage"] = token_usage
+ logger.info(f"openai success response: {result.result_str}")
+ return result
+ case _:
+ task.state = ComputeTaskState.ERROR
+ task.error_str = f"ComputeTask's TaskType : {task.task_type} not support!"
+ result.error_str = f"ComputeTask's TaskType : {task.task_type} not support!"
+ return None
+
+ def start(self):
+ if self.is_start is True:
+ return
+ self.is_start = True
+
+ async def _run_task_loop():
+ while True:
+ task = await self.task_queue.get()
+ logger.info(f"openai_node get task: {task.display()}")
+ result = self._run_task(task)
+ if result is not None:
+ task.state = ComputeTaskState.DONE
+ task.result = result
+
+ asyncio.create_task(_run_task_loop())
+
+ def display(self) -> str:
+ return f"OpenAI_ComputeNode: {self.node_id}"
+
+ def get_task_state(self, task_id: str):
+ pass
+
+ def get_capacity(self):
+ pass
+
+
+ def is_support(self, task: ComputeTask) -> bool:
+ if task.task_type == ComputeTaskType.LLM_COMPLETION:
+ if not task.params["model_name"]:
+ return True
+ model_name : str = task.params["model_name"]
+ if model_name.startswith("gpt-"):
+ return True
+
+ #if task.task_type == ComputeTaskType.TEXT_EMBEDDING:
+ # if task.params["model_name"] == "text-embedding-ada-002":
+ # return True
+ return False
+
+
+ def is_local(self) -> bool:
+ return False
diff --git a/src/aios_kernel/queue_compute_node.py b/src/aios_kernel/queue_compute_node.py
new file mode 100644
index 0000000..4eb22f4
--- /dev/null
+++ b/src/aios_kernel/queue_compute_node.py
@@ -0,0 +1,64 @@
+
+import asyncio
+from asyncio import Queue
+import logging
+from abc import abstractmethod
+
+from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskResultCode, ComputeTaskState, ComputeTaskType
+from .compute_node import ComputeNode
+
+logger = logging.getLogger(__name__)
+
+class Queue_ComputeNode(ComputeNode):
+ def __init__(self):
+ super().__init__()
+ self.task_queue = Queue()
+ self.is_start = False
+
+ @abstractmethod
+ async def execute_task(self, task: ComputeTask)->ComputeTaskResult:
+ pass
+
+ async def push_task(self, task: ComputeTask, proiority: int = 0):
+ logger.info(f"{self.display()} push task: {task.display()}")
+ self.task_queue.put_nowait(task)
+
+ async def remove_task(self, task_id: str):
+ pass
+
+ async def _run_task(self, task: ComputeTask):
+ task.state = ComputeTaskState.RUNNING
+
+ result = ComputeTaskResult()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.set_from_task(task)
+ result.worker_id = self.node_id
+
+ real_result = await self.execute_task(task)
+
+ if real_result:
+ if real_result.result_code == ComputeTaskResultCode.OK:
+ task.state = ComputeTaskState.DONE
+ else:
+ task.state = ComputeTaskState.ERROR
+ return real_result
+ else:
+ task.state = ComputeTaskState.ERROR
+ return result
+
+ def start(self):
+ if self.is_start is True:
+ return
+ self.is_start = True
+
+ async def _run_task_loop():
+ while True:
+ task = await self.task_queue.get()
+ logger.info(f"openai_node get task: {task.display()}")
+ await self._run_task(task)
+
+ asyncio.create_task(_run_task_loop())
+
+
+ def get_task_state(self, task_id: str):
+ pass
diff --git a/src/aios_kernel/role.py b/src/aios_kernel/role.py
new file mode 100644
index 0000000..83d64d6
--- /dev/null
+++ b/src/aios_kernel/role.py
@@ -0,0 +1,81 @@
+import logging
+
+from .agent import AIAgent,AgentPrompt
+
+class AIRole:
+ def __init__(self) -> None:
+ self.agent_instance_id : str = None
+ self.role_name : str = None
+ self.role_id :str = None # $workflow_id.$sub_workflow_id.$role_name
+ self.fullname : str = None
+ self.agent_name : str = None
+ self.prompt : AgentPrompt = None
+ self.introduce : str = None
+ self.agent = None
+ self.enable_function_list : list[str] = None
+ self.history_len = 10
+
+ def load_from_config(self,config:dict) -> bool:
+ name_node = config.get("name")
+ if name_node is None:
+ logging.error("role name is not found!")
+ return False
+ self.role_name = name_node
+
+
+ agent_id_node = config.get("agent")
+ if agent_id_node is None:
+ logging.error("agent id is not found!")
+ return False
+ self.agent_name = agent_id_node
+
+ prompt_node = config.get("prompt")
+ if prompt_node:
+ self.prompt = AgentPrompt()
+ if self.prompt.load_from_config(prompt_node) is False:
+ logging.error("load prompt failed!")
+ return False
+
+ intro_node = config.get("intro")
+ if intro_node is not None:
+ self.introduce = intro_node
+
+ history_node = config.get("history_len")
+ if history_node:
+ self.history_len = int(history_node)
+
+ if config.get("enable_function") is not None:
+ self.enable_function_list = config["enable_function"]
+
+ def get_role_id(self) -> str:
+ return self.role_id
+
+ def get_intro(self) -> str:
+ return self.introduce
+
+ def get_name(self) -> str:
+ return self.role_name
+
+ def get_prompt(self) -> AgentPrompt:
+ return self.prompt
+
+class AIRoleGroup:
+ def __init__(self) -> None:
+ self.roles : dict[str,AIRole] = {}
+ self.owner_name : str = None
+
+ def load_from_config(self,config:dict) -> bool:
+ for k,v in config.items():
+ role = AIRole()
+ if role.load_from_config(v) is False:
+ logging.error(f"load role {k} failed!")
+ return False
+ role.role_id = self.owner_name + "." + k
+ self.roles[k] = role
+
+ return True
+
+ def get(self,role_name:str) -> AIRole:
+ return self.roles.get(role_name)
+
+
\ No newline at end of file
diff --git a/src/aios_kernel/stability_node.py b/src/aios_kernel/stability_node.py
new file mode 100644
index 0000000..2ea1f44
--- /dev/null
+++ b/src/aios_kernel/stability_node.py
@@ -0,0 +1,201 @@
+import os
+import io
+import asyncio
+from asyncio import Queue
+import logging
+from pathlib import Path
+
+from PIL import Image
+from stability_sdk import client
+import stability_sdk.interfaces.gooseai.generation.generation_pb2 as generation
+
+from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType, ComputeTaskResultCode
+from .compute_node import ComputeNode
+from .storage import AIStorage, UserConfig
+
+logger = logging.getLogger(__name__)
+
+
+class Stability_ComputeNode(ComputeNode):
+ _instance = None
+
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = Stability_ComputeNode()
+ return cls._instance
+
+ @classmethod
+ def declare_user_config(cls):
+ user_config = AIStorage.get_instance().get_user_config()
+ user_config.add_user_config(
+ "stability_api_key", "stability api key", False, None)
+ user_config.add_user_config(
+ "stability_model", "stability model name", True, "stable-diffusion-512-v2-1")
+ if os.getenv("TEXT2IMG_OUTPUT_DIR") is None:
+ home_dir = Path.home()
+ output_dir = Path.joinpath(home_dir, "text2img_output")
+ Path.mkdir(output_dir, exist_ok=True)
+ user_config.add_user_config(
+ "text2img_output_dir", "text2image output dir", True, output_dir)
+ if os.getenv("STABILITY_DEFAULT_MODEL") is None:
+ user_config.add_user_config(
+ "stability_default_model", "stability default model", True, "stable-diffusion-512-v2-1")
+
+ def __init__(self):
+ super().__init__()
+
+ self.is_start = False
+ self.node_id = "stability_node"
+ self.api_key = ""
+ self.default_model = ""
+
+ self.task_queue = Queue()
+
+ async def initial(self):
+ if os.getenv("STABILITY_API_KEY") is not None:
+ self.api_key = os.getenv("STABILITY_API_KEY")
+ else:
+ self.api_key = AIStorage.get_instance(
+ ).get_user_config().get_value("stability_api_key")
+
+ if self.api_key is None:
+ logger.error("stability api key is None!")
+ return False
+
+ # Check out the following link for a list of available engines: https://platform.stability.ai/docs/features/api-parameters#engine
+ if os.getenv("STABILITY_DEFAULT_MODEL") is not None:
+ self.default_model = os.getenv("STABILITY_DEFAULT_MODEL")
+ else:
+ self.default_model = AIStorage.get_instance().get_user_config().get_value("stability_default_model")
+
+ if self.default_model is None:
+ self.default_model = "stable-diffusion-512-v2-1"
+
+ if os.getenv("TEXT2IMG_OUTPUT_DIR") is not None:
+ self.output_dir = os.getenv("TEXT2IMG_OUTPUT_DIR")
+ else:
+ self.output_dir = AIStorage.get_instance(
+ ).get_user_config().get_value("text2img_output_dir")
+
+ if self.output_dir is None:
+ self.output_dir = "./"
+ self.output_dir = os.path.abspath(self.output_dir)
+
+ self.start()
+
+ return True
+
+ async def push_task(self, task: ComputeTask, proiority: int = 0):
+ logger.info(f"stability_node push task: {task.display()}")
+ self.task_queue.put_nowait(task)
+
+ async def remove_task(self, task_id: str):
+ pass
+
+ def _run_task(self, task: ComputeTask):
+ task.state = ComputeTaskState.RUNNING
+ result = ComputeTaskResult()
+ result.result_code = ComputeTaskResultCode.ERROR
+ result.set_from_task(task)
+
+ model_name = task.params["model_name"]
+ prompt = task.params["prompt"]
+ negative_prompt = task.params["negative_prompt"]
+
+ logging.info(f"call stability {self.default_model} prompts: {prompt}, negative_prompt: {negative_prompt}")
+
+ api = None
+ try:
+ api = client.StabilityInference(
+ key=self.api_key,
+ verbose=True, # Print debug messages.
+ engine=model_name,
+ )
+ except Exception as e:
+ task.error_str = f"create stability client failed: {e}"
+ result.error_str = f"create stability client failed: {e}"
+ logging.warn(task.error_str)
+ task.state = ComputeTaskState.ERROR
+ return result
+
+ answers = api.generate(
+ prompt=prompt,
+ # If a seed is provided, the resulting generated image will be deterministic.
+ seed=0,
+ # What this means is that as long as all generation parameters remain the same, you can always recall the same image simply by generating it again.
+ # Note: This isn't quite the case for Clip Guided generations, which we'll tackle in a future example notebook.
+ # Amount of inference steps performed on image generation. Defaults to 30.
+ steps=30,
+ # Influences how strongly your generation is guided to match your prompt.
+ cfg_scale=7.0,
+ # Setting this value higher increases the strength in which it tries to match your prompt.
+ # Defaults to 7.0 if not specified.
+ width=512, # Generation width, defaults to 512 if not included.
+ height=512, # Generation height, defaults to 512 if not included.
+ # Number of images to generate, defaults to 1 if not included.
+ samples=1,
+ # Choose which sampler we want to denoise our generation with.
+ sampler=generation.SAMPLER_K_DPMPP_2M
+ # Defaults to k_dpmpp_2m if not specified. Clip Guidance only supports ancestral samplers.
+ # (Available Samplers: ddim, plms, k_euler, k_euler_ancestral, k_heun, k_dpm_2, k_dpm_2_ancestral, k_dpmpp_2s_ancestral, k_lms, k_dpmpp_2m, k_dpmpp_sde)
+ )
+
+ for resp in answers:
+ for artifact in resp.artifacts:
+ if artifact.finish_reason == generation.FILTER:
+ err_msg = "request activated the API's safety filters"
+ logging.warn(err_msg)
+ task.error_str = err_msg
+ result.error_str = err_msg
+ task.state = ComputeTaskState.ERROR
+ return result
+ if artifact.type == generation.ARTIFACT_IMAGE:
+ img = Image.open(io.BytesIO(artifact.binary))
+ # Save our generated images with the task_id as the filename.
+ file_name = os.path.join(self.output_dir, task.task_id + ".png")
+ img.save(file_name)
+
+ task.state = ComputeTaskState.DONE
+ result.result_code = ComputeTaskResultCode.OK
+ result.worker_id = self.node_id
+ result.result = {"file": file_name}
+
+ return result
+
+ task.error_str = "Unknown error!"
+ result.error_str = "Unknown error!"
+ task.state = ComputeTaskState.ERROR
+ return result
+
+ def start(self):
+ if self.is_start:
+ return
+ self.is_start = True
+
+ async def _run_task_loop():
+ while True:
+ logger.info("stability_node is waiting for task...")
+ task = await self.task_queue.get()
+ logger.info(f"stability_node get task: {task.display()}")
+ result = self._run_task(task)
+ # if result is not None:
+ # task.state = ComputeTaskState.DONE
+ # task.result = result
+
+ asyncio.create_task(_run_task_loop())
+
+ def display(self) -> str:
+ return f"Stability_ComputeNode: {self.node_id}"
+
+ def get_task_state(self, task_id: str):
+ pass
+
+ def get_capacity(self):
+ pass
+
+ def is_support(self, task: ComputeTask) -> bool:
+ return task.task_type == ComputeTaskType.TEXT_2_IMAGE
+
+ def is_local(self) -> bool:
+ return False
diff --git a/src/aios_kernel/storage.py b/src/aios_kernel/storage.py
new file mode 100644
index 0000000..2d83af6
--- /dev/null
+++ b/src/aios_kernel/storage.py
@@ -0,0 +1,235 @@
+from typing import Any
+from pathlib import Path
+import os
+import logging
+import toml
+import aiofiles
+
+logger = logging.getLogger(__name__)
+
+_file_dir = os.path.dirname(__file__)
+
+class ResourceLocation:
+ def __init__(self) -> None:
+ pass
+
+class FeatureItem:
+ def __init__(self) -> None:
+ pass
+
+class UserConfigItem:
+ def __init__(self,desc:str=None) -> None:
+ self.default_value = None
+ self.is_optional = False
+ self.item_type = "str"
+ self.desc = desc
+ self.value = None
+ self.user_set = False
+
+ def clone(self):
+ new_config_item = UserConfigItem()
+ new_config_item.default_value = self.default_value
+ new_config_item.is_optional = self.is_optional
+ new_config_item.desc = self.desc
+ new_config_item.item_type = self.item_type
+ new_config_item.value = self.value
+ return new_config_item
+
+class UserConfig:
+ def __init__(self) -> None:
+ self.config_table = {}
+ self.user_config_path:str = None
+
+
+ def add_user_config(self,key:str,desc:str,is_optional:bool,default_value:Any=None,item_type="str") -> None:
+ if self.config_table.get(key) is not None:
+ logger.warning("user config key %s already exist, will be overrided",key)
+
+ new_config_item = UserConfigItem()
+ new_config_item.default_value = default_value
+ new_config_item.is_optional = is_optional
+ new_config_item.desc = desc
+ new_config_item.item_type = item_type
+ self.config_table[key] = new_config_item
+
+ async def load_value_from_file(self,file_path:str,is_user_config = False) -> None:
+ try:
+ all_config = toml.load(file_path)
+ if all_config is not None:
+ for key,value in all_config.items():
+ config_item = self.config_table.get(key)
+ if config_item is None:
+ logger.warning("user config key %s not exist",key)
+ continue
+ config_item.value = value
+ config_item.user_set = is_user_config
+
+ except Exception as e:
+ logger.warn(f"load user config from {file_path} failed!")
+
+
+ async def save_to_user_config(self) -> None:
+ will_save_config = {}
+ for key,value in self.config_table.items():
+ if value.user_set:
+ will_save_config[key] = value.value
+
+ if len(will_save_config) > 0:
+ try:
+ directory = os.path.dirname(self.user_config_path)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ async with aiofiles.open(self.user_config_path,"w") as f:
+ toml_str = toml.dumps(will_save_config)
+ await f.write(toml_str)
+ except Exception as e:
+ logger.error(f"save user config to {self.user_config_path} failed!")
+ return False
+
+ return True
+
+ def get_config_item(self,key:str) -> Any:
+ config_item = self.config_table.get(key)
+ if config_item is None:
+ logger.warning(f"user config key {key} not exist")
+ return None
+
+ return config_item
+
+ def get_value(self,key:str)->Any:
+ config_item = self.config_table.get(key)
+ if config_item is None:
+ logger.warning(f"user config key {key} not exist")
+ return None
+
+ if config_item.value is None:
+ return config_item.default_value
+
+ return config_item.value
+
+ def set_value(self,key:str,value:Any) -> None:
+ config_item = self.config_table.get(key)
+ if config_item is None:
+ logger.warning("user config key %s not exist",key)
+ return
+
+ config_item.value = value
+ config_item.user_set = True
+ #TODO: save to file?
+
+
+ def check_config(self) -> None:
+ check_result = {}
+ for key,config_item in self.config_table.items():
+ if config_item.value is None and not config_item.is_optional:
+ check_result[key] = config_item
+
+ if len(check_result) > 0:
+ return check_result
+ else:
+ return None
+
+# storage sytem for current user
+class AIStorage:
+ _instance = None
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = AIStorage()
+ return cls._instance
+
+ def __init__(self) -> None:
+ self.is_dev_mode = False
+ self.user_config = UserConfig()
+ self.feature_init_results = {}
+
+ async def initial(self)->bool:
+ self.user_config.user_config_path = str(self.get_myai_dir() / "etc/system.cfg.toml")
+ await self.user_config.load_value_from_file(self.get_system_dir() + "/system.cfg.toml")
+ await self.user_config.load_value_from_file(self.user_config.user_config_path,True)
+
+ async def enable_feature(self,feature_name:str) -> None:
+ self.user_config.set_value(f"feature.{feature_name}","True")
+ await self.user_config.save_to_user_config()
+
+
+ async def disable_feature(self,feature_name:str) -> None:
+ self.user_config.set_value(f"feature.{feature_name}","False")
+ await self.user_config.save_to_user_config()
+
+ async def set_feature_init_result(self,feature_name:str,result:bool) -> None:
+ self.feature_init_results[feature_name] = result
+
+ async def is_feature_enable(self,feature_name:str) -> bool:
+ is_enable = self.user_config.get_value(f"feature.{feature_name}")
+ if is_enable is None:
+ return False
+
+ init_result = self.feature_init_results.get(feature_name)
+ if init_result:
+ if init_result is False:
+ return False
+
+ if is_enable == "True":
+ return True
+ return False
+
+ def get_user_config(self) -> UserConfig:
+ return self.user_config
+
+ def get_system_dir(self) -> str:
+ """
+ system dir is dir for aios system
+ /opt/aios
+ """
+ if self.is_dev_mode:
+ return os.path.abspath(_file_dir + "/../")
+ else:
+ return "/opt/aios/"
+
+
+ def get_system_app_dir(self)->str:
+ """
+ system app dir is the dir for aios build-in app
+ /opt/aios/app
+ """
+ if self.is_dev_mode:
+ return os.path.abspath(_file_dir + "/../../rootfs/")
+ else:
+ return "/opt/aios/app/"
+
+ def get_myai_dir(self) -> str:
+ """
+ my ai dir is the dir for user to store their ai app and data
+ ~/myai/
+ """
+ return Path.home() / "myai"
+
+ def get_db(self,app_name:str)->ResourceLocation:
+ pass
+
+ def open_file(self,file_path:str,options:dict):
+ pass
+
+ def get_named_object(self,name:str) -> Any:
+ pass
+
+ def put_named_object(self,name:str,obj:Any) -> None:
+ pass
+
+ async def try_create_file_with_default_value(self,path:str,default_value:str):
+ if os.path.exists(path):
+ return None
+
+ try:
+ directory = os.path.dirname(path)
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ async with aiofiles.open(path,"w") as f:
+ await f.write(default_value)
+
+ except Exception as e:
+ logger.error(f"open or create file {path} failed! {str(e)}")
+
+
diff --git a/src/aios_kernel/text_to_speech_function.py b/src/aios_kernel/text_to_speech_function.py
new file mode 100644
index 0000000..90dfd49
--- /dev/null
+++ b/src/aios_kernel/text_to_speech_function.py
@@ -0,0 +1,101 @@
+import io
+import logging
+import os
+import random
+from typing import Dict
+
+from aios_kernel import ComputeKernel
+from aios_kernel.ai_function import AIFunction
+
+from pydub import AudioSegment
+
+logger = logging.getLogger(__name__)
+
+class TextToSpeechFunction(AIFunction):
+ def __init__(self):
+ self.func_id = "text_to_speech"
+ self.description = "根据输入的剧本生成音频文件,成功时会返回音频文件路径"
+
+ def get_name(self) -> str:
+ return self.func_id
+
+ def get_description(self) -> str:
+ return self.description
+
+ def get_parameters(self) -> Dict:
+ return {
+ "type": "object",
+ "properties": {
+ "language": {"type": "string", "description": "演播语言", "enum": ["zh", "en"]},
+ "roles": {"type": "array", "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "角色名字"},
+ "gender": {"type": "string", "description": "角色性别", "enum": ["man", "female"]},
+ "age": {"type": "string", "description": "年龄", "enum": ["child", "adult"]},
+ }}},
+ "lines": {"type": "array", "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "角色名字"},
+ "tone": {"type": "string", "description": "演播情感",
+ "enum": ["happy", "sad", "angry", "fear", "disgust", "surprise", "neutral"]},
+ "text": {"type": "string", "description": "台词"},
+ }
+ }}
+ }
+ }
+
+ async def execute(self, **kwargs) -> str:
+ logger.info(f"execute text_to_speech function: {kwargs}")
+
+ language = kwargs.get("language")
+ if language is None:
+ language = "zh"
+ roles = kwargs.get("roles")
+ lines = kwargs.get("lines")
+
+ audio = None
+ for line in lines:
+ name = line.get("name")
+ tone = line.get("tone")
+ text = line.get("text")
+ gender = None
+ age = None
+ for role in roles:
+ role_name = role.get("name")
+ if role_name == name:
+ gender = role.get("gender")
+ age = role.get("age")
+ break
+ i = 0
+ while i < 3:
+ try:
+ data = await ComputeKernel.get_instance().do_text_to_speech(text, language, gender, age, name, tone)
+ if audio is None:
+ audio = AudioSegment.from_mp3(io.BytesIO(data))
+ else:
+ audio = audio + AudioSegment.from_mp3(io.BytesIO(data))
+ break
+ except Exception as e:
+ logger.error(f"do_text_to_speech failed: {e}")
+ i += 1
+ continue
+
+ if audio is not None:
+ path = os.path.join(os.path.realpath(os.curdir), "{}.mp3".format(''.join(random.sample('zyxwvutsrqponmlkjihgfedcba', 10))))
+ audio.export(path, format="mp3")
+ return "exec text_to_speech OK,speech file store at {}".format(path)
+ else:
+ return "exec text_to_speech failed"
+
+ def is_local(self) -> bool:
+ return True
+
+ def is_in_zone(self) -> bool:
+ return True
+
+ def is_ready_only(self) -> bool:
+ return False
+
+
diff --git a/src/aios_kernel/tg_tunnel.py b/src/aios_kernel/tg_tunnel.py
new file mode 100644
index 0000000..53aed7a
--- /dev/null
+++ b/src/aios_kernel/tg_tunnel.py
@@ -0,0 +1,272 @@
+import logging
+import threading
+import asyncio
+import uuid
+import time
+import aiofiles
+
+from telegram import Update,Message
+from telegram import Bot
+from telegram.ext import Updater
+from telegram.error import Forbidden, NetworkError
+
+from knowledge.object.object_id import ObjectType
+
+from .knowledge_base import KnowledgeBase
+
+from .tunnel import AgentTunnel
+from .storage import AIStorage
+from .contact_manager import ContactManager,Contact,FamilyMember
+from .agent_message import AgentMsg,AgentMsgType
+
+
+logger = logging.getLogger(__name__)
+
+class TelegramTunnel(AgentTunnel):
+
+ @classmethod
+ def register_to_loader(cls):
+ async def load_tg_tunnel(config:dict) -> AgentTunnel:
+ result_tunnel = TelegramTunnel("")
+ if await result_tunnel.load_from_config(config):
+ return result_tunnel
+ else:
+ return None
+
+ AgentTunnel.register_loader("TelegramTunnel",load_tg_tunnel)
+
+
+ async def load_from_config(self,config:dict)->bool:
+ self.type = "TelegramTunnel"
+ self.tg_token = config["token"]
+ self.target_id = config["target"]
+ self.tunnel_id = config["tunnel_id"]
+ if config.get("allow") is not None:
+ self.allow_group = config["allow"]
+
+ return True
+
+ def dump_to_config(self) -> dict:
+ pass
+
+ def __init__(self,tg_token:str) -> None:
+ super().__init__()
+ self.is_start = False
+ self.tg_token = tg_token
+ self.bot:Bot = None
+ self.update_queue = None
+ self.allow_group = "contact"
+ self.in_process_tg_msg = {}
+
+ async def _do_process_raw_message(self,bot: Bot, update_id: int) -> int:
+ # Request updates after the last update_id
+ updates = await bot.get_updates(offset=update_id, timeout=10, allowed_updates=Update.ALL_TYPES)
+ for update in updates:
+ next_update_id = update.update_id + 1
+
+ if update.message and update.message.text:
+
+ await self.on_message(bot,update)
+ return next_update_id
+
+ return update_id
+
+ async def start(self) -> bool:
+ if self.is_start:
+ logger.warning(f"tunnel {self.tunnel_id} is already started")
+ return False
+ self.is_start = True
+ logger.info(f"tunnel {self.tunnel_id} is starting...")
+
+ self.bot = Bot(self.tg_token)
+ self.bot_username = (await self.bot.get_me()).username
+ self.update_queue = asyncio.Queue()
+ self.bot_updater = Updater(self.bot,update_queue=self.update_queue)
+
+
+ async def _run_app():
+ try:
+ update_id = (await self.bot.get_updates())[0].update_id
+ except IndexError:
+ update_id = None
+
+ #logger.info("listening for new messages...")
+ while True:
+ try:
+ update_id = await self._do_process_raw_message(self.bot, update_id)
+ except NetworkError:
+ await asyncio.sleep(1)
+ except Forbidden:
+ # The user has removed or blocked the bot.
+ update_id += 1
+ except Exception as e:
+ logger.error(f"tg_tunnel error:{e}")
+ await asyncio.sleep(1)
+
+
+
+ asyncio.create_task(_run_app())
+ logger.info(f"tunnel {self.tunnel_id} started.")
+ return True
+
+ async def close(self) -> None:
+ pass
+
+ async def _process_message(self, msg: AgentMsg) -> None:
+ logger.warn(f"process message {msg.msg_id} from {msg.sender} to {msg.target}")
+
+
+ async def conver_tg_msg_to_agent_msg(self,message:Message) -> AgentMsg:
+ agent_msg = AgentMsg()
+ agent_msg.topic = "_telegram"
+ agent_msg.msg_id = "tg_msg#" + str(message.message_id) + "#" + uuid.uuid4().hex
+ agent_msg.target = self.target_id
+ agent_msg.body = message.text
+ agent_msg.create_time = time.time()
+ messag_type = message.chat.type
+ if messag_type == "supergroup" or messag_type == "group":
+ agent_msg.target = f"tg_group{message.chat_id}"
+ agent_msg.msg_type = AgentMsgType.TYPE_GROUPMSG
+ agent_msg.mentions = []
+ else:
+ agent_msg.msg_type = AgentMsgType.TYPE_MSG
+
+ if message.entities:
+ for entity in message.entities:
+ if entity.type == 'mention':
+ mention = message.text[entity.offset:entity.offset+entity.length]
+ if mention == '@' + self.bot_username:
+ agent_msg.mentions.append(self.target_id)
+ else:
+ agent_msg.mentions.append(mention)
+
+ if message.caption_entities:
+ for entity in message.caption_entities:
+ if entity.type == 'mention':
+ mention = message.caption[entity.offset:entity.offset+entity.length]
+ if mention == '@' + self.bot_username:
+ agent_msg.mentions.append(self.target_id)
+ else:
+ agent_msg.mentions.append(mention)
+
+ return agent_msg
+
+ def is_bot_mentioned(self,message:Message):
+ if message.entities:
+ for entity in message.entities:
+ if entity.type == 'mention':
+ mention = message.text[entity.offset:entity.offset+entity.length]
+ if mention == '@' + self.bot_username:
+ return True
+
+ if message.caption_entities:
+ for entity in message.caption_entities:
+ if entity.type == 'mention':
+ mention = message.caption[entity.offset:entity.offset+entity.length]
+ if mention == '@' + self.bot_username:
+ return True
+
+ return False
+
+ async def on_message(self, bot:Bot, update: Update) -> None:
+ message = update.message
+ logger.info(f"on_message: {message.message_id} from {message.from_user.username} ({update.effective_user.username}) to {message.chat.title}({message.chat.id})")
+ if update.effective_user.is_bot:
+ logger.warning(f"ignore message from telegram bot {update.effective_user.id}")
+ return None
+
+ if self.in_process_tg_msg.get(update.message.message_id) is not None:
+ logger.warning(f"ignore message from telegram bot {update.effective_user.id}")
+ return None
+
+ self.in_process_tg_msg[update.message.message_id] = True
+
+ agent_msg = await self.conver_tg_msg_to_agent_msg(message)
+ cm : ContactManager = ContactManager.get_instance()
+ reomte_user_name = f"{update.effective_user.id}@telegram"
+
+ contact : Contact = cm.find_contact_by_telegram(update.effective_user.username)
+ if contact is None:
+ contact = cm.find_contact_by_telegram(str(update.effective_user.id))
+
+ if contact is not None:
+ reomte_user_name = contact.name
+ if not contact.is_family_member:
+ if self.allow_group != "contact" and self.allow_group !="guest":
+ await update.message.reply_text(f"You're not supposed to talk to me! Please contact my father~")
+ return
+
+ else:
+ if self.allow_group != "guest":
+ await update.message.reply_text(f"You're not supposed to talk to me! Please contact my father~")
+ return
+
+ if cm.is_auto_create_contact_from_telegram:
+ contact_name = update.effective_user.first_name
+ if update.effective_user.last_name is not None:
+ contact_name += " " + update.effective_user.last_name
+
+ contact = Contact(contact_name)
+ contact.telegram = update.effective_user.username if update.effective_user.username is not None else str(update.effective_user.id)
+ contact.added_by = self.target_id
+ cm.add_contact(contact.name, contact)
+ reomte_user_name = contact.name
+
+
+ agent_msg.sender = reomte_user_name
+ logger.info(f"process message {agent_msg.msg_id} from {agent_msg.sender} to {agent_msg.target}")
+ if agent_msg.msg_type == AgentMsgType.TYPE_GROUPMSG:
+ self.ai_bus.register_message_handler(agent_msg.target, self._process_message)
+ resp_msg = await self.ai_bus.send_message(agent_msg,self.target_id,agent_msg.target)
+ else:
+ self.ai_bus.register_message_handler(reomte_user_name, self._process_message)
+ resp_msg = await self.ai_bus.send_message(agent_msg)
+ #await bot.send_chat_action(chat_id=update.effective_chat.id, action="typing")
+
+
+
+ if resp_msg is None:
+ await update.message.reply_text(f"System Error: Timeout,{self.target_id} no resopnse! Please check logs/aios.log for more details!")
+ else:
+ if resp_msg.body_mime is None:
+ if resp_msg.body is None:
+ return
+
+ if len(resp_msg.body) < 1:
+ await update.message.reply_text("")
+ return
+
+ knowledge_object = KnowledgeBase().parse_object_in_message(resp_msg.body)
+ if knowledge_object is not None:
+ if knowledge_object.get_object_type() == ObjectType.Image:
+ image = KnowledgeBase().bytes_from_object(knowledge_object)
+ try:
+ async with aiofiles.open("tg_send_temp.png", mode='wb') as local_file:
+ if local_file:
+ await local_file.write(image)
+ await update.message.reply_photo("tg_send_temp.png")
+ except Exception as e:
+ logger.error(f"save image error: {e}")
+ return
+ else:
+ pos = resp_msg.body.find("audio file")
+ if pos != -1:
+ audio_file = resp_msg.body[pos+11:].strip()
+ if audio_file.startswith("\""):
+ audio_file = audio_file[1:-1]
+ await update.message.reply_voice(audio_file)
+ return
+ await update.message.reply_text(resp_msg.body)
+ else:
+ if resp_msg.body_mime.startswith("image"):
+ photo_file = open(resp_msg.body,"rb")
+ if photo_file:
+ await update.message.reply_photo(resp_msg.body)
+ photo_file.close()
+ else:
+ await update.message.reply_text(resp_msg.body)
+
+ else:
+ await update.message.reply_text(resp_msg.body)
+
+
diff --git a/src/aios_kernel/tunnel.py b/src/aios_kernel/tunnel.py
new file mode 100644
index 0000000..a3691cd
--- /dev/null
+++ b/src/aios_kernel/tunnel.py
@@ -0,0 +1,90 @@
+from abc import ABC, abstractmethod
+import logging
+from typing import Coroutine
+from .agent_message import AgentMsg
+from .bus import AIBus
+
+logger = logging.getLogger(__name__)
+
+class AgentTunnel(ABC):
+ _all_loader = {}
+ _all_tunnels = {}
+ @classmethod
+ def register_loader(cls,tunnel_type:str,loader:Coroutine) -> None:
+ cls._all_loader[tunnel_type] = loader
+
+
+ @classmethod
+ async def load_all_tunnels_from_config(cls,config:dict) -> None:
+ for tunnel_id,tunnel_config in config.items():
+ loader = cls._all_loader.get(tunnel_config["type"])
+ tid = tunnel_config.get("tunnel_id")
+ if tid is not None:
+ if tunnel_id != tid:
+ logger.warning(f"load tunnel {tunnel_id} error,{tunnel_id} != {tid} in config!")
+ continue
+ else:
+ tunnel_config["tunnel_id"] = tunnel_id
+
+ if loader is not None:
+ tunnel = await loader(tunnel_config)
+ if tunnel is not None:
+ cls._all_tunnels[tunnel_id] = tunnel
+ tunnel.connect_to(AIBus.get_default_bus(),tunnel.target_id)
+ await tunnel.start()
+ else:
+ logger.error(f"load tunnel {tunnel_id} failed")
+ else:
+ logger.error(f"load tunnel {tunnel_id} failed,loader not found")
+
+ @classmethod
+ async def load_tunnel_from_config(cls,tunnel_config:dict):
+ loader = cls._all_loader.get(tunnel_config["type"])
+ if loader is not None:
+ tunnel = await loader(tunnel_config)
+ if tunnel is not None:
+ cls._all_tunnels[tunnel.tunnel_id] = tunnel
+ tunnel.connect_to(AIBus.get_default_bus(),tunnel.target_id)
+ await tunnel.start()
+ return True
+ else:
+ logger.error(f"load tunnel {tunnel_config['tunnel_id']} failed")
+ else:
+ logger.error(f"load tunnel {tunnel_config['type']} failed,loader not found")
+
+ return False
+
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.tunnel_id = None
+ self.target_id = None
+ self.target_type = None
+ self.ai_bus = None
+ self.is_connected = False
+
+ def connect_to(self, ai_bus:AIBus,target_id: str) -> None:
+ """
+ Connect to the agent with the given id
+ """
+ if self.is_connected:
+ logger.warning(f"tunnel {self.tunnel_id} is already connected to {self.target_id}")
+ return
+ self.target_id = target_id
+ self.target_type = "agent"
+ self.ai_bus = ai_bus
+ self.is_connected = True
+
+
+
+ @abstractmethod
+ async def start(self) -> bool:
+ pass
+
+ @abstractmethod
+ async def close(self) -> None:
+ pass
+
+ @abstractmethod
+ async def _process_message(self, msg: AgentMsg) -> None:
+ pass
diff --git a/src/aios_kernel/whisper_node.py b/src/aios_kernel/whisper_node.py
new file mode 100644
index 0000000..b036ad5
--- /dev/null
+++ b/src/aios_kernel/whisper_node.py
@@ -0,0 +1,111 @@
+from asyncio import Queue
+import asyncio
+import openai
+import os
+import logging
+
+from .compute_node import ComputeNode
+from .compute_task import ComputeTask, ComputeTaskResult, ComputeTaskState, ComputeTaskType
+
+logger = logging.getLogger(__name__)
+
+
+class WhisperComputeNode(ComputeNode):
+ _instance = None
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ cls._instance.is_start = False
+ return cls._instance
+
+ def __init__(self) -> None:
+ super().__init__()
+ if self.is_start is True:
+ logger.warn("WhisperComputeNode is already start")
+ return
+
+ self.is_start = True
+ self.node_id = "whisper_node"
+ self.enable = True
+ self.task_queue = Queue()
+ self.open_api_key = None
+
+ if self.open_api_key is None and os.getenv("OPENAI_API_KEY") is not None:
+ self.open_api_key = os.getenv("OPENAI_API_KEY")
+
+ if self.open_api_key is None:
+ raise Exception("WhisperComputeNode open_api_key is None")
+
+ self.start()
+
+ def start(self):
+ async def _run_task_loop():
+ while True:
+ task = await self.task_queue.get()
+ try:
+ result = self._run_task(task)
+ if result is not None:
+ task.state = ComputeTaskState.DONE
+ task.result = result
+ except Exception as e:
+ logger.error(f"whisper_node run task error: {e}")
+ task.state = ComputeTaskState.ERROR
+ task.result = ComputeTaskResult()
+ task.result.set_from_task(task)
+ task.result.worker_id = self.node_id
+ task.result.result_str = str(e)
+
+ asyncio.create_task(_run_task_loop())
+
+ def _run_task(self, task: ComputeTask):
+ task.state = ComputeTaskState.RUNNING
+ prompt = task.params["prompt"]
+ response_format = None
+ if "response_format" in task.params:
+ response_format = task.params["response_format"]
+ temperature = None
+ if "temperature" in task.params:
+ temperature = task.params["temperature"]
+ language = None
+ if "language" in task.params:
+ language = task.params["language"]
+ file = task.params["file"]
+
+ resp = openai.Audio.transcribe("whisper-1",
+ file,
+ self.open_api_key,
+ prompt=prompt,
+ response_format=response_format,
+ temperature=temperature,
+ language=language)
+ result = ComputeTaskResult()
+ result.set_from_task(task)
+ result.worker_id = self.node_id
+ result.result_str = resp["text"]
+ result.result = resp
+ return result
+
+ async def push_task(self, task: ComputeTask, proiority: int = 0):
+ logger.info(f"whisper_node push task: {task.display()}")
+ self.task_queue.put_nowait(task)
+
+ async def remove_task(self, task_id: str):
+ pass
+
+ def get_task_state(self, task_id: str):
+ pass
+
+ def display(self) -> str:
+ return f"WhisperComputeNode: {self.node_id}"
+
+ def get_capacity(self):
+ return 0
+
+ def is_support(self, task_type: ComputeTaskType) -> bool:
+ if task_type == ComputeTaskType.VOICE_2_TEXT:
+ return True
+ return False
+
+ def is_local(self) -> bool:
+ return False
diff --git a/src/aios_kernel/workflow.py b/src/aios_kernel/workflow.py
new file mode 100644
index 0000000..a25e202
--- /dev/null
+++ b/src/aios_kernel/workflow.py
@@ -0,0 +1,587 @@
+import logging
+import asyncio
+import json
+import os
+import time
+from asyncio import Queue
+from typing import Optional,Tuple,List
+from abc import ABC, abstractmethod
+
+from .environment import Environment,EnvironmentEvent
+from .agent_message import AgentMsg,AgentMsgStatus,FunctionItem,LLMResult
+from .agent import AgentPrompt,AgentMsg
+from .chatsession import AIChatSession
+from .role import AIRole,AIRoleGroup
+from .ai_function import AIFunction,FunctionItem
+from .compute_kernel import ComputeKernel
+from .compute_task import ComputeTask,ComputeTaskResult,ComputeTaskState,ComputeTaskResultCode
+from .bus import AIBus
+from .workflow_env import WorkflowEnvironment
+
+
+logger = logging.getLogger(__name__)
+
+class MessageFilter:
+ def __init__(self) -> None:
+ self.filters = {}
+
+ def select(self,msg:AgentMsg) -> str:
+ star_target = self.filters.get("*")
+ if star_target is not None:
+ return star_target
+
+ # TODO: add more filter
+ return None
+
+ def load_from_config(self,config:dict) -> bool:
+ self.filters = config
+ return True
+
+
+
+
+
+class Workflow:
+ def __init__(self) -> None:
+ self.workflow_name : str = None
+ self.workflow_id : str = None
+ self.rule_prompt : AgentPrompt = None
+ self.workflow_config = None
+ self.role_group : dict = None
+ self.input_filter : MessageFilter= None
+ self.connected_environment = {}
+ self.sub_workflows = {}
+ self.owner_workflow = None
+ self.db_file = None
+ self.env_db_file = None
+ self.workflow_env:WorkflowEnvironment = None
+
+ self.is_start = False
+ self.msg_queue = Queue()
+
+ def get_bus(self) -> AIBus:
+ return AIBus.get_default_bus()
+
+ def set_owner(self,owner):
+ self.owner_workflow = owner
+
+ def load_from_config(self,config:dict) -> bool:
+ if config is None:
+ return False
+
+ if config.get("name") is None:
+ logger.error("workflow config must have name")
+ return False
+ self.workflow_name = config.get("name")
+ if self.owner_workflow is None:
+ self.workflow_id = self.workflow_name
+ else:
+ self.workflow_id = self.owner_workflow.workflow_id + "." + self.workflow_name
+ self.db_file = self.owner_workflow.db_file
+
+ if config.get("prompt") is not None:
+ self.rule_prompt = AgentPrompt()
+ if self.rule_prompt.load_from_config(config.get("prompt")) is False:
+ logger.error("Workflow load prompt failed")
+ return False
+
+ if config.get("roles") is None:
+ logger.error("workflow config must have roles")
+ return False
+ self.role_group = AIRoleGroup()
+ self.role_group.owner_name = self.workflow_id
+ if self.role_group.load_from_config(config.get("roles")) is False:
+ logger.error("Workflow load role_group failed")
+ return False
+
+ if config.get("filter") is not None:
+ self.input_filter = MessageFilter()
+ if self.input_filter.load_from_config(config.get("filter")) is False:
+ logger.error("Workflow load input_filter failed")
+ return False
+
+ if self.owner_workflow is None:
+ self.env_db_file = os.path.dirname(self.db_file) + "/" + self.workflow_id + "_env.db"
+ else:
+ self.env_db_file = self.owner_workflow.env_db_file
+ self.workflow_env = WorkflowEnvironment(self.workflow_id,self.env_db_file)
+
+ env_ndoe = config.get("enviroment")
+ if env_ndoe is not None:
+ if self._load_env_from_config(env_ndoe) is False:
+ logger.error("Workflow load env failed")
+ return False
+
+ connected_env_ndoe = config.get("connected_env")
+ if connected_env_ndoe is not None:
+ for _node in connected_env_ndoe:
+ env_id = _node.get("env_id")
+ if env_id is None:
+ continue
+
+ remote_env = Environment.get_env_by_id(env_id)
+ if remote_env is None:
+ logger.error(f"Workflow load connected_env failed, env {env_id} not found!")
+ return False
+ self.connect_to_environment(remote_env,_node.get("event2msg"))
+
+ sub_workflows = config.get("sub_workflows")
+ if sub_workflows is not None:
+ if self._load_sub_workflows(sub_workflows) is False:
+ logger.error("Workflow load sub workflows failed")
+ return False
+
+ return True
+
+ def _load_env_from_config(self,config:dict) -> bool:
+ for k,v in config.items():
+ self.workflow_env.set_value(k,v,False)
+
+ def _load_sub_workflows(self,config:dict) -> bool:
+ for k,v in config.items():
+ sub_workflow = Workflow()
+ sub_workflow.set_owner(self)
+
+ if sub_workflow.load_from_config(v) is False:
+ logger.error(f"load sub workflow {k} failed!")
+ return False
+ self.sub_workflows[k] = sub_workflow
+ return True
+
+ def _parse_msg_target(self,s:str)->list[str]:
+ return s.split(".")
+
+ async def _forword_msg(self,inner_obj_id,msg):
+ i : int = 1
+ current_workflow = self
+ while i < len(inner_obj_id):
+ if i == len(inner_obj_id) - 1:
+ the_role : AIRole = current_workflow.role_group.get(inner_obj_id[i])
+ current_workflow_chatsession = AIChatSession.get_session(current_workflow.workflow_id,msg.sender + "#" + msg.topic,current_workflow.db_file)
+ if the_role is not None:
+ return await current_workflow.role_process_msg(msg,the_role,current_workflow_chatsession)
+ sub_workflow = current_workflow.sub_workflows.get(inner_obj_id[i])
+ if sub_workflow is not None:
+ return await sub_workflow._process_msg(msg)
+ logger.error(f"{msg.target} not found! forword message failed!")
+ return None
+ else:
+ current_workflow = current_workflow.sub_workflows.get(inner_obj_id[i])
+ if current_workflow is None:
+ logger.error(f"sub workflow {inner_obj_id[i]} not found!")
+ return None
+
+ i += 1
+
+ logger.error(f"{msg.target} not found! forword message failed!")
+ return None
+
+ def get_workflow_id_from_target(self,target:str) -> str:
+ target_list = target.split(".")
+ if len(target_list) == 0:
+ return target
+ else:
+ result_str = ""
+ p = 0
+ for s in target_list:
+ p = p + 1
+ result_str += s
+ if p < len(target_list)-1:
+ result_str += "."
+ else:
+ return result_str
+
+ async def _process_msg(self,msg:AgentMsg) -> AgentMsg:
+ real_target = msg.target.split(".")[0]
+ targets = self._parse_msg_target(msg.target)
+ if len(targets) > 1:
+ return await self._forword_msg(targets,msg)
+
+ #0 we don't support workflow join a group right now, this cloud be a feture in future
+ if msg.mentions is not None:
+ logger.warn(f"workflow {self.workflow_id} recv a group chat message,not support ignore!")
+ error_resp = msg.create_error_resp(f"workflow {self.workflow_id} recv a group chat message,not support ignore!")
+ return error_resp
+
+ #1. workflow start process message
+ # this is workflow's group_chat session
+ session_topic = msg.sender + "#" + msg.topic
+ chatsesssion = AIChatSession.get_session(self.workflow_id,session_topic,self.db_file)
+
+ #2. find role by msg.mentions or workflow's selector logic
+ if msg.mentions is not None:
+ if not self.workflow_id in msg.mentions:
+ chatsesssion.append(msg)
+ logger.info(f"workflow {self.workflow_id} recv a group chat message from {msg.sender},but is not mentioned,ignore!")
+ return None
+
+ for mention in msg.mentions:
+ this_role = self.role_group.get(mention)
+ if this_role is not None:
+ return await self.role_process_msg(msg,this_role,chatsesssion)
+
+ if self.input_filter is not None:
+ select_role_id = self.input_filter.select(msg)
+ if select_role_id is not None:
+ select_role = self.role_group.get(select_role_id)
+ if select_role is None:
+ logger.error(f"input_filter return invalid role id:{select_role_id}, role not found in role_group")
+ return None
+
+ return await self.role_process_msg(msg,select_role,chatsesssion)
+ else:
+ logger.error(f"input_filter return None for :{msg.body}")
+ return None
+
+ err_str = f"{self.workflow_id}:no role can process this msg:{msg.body}"
+ logger.error(err_str)
+ error_resp = msg.create_error_resp(err_str)
+ return error_resp
+
+ @classmethod
+ def prase_llm_result(cls,llm_result_str:str)->LLMResult:
+ r = LLMResult()
+ if llm_result_str is None:
+ r.state = "ignore"
+ return r
+ if llm_result_str == "ignore":
+ r.state = "ignore"
+ return r
+
+ lines = llm_result_str.splitlines()
+ is_need_wait = False
+
+ def check_args(func_item:FunctionItem):
+ match func_name:
+ case "send_msg":# sendmsg($target_id,$msg_content)
+ if len(func_args) != 1:
+ logger.error(f"parse sendmsg failed! {func_call}")
+ return False
+ new_msg = AgentMsg()
+ target_id = func_item.args[0]
+ msg_content = func_item.body
+ new_msg.set("_",target_id,msg_content)
+
+ r.send_msgs.append(new_msg)
+ is_need_wait = True
+
+ case "post_msg":# postmsg($target_id,$msg_content)
+ if len(func_args) != 1:
+ logger.error(f"parse postmsg failed! {func_call}")
+ return False
+ new_msg = AgentMsg()
+ target_id = func_item.args[0]
+ msg_content = func_item.body
+ new_msg.set("_",target_id,msg_content)
+ r.post_msgs.append(new_msg)
+
+ case "call":# call($func_name,$args_str)
+ r.calls.append(func_item)
+ is_need_wait = True
+ return True
+ case "post_call": # post_call($func_name,$args_str)
+ r.post_calls.append(func_item)
+ return True
+
+ current_func : FunctionItem = None
+ for line in lines:
+ if line.startswith("##/"):
+ if current_func:
+ if check_args(current_func) is False:
+ r.resp += current_func.dumps()
+
+ func_name,func_args = AgentMsg.parse_function_call(line[3:])
+ current_func = FunctionItem(func_name,func_args)
+ else:
+ if current_func:
+ current_func.append_body(line + "\n")
+ else:
+ r.resp += line + "\n"
+
+ if current_func:
+ if check_args(current_func) is False:
+ r.resp += current_func.dumps()
+
+ if len(r.send_msgs) > 0 or len(r.calls) > 0:
+ r.state = "waiting"
+ else:
+ r.state = "reponsed"
+
+ return r
+
+ async def role_post_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession):
+ msg.sender = the_role.get_role_id()
+
+ target_role = self.role_group.get(msg.target)
+ if target_role:
+ msg.target = target_role.get_role_id()
+ logger.info(f"{msg.sender} post message {msg.msg_id} to inner role: {msg.target}")
+ asyncio.create_task(self.role_process_msg(msg,target_role,workflow_chat_session))
+ return
+
+ target_workflow = self.sub_workflows.get(msg.target)
+ if target_workflow:
+ msg.target = target_workflow.workflow_id
+ logger.info(f"{msg.sender} post message {msg.msg_id} to sub workflow: {msg.target}")
+ asyncio.create_task(target_workflow._process_msg(msg))
+
+ logger.info(f"{msg.sender} post message {msg.msg_id} to AIBus: {msg.target}")
+ await self.get_bus().post_message(msg,msg.target)
+ return
+
+
+ async def role_send_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession):
+ msg.sender = the_role.get_role_id()
+ target_role = self.role_group.get(msg.target)
+ if target_role:
+ # msg.target = target_role.get_role_id()
+ logger.info(f"{msg.sender} send message {msg.msg_id} to inner role: {msg.target}")
+ return await self.role_process_msg(msg,target_role,workflow_chat_session)
+
+ target_workflow = self.sub_workflows.get(msg.target)
+ if target_workflow:
+ # msg.target = target_workflow.workflow_id
+ logger.info(f"{msg.sender} send message {msg.msg_id} to sub workflow: {msg.target}")
+ return await target_workflow._process_msg(msg)
+
+ logger.info(f"{msg.sender} post message {msg.msg_id} to AIBus: {msg.target}")
+ return await self.get_bus().send_message(msg)
+
+ async def role_call(self,func_item:FunctionItem,the_role:AIRole):
+ logger.info(f"{the_role.role_id} call {func_item.name} ")
+ arguments = func_item.args
+
+ func_node : AIFunction = self.workflow_env.get_ai_function(func_item.name)
+ if func_node is None:
+ return "execute failed,function not found"
+
+ result_str:str = await func_node.execute(**arguments)
+ return result_str
+
+ async def role_post_call(self,func_item:FunctionItem,the_role:AIRole):
+ logger.info(f"{the_role.role_id} post call {func_item.name} ")
+ return await self.role_call(func_item,the_role)
+
+ def _format_msg_by_env_value(self,prompt:AgentPrompt):
+ if self.workflow_env is None:
+ return
+
+ for msg in prompt.messages:
+ old_content = msg.get("content")
+ msg["content"] = old_content.format_map(self.workflow_env)
+
+ def _get_inner_functions(self,the_role:AIRole) -> dict:
+ all_inner_function = self.workflow_env.get_all_ai_functions()
+ if all_inner_function is None:
+ return None
+
+ result_func = []
+ for inner_func in all_inner_function:
+ func_name = inner_func.get_name()
+ if the_role.enable_function_list is not None:
+ if len(the_role.enable_function_list) > 0:
+ if func_name not in the_role.enable_function_list:
+ logger.debug(f"agent {self.agent_id} ignore inner func:{func_name}")
+ continue
+ else:
+ continue
+ this_func = {}
+ this_func["name"] = func_name
+ this_func["description"] = inner_func.get_description()
+ this_func["parameters"] = inner_func.get_parameters()
+ result_func.append(this_func)
+ if len(result_func) > 0:
+ return result_func
+ return None
+
+ async def _role_execute_func(self,the_role:AIRole,inenr_func_call_node:dict,prompt:AgentPrompt,org_msg:AgentMsg,stack_limit = 5) -> [str,int]:
+ from .compute_kernel import ComputeKernel
+
+ func_name = inenr_func_call_node.get("name")
+ arguments = json.loads(inenr_func_call_node.get("arguments"))
+ ineternal_call_record = AgentMsg.create_internal_call_msg(func_name,arguments,org_msg.get_msg_id(),org_msg.target)
+ func_node : AIFunction = self.workflow_env.get_ai_function(func_name)
+ result_str : str = ""
+ if func_node is None:
+ result_str = f"execute {func_name} failed,function not found"
+ else:
+ try:
+ result_str = await func_node.execute(**arguments)
+ except Exception as e:
+ result_str = f"execute {func_name} error:{str(e)}"
+ logger.error(f"llm execute inner func:{func_name} error:{e}")
+
+
+ inner_functions = self._get_inner_functions(the_role)
+ prompt.messages.append({"role":"function","content":result_str,"name":func_name})
+ task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,
+ the_role.agent.llm_model_name,the_role.agent.max_token_size,
+ inner_functions)
+ if task_result.result_code != ComputeTaskResultCode.OK:
+ logger.error(f"llm compute error:{task_result.error_str}")
+ return task_result.error_str,1
+
+ ineternal_call_record.result_str = task_result.result_str
+ ineternal_call_record.done_time = time.time()
+ org_msg.inner_call_chain.append(ineternal_call_record)
+ if stack_limit > 0:
+ result_message = task_result.result.get("message")
+ if result_message:
+ inner_func_call_node = result_message.get("function_call")
+
+ if inner_func_call_node:
+ return await self._role_execute_func(the_role,inner_func_call_node,prompt,org_msg,stack_limit-1)
+ else:
+ return task_result.result_str,0
+
+ def _is_in_same_workflow(self,msg) -> bool:
+ pass
+
+ async def role_process_msg(self,msg:AgentMsg,the_role:AIRole,workflow_chat_session:AIChatSession) -> AgentMsg:
+ msg.target = the_role.get_role_id()
+
+
+ prompt = AgentPrompt()
+ prompt.append(the_role.agent.prompt)
+ prompt.append(self.get_workflow_rule_prompt())
+ prompt.append(the_role.get_prompt())
+ # prompt.append(self._get_function_prompt(the_role.get_name()))
+ # prompt.append(self._get_knowlege_prompt(the_role.get_name()))
+
+ #support group chat, user content include sender name!
+ prompt.append(await self._get_prompt_from_session(the_role,workflow_chat_session))
+
+ msg_prompt = AgentPrompt()
+ msg_prompt.messages = [{"role":"user","content":f"user name is {msg.sender}, his question is :{msg.body}"}]
+ prompt.append(msg_prompt)
+
+ self._format_msg_by_env_value(prompt)
+ inner_functions = self._get_inner_functions(the_role)
+
+ async def _do_process_msg():
+ #TODO: send msg to agent might be better?
+ task_result:ComputeTaskResult = await ComputeKernel.get_instance().do_llm_completion(prompt,the_role.agent.get_llm_model_name(),the_role.agent.get_max_token_size(),inner_functions)
+ if task_result.result_code != ComputeTaskResultCode.OK:
+ logger.error(f"llm compute error:{task_result.error_str}")
+ error_resp = msg.create_error_resp(task_result.error_str)
+ return error_resp
+
+ result_str = task_result.result_str
+ logger.info(f"{the_role.role_id} process {msg.sender}:{msg.body},llm str is :{result_str}")
+
+ result_message = task_result.result.get("message")
+ if result_message:
+ inner_func_call_node = result_message.get("function_call")
+
+ if inner_func_call_node:
+ #TODO to save more token ,can i use msg_prompt?
+ result_str,r_code = await self._role_execute_func(the_role,inner_func_call_node,prompt,msg)
+ if r_code != 0:
+ error_resp = msg.create_error_resp(result_str)
+ return error_resp
+
+ result : LLMResult = Workflow.prase_llm_result(result_str)
+ for postmsg in result.post_msgs:
+ postmsg.prev_msg_id = msg.get_msg_id()
+ # might be craete a new msg.topic for this postmsg
+ postmsg.topic = msg.topic
+
+ await self.role_post_msg(postmsg,the_role,workflow_chat_session)
+ if not self._is_in_same_workflow(postmsg):
+ role_sesion = AIChatSession.get_session(the_role.get_role_id(),f"{postmsg.target}#{msg.topic}",self.db_file)
+ role_sesion.append(postmsg)
+ else:
+ # message will be saved in role.process_message
+ pass
+
+
+ for post_call in result.post_calls:
+ action_msg = msg.create_action_msg(post_call[0],post_call[1],the_role.get_role_id())
+ workflow_chat_session.append(action_msg)
+ await self.role_post_call(post_call,the_role)
+ #save post_call
+
+ result_prompt_str = ""
+ match result.state:
+ case "ignore":
+ return None
+ case "reponsed":
+ resp_msg = msg.create_resp_msg(result.resp)
+ resp_msg.sender = the_role.get_role_id()
+ # It is always the person handling the messages who puts them into the session.
+ workflow_chat_session.append(msg)
+ workflow_chat_session.append(resp_msg)
+ #await self.get_bus().resp_message(resp_msg)
+ return resp_msg
+ case "waiting":
+ for sendmsg in result.send_msgs:
+ target = sendmsg.target
+ sendmsg.topic = msg.topic
+ sendmsg.prev_msg_id = msg.get_msg_id()
+ send_resp = await self.role_send_msg(sendmsg,the_role,workflow_chat_session)
+ if send_resp is not None:
+ result_prompt_str += f"\n# {target} response is : \n{send_resp.body}"
+
+ if not self._is_in_same_workflow(sendmsg):
+ role_sesion = AIChatSession.get_session(the_role.get_role_id(),f"{sendmsg.target}#{sendmsg.topic}",self.db_file)
+ role_sesion.append(sendmsg)
+ role_sesion.append(send_resp)
+ else:
+ # message will be saved in role.process_message
+ pass
+
+ this_llm_resp_prompt = AgentPrompt()
+ this_llm_resp_prompt.messages = [{"role":"assistant","content":result_str}]
+ prompt.append(this_llm_resp_prompt)
+
+ result_prompt = AgentPrompt()
+ result_prompt.messages = [{"role":"user","content":result_prompt_str}]
+ prompt.append(result_prompt)
+ return await _do_process_msg()
+
+ return await _do_process_msg()
+
+ async def _get_prompt_from_session(self,the_role:AIRole,chatsession:AIChatSession) -> AgentPrompt:
+ messages = chatsession.read_history(the_role.history_len) # read last 10 message
+ result_prompt = AgentPrompt()
+ for msg in reversed(messages):
+ if msg.sender == chatsession.owner_id:
+ result_prompt.messages.append({"role":"assistant","content":msg.body})
+ else:
+ result_prompt.messages.append({"role":"user","content":f"{msg.body}"})
+
+ return result_prompt
+
+ def _get_knowlege_prompt(self,role_name:str) -> AgentPrompt:
+ pass
+
+ def get_workflow_rule_prompt(self) -> AgentPrompt:
+ return self.rule_prompt
+
+ def _env_event_to_msg(self,env_event:EnvironmentEvent) -> AgentMsg:
+ pass
+
+ def get_inner_environment(self,env_id:str) -> Environment:
+ pass
+
+ def connect_to_environment(self,the_env:Environment,conn_info:dict) -> None:
+ if the_env is not None:
+ self.workflow_env.add_owner_env(the_env)
+
+ #for event2msg in conn_info:
+ # for k,v in event2msg:
+ # if k == "role":
+ # continue
+ # else:
+ #
+ # def _env_msg_handler(env_event:EnvironmentEvent) -> None:
+ # the_msg:AgentMsg= self._env_event_to_msg(env_event)
+ # self.role_post_msg
+
+ # the_env.attach_event_handler(k,_env_msg_handler)
+ # break
+
+
+
+
+
diff --git a/src/aios_kernel/workflow_env.py b/src/aios_kernel/workflow_env.py
new file mode 100644
index 0000000..c62fc45
--- /dev/null
+++ b/src/aios_kernel/workflow_env.py
@@ -0,0 +1,427 @@
+
+from datetime import datetime
+import asyncio
+import json
+import sqlite3 # Because sqlite3 IO operation is small, so we can use sqlite3 directly.(so we don't need to use async sqlite3 now)
+from sqlite3 import Error
+import threading
+import logging
+from typing import Optional
+
+from .text_to_speech_function import TextToSpeechFunction
+from .compute_kernel import ComputeKernel, ComputeTaskResultCode
+from .environment import Environment,EnvironmentEvent
+from .ai_function import SimpleAIFunction
+from .storage import AIStorage
+from .contact_manager import ContactManager,Contact,FamilyMember
+
+import aiosqlite
+
+logger = logging.getLogger(__name__)
+
+
+class CalenderEvent(EnvironmentEvent):
+ def __init__(self,data) -> None:
+ super().__init__()
+ self.event_name = "timer"
+ self.data = data
+
+ def display(self) -> str:
+ return f"#event timer:{self.data}"
+
+# AI Calender GOAL: Let user use "create notify after 2 days" to create a timer event
+class CalenderEnvironment(Environment):
+ def __init__(self, env_id: str) -> None:
+ super().__init__(env_id)
+ self.db_file = AIStorage.get_instance().get_myai_dir() / "calender.db"
+ self.is_run = False
+
+ self.add_ai_function(SimpleAIFunction("get_time",
+ "get current time",
+ self._get_now))
+
+ #self.add_ai_function(SimpleAIFunction("serach_events",
+ # "search events in calender",
+ # self._search_events))
+
+ get_param = {
+ "start_time": "start time (UTC) of event",
+ "end_time": "end time (UTC) of event"
+ }
+ self.add_ai_function(SimpleAIFunction("get_events",
+ "get events in calender by time range",
+ self._get_events_by_time_range,get_param))
+
+ add_param = {
+ "title": "title of event",
+ "start_time": "start time (UTC) of event",
+ "end_time": "end time (UTC) of event",
+ "participants": "participants of event",
+ "location": "location of event",
+ "details": "details of event"
+ }
+ self.add_ai_function(SimpleAIFunction("add_event",
+ "add event to calender",
+ self._add_event,add_param))
+
+ delete_param = {
+ "event_id": "id of event"
+ }
+ self.add_ai_function(SimpleAIFunction("delete_event",
+ "delete event from calender",
+ self._delete_event,delete_param))
+
+ update_param = {
+ "event_id": "id of event",
+ "new_title": "new title of event",
+ "new_participants": "new participants of event",
+ "new_location": "new location of event",
+ "new_details": "new details of event",
+ "start_time": "new start time (UTC) of event",
+ "end_time": "new end time (UTC) of event"
+ }
+ self.add_ai_function(SimpleAIFunction("update_event",
+ "update event in calender",
+ self._update_event,update_param))
+
+
+ #maybe this function should be in other env?
+ paint_param = {
+ "prompt": "A description of the content of the painting",
+ "model_name": "Which model to use to draw the picture, can be None"
+ }
+ self.add_ai_function(SimpleAIFunction("paint",
+ "Draw a picture according to the description",
+ self._paint,paint_param))
+
+ self.add_ai_function(SimpleAIFunction("get_contact",
+ "get contact info",
+ self._get_contact,{"name":"name of contact"}))
+
+ self.add_ai_function(SimpleAIFunction("set_contact",
+ "set contact info",
+ self._set_contact,{"name":"name of contact","contact_info":"A json to descrpit contact"}))
+
+
+
+
+ #self.add_ai_function(SimpleAIFunction("user_confirm",
+ # "user confirm",
+ # self._user_confirm))
+
+ async def init_db(self):
+ async with aiosqlite.connect(self.db_file) as db:
+ await db.execute("""
+ CREATE TABLE IF NOT EXISTS events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT,
+ start_time DATETIME,
+ end_time DATETIME,
+ participants TEXT,
+ location TEXT,
+ details TEXT
+ );
+ """)
+ await db.commit()
+
+ async def _add_event(self,title, start_time, end_time, participants=None, location=None, details=None):
+ async with aiosqlite.connect(self.db_file) as db:
+ await db.execute("""
+ INSERT INTO events (title, start_time, end_time, participants, location, details)
+ VALUES (?, ?, ?, ?, ?, ?);
+ """, (title, start_time, end_time, participants, location, details))
+ await db.commit()
+ return f"execute add_event OK,event '{title}' already add to calender!"
+
+ async def _search_events(self,query):
+ async with aiosqlite.connect(self.db_file) as db:
+ cursor = await db.execute("""
+ SELECT id,title, start_time, end_time, participants, location, details FROM events
+ WHERE title LIKE ? OR participants LIKE ? OR location LIKE ? OR details LIKE ?;
+ """, (f"%{query}%", f"%{query}%", f"%{query}%", f"%{query}%"))
+ rows = await cursor.fetchall()
+
+ result = {}
+ for row in rows:
+ _event = {}
+ _event["title"] = row[1]
+ _event["start_time"] = row[2]
+ _event["end_time"] = row[3]
+ _event["participants"] = row[4]
+ _event["location"] = row[5]
+ _event["details"] = row[6]
+ result[row[0]] = _event
+ return json.dumps(result, indent=4, sort_keys=True)
+
+ async def _get_events_by_time_range(self,start_time, end_time):
+ async with aiosqlite.connect(self.db_file) as db:
+ cursor = await db.execute("""
+ SELECT id,title, start_time, end_time, participants, location, details FROM events
+ WHERE start_time >= ? AND end_time <= ?;
+ """, (start_time, end_time))
+ rows = await cursor.fetchall()
+
+ result = {}
+ have_result = False
+ for row in rows:
+ have_result = True
+ _event = {}
+ _event["title"] = row[1]
+ _event["start_time"] = row[2]
+ _event["end_time"] = row[3]
+ _event["participants"] = row[4]
+ _event["location"] = row[5]
+ _event["details"] = row[6]
+ result[row[0]] = _event
+
+ if not have_result:
+ return "No event."
+
+ return json.dumps(result, indent=4, sort_keys=True)
+
+ async def _update_event(self,event_id, new_title=None, new_participants=None, new_location=None, new_details=None ,start_time=None, end_time=None):
+ fields_to_update = []
+ values = []
+
+ if new_title is not None:
+ fields_to_update.append("title = ?")
+ values.append(new_title)
+
+ if new_participants is not None:
+ fields_to_update.append("participants = ?")
+ values.append(new_participants)
+
+ if new_location is not None:
+ fields_to_update.append("location = ?")
+ values.append(new_location)
+
+ if new_details is not None:
+ fields_to_update.append("details = ?")
+ values.append(new_details)
+
+ if start_time is not None:
+ fields_to_update.append("start_time = ?")
+ values.append(start_time)
+
+ if end_time is not None:
+ fields_to_update.append("end_time = ?")
+ values.append(end_time)
+
+ if not fields_to_update:
+ return "No fields to update."
+
+ sql_update_query = f"""
+ UPDATE events
+ SET {', '.join(fields_to_update)}
+ WHERE id = ?;
+ """
+
+ values.append(event_id)
+
+ async with aiosqlite.connect(self.db_file) as db:
+ await db.execute(sql_update_query, values)
+ await db.commit()
+ return "update ok"
+
+ async def _delete_event(self,event_id):
+ async with aiosqlite.connect(self.db_file) as db:
+ await db.execute("""
+ DELETE FROM events
+ WHERE id = ?;
+ """, (event_id,))
+ await db.commit()
+ return "Delete event ok"
+
+ def _do_get_value(self,key:str) -> Optional[str]:
+ return None
+
+ async def _get_contact(self,name:str) -> str:
+ cm = ContactManager.get_instance()
+ contact : Contact = cm.find_contact_by_name(name)
+ if contact:
+ s = json.dumps(contact.to_dict())
+ return f"Execute get_contact OK , contact {name} is {s}"
+ else:
+ return f"Execute get_contact OK , contact {name} not found!"
+
+ async def _set_contact(self,name:str,contact_info:str) -> str:
+ cm = ContactManager.get_instance()
+ contact = cm.find_contact_by_name(name)
+ contact_info = json.loads(contact_info)
+ if contact is None:
+ contact = Contact(name)
+ contact.email = contact_info.get("email")
+ contact.telegram = contact_info.get("telegram")
+ contact.notes = contact_info.get("notes")
+ contact.added_by = self.env_id
+
+ cm.add_contact(name,contact)
+
+ return f"Execute set_contact OK , new contact {name} added!"
+ else:
+ if contact_info.get("email") is not None:
+ contact.email = contact_info.get("email")
+ if contact_info.get("telegram") is not None:
+ contact.telegram = contact_info.get("telegram")
+ if contact_info.get("notes") is not None:
+ contact.notes = contact_info.get("notes")
+
+ contact.added_by = self.env_id
+ cm.set_contact(name,contact)
+
+ return f"Execute set_contact OK , contact {name} updated!"
+
+
+
+
+ async def start(self) -> None:
+ if self.is_run:
+ return
+ self.is_run = True
+ await self.init_db()
+
+ self.register_get_handler("now",self.get_now)
+ async def timer_loop():
+ while True:
+ if self.is_run == False:
+ break
+
+ await asyncio.sleep(1.0)
+ now = datetime.now()
+ formatted_time = now.strftime('%Y-%m-%d %H:%M:%S')
+ env_event:CalenderEvent = CalenderEvent(formatted_time)
+ await self.fire_event("timer",env_event)
+
+ return
+
+ asyncio.create_task(timer_loop())
+
+ def stop(self):
+ self.is_run = False
+
+ def get_now(self)->str:
+ now = datetime.now()
+ formatted_time = now.strftime('%Y-%m-%d %H:%M:%S')
+ return formatted_time
+
+ async def _get_now(self) -> str:
+ now = datetime.now()
+ formatted_time = now.strftime('%Y-%m-%d %H:%M:%S')
+ return formatted_time
+
+
+ async def _paint(self, prompt, model_name = None) -> str:
+ result = await ComputeKernel.get_instance().do_text_2_image(prompt, model_name)
+ if result.result_code == ComputeTaskResultCode.ERROR:
+ return f"exec paint failed. err:{result.error_str}"
+ else:
+ return f'exec paint OK, saved as a local file, path is: {result.result["file"]}'
+
+
+class PaintEnvironment(Environment):
+ def __init__(self, env_id: str) -> None:
+ super().__init__(env_id)
+ self.is_run = False
+
+ paint_param = {
+ "prompt": "Keywords of the content of the painting",
+ "model_name": "Which model to use to draw the picture, can be None",
+ "negative_prompt": "Keywords that describe what is not to be drawn, can be None"
+ }
+ self.add_ai_function(SimpleAIFunction("paint",
+ "Draw a picture according to the keywords",
+ self._paint,paint_param))
+
+ def _do_get_value(self,key:str) -> Optional[str]:
+ return None
+
+
+ async def _paint(self, prompt, model_name = None, negative_prompt = None) -> str:
+ err, result = await ComputeKernel.get_instance().do_text_2_image(prompt, model_name, negative_prompt)
+ if err is not None:
+ return f"exec paint failed. err:{err}"
+ else:
+ return f'exec paint OK, saved as a local file, path is: {result.result["file"]}'
+
+
+# Default Workflow Environment(Context)
+class WorkflowEnvironment(Environment):
+ def __init__(self, env_id: str,db_file:str) -> None:
+ super().__init__(env_id)
+ self.db_file = db_file
+ self.local = threading.local()
+ self.table_name = "WorkflowEnv_" + env_id
+ self.add_ai_function(TextToSpeechFunction())
+
+
+ def _get_conn(self):
+ """ get db connection """
+ if not hasattr(self.local, 'conn'):
+ self.local.conn = self._create_connection()
+ return self.local.conn
+
+ def _create_connection(self):
+ """ create a database connection to a SQLite database """
+ conn = None
+ try:
+ conn = sqlite3.connect(self.db_file)
+ except Error as e:
+ logging.error("Error occurred while connecting to database: %s", e)
+ return None
+
+ if conn:
+ self._create_table(conn)
+
+ return conn
+
+ def close(self):
+ if not hasattr(self.local, 'conn'):
+ return
+ self.local.conn.close()
+
+ def _create_table(self, conn):
+ """ create table """
+ try:
+ # create sessions table
+ conn.execute(f"""
+ CREATE TABLE IF NOT EXISTS """ + self.table_name + """ (
+ EnvKey TEXT PRIMARY KEY,
+ EnvValue TEXT,
+ UpdateTime TEXT
+ );
+ """)
+ conn.commit()
+ except Error as e:
+ logging.error("Error occurred while creating tables: %s", e)
+
+ def _do_get_value(self, key: str) -> str | None:
+ try:
+ conn = self._get_conn()
+ c = conn.cursor()
+ c.execute("SELECT EnvValue FROM " + self.table_name +" WHERE EnvKey = ?", (key,))
+ value = c.fetchone()
+ if value is None:
+ return None
+ return value[0]
+ except Error as e:
+ logging.error(f"Error occurred while _do_get_value{key}: {e}")
+ return None
+
+ def set_value(self, key: str, str_value: str, is_storage:bool=True):
+ super().set_value(key,str_value)
+ if is_storage is False:
+ return
+
+ try:
+ conn = self._get_conn()
+ conn.execute("""
+ INSERT OR REPLACE INTO """ + self.table_name+ """ (EnvKey, EnvValue, UpdateTime)
+ VALUES (?, ?, ?)
+ """, (key, str_value, datetime.now()))
+ conn.commit()
+ return 0 # return 0 if successful
+ except Error as e:
+ logging.error(f"Error occurred while update env{self.env_id}.{key} ,error:{e}")
+
+ def get_functions(self):
+ pass
\ No newline at end of file
diff --git a/src/aios_kernel/workspace_env.py b/src/aios_kernel/workspace_env.py
new file mode 100644
index 0000000..8bfe00e
--- /dev/null
+++ b/src/aios_kernel/workspace_env.py
@@ -0,0 +1,173 @@
+# this env is designed for workflow owner filesystem, support file/directory operations
+
+import subprocess
+import tempfile
+import threading
+import traceback
+import time
+import ast
+import sys
+import os
+import re
+
+from .environment import Environment,EnvironmentEvent
+from .ai_function import AIFunction,SimpleAIFunction
+
+
+class CodeInterpreter:
+ def __init__(self, language, debug_mode):
+ self.language = language
+ self.proc = None
+ self.active_line = None
+ self.debug_mode = debug_mode
+
+ def start_process(self):
+ start_cmd = sys.executable + " -i -q -u"
+ self.proc = subprocess.Popen(start_cmd.split(),
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ bufsize=0)
+
+ # Start watching ^ its `stdout` and `stderr` streams
+ threading.Thread(target=self.save_and_display_stream,
+ args=(self.proc.stdout, False), # Passes False to is_error_stream
+ daemon=True).start()
+ threading.Thread(target=self.save_and_display_stream,
+ args=(self.proc.stderr, True), # Passes True to is_error_stream
+ daemon=True).start()
+
+ def warp_code(self,pycode:str)->str:
+ # Add import traceback
+ code = "import traceback\n" + pycode
+ # Parse the input code into an AST
+ parsed_code = ast.parse(code)
+ # Wrap the entire code's AST in a single try-except block
+ try_except = ast.Try(
+ body=parsed_code.body,
+ handlers=[
+ ast.ExceptHandler(
+ type=ast.Name(id="Exception", ctx=ast.Load()),
+ name=None,
+ body=[
+ ast.Expr(
+ value=ast.Call(
+ func=ast.Attribute(value=ast.Name(id="traceback", ctx=ast.Load()), attr="print_exc", ctx=ast.Load()),
+ args=[],
+ keywords=[]
+ )
+ ),
+ ]
+ )
+ ],
+ orelse=[],
+ finalbody=[]
+ )
+
+ parsed_code.body = [try_except]
+ return ast.unparse(parsed_code)
+
+ def run(self,py_code:str):
+ """
+ Executes code.
+ """
+ # Get code to execute
+ self.code = py_code
+
+ # Start the subprocess if it hasn't been started
+ if not self.proc:
+ try:
+ self.start_process()
+ except Exception as e:
+ # Sometimes start_process will fail!
+ # Like if they don't have `node` installed or something.
+
+ traceback_string = traceback.format_exc()
+ self.output = traceback_string
+ # Before you return, wait for the display to catch up?
+ # (I'm not sure why this works)
+ time.sleep(0.1)
+
+ return self.output
+
+ self.output = ""
+
+ self.print_cmd = 'print("{}")'
+ code = self.warp_code(py_code)
+
+ if self.debug_mode:
+ print("Running code:")
+ print(code)
+ print("---")
+
+ self.done = threading.Event()
+ self.done.clear()
+
+ # Write code to stdin of the process
+ try:
+ self.proc.stdin.write(code + "\n")
+ self.proc.stdin.flush()
+ except BrokenPipeError:
+ return
+ self.done.wait()
+ time.sleep(0.1)
+ return self.output
+
+ def save_and_display_stream(self, stream, is_error_stream):
+
+ for line in iter(stream.readline, ''):
+ if self.debug_mode:
+ print("Recieved output line:")
+ print(line)
+ print("---")
+
+ line = line.strip()
+ if is_error_stream and "KeyboardInterrupt" in line:
+ raise KeyboardInterrupt
+ elif "END_OF_EXECUTION" in line:
+ self.done.set()
+ self.active_line = None
+ else:
+ self.output += "\n" + line
+ self.output = self.output.strip()
+
+
+
+class WorkspaceEnvironment(Environment):
+ def __init__(self, env_id: str) -> None:
+ super().__init__(env_id)
+
+ operator_param = {
+ "command": "command will execute",
+ }
+ self.add_ai_function(SimpleAIFunction("shell_exec",
+ "execute shell command in linux bash",
+ self.shell_exec,operator_param))
+
+ #run_code_param = {
+ # "pycode": "python code will execute",
+ #}
+ #self.add_ai_function(SimpleAIFunction("run_code",
+ # "execute python code",
+ # self.run_code,run_code_param))
+
+
+ async def shell_exec(self,command:str) -> str:
+ import asyncio.subprocess
+ process = await asyncio.create_subprocess_shell(
+ command,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE
+ )
+ stdout, stderr = await process.communicate()
+ returncode = process.returncode
+ if returncode == 0:
+ return f"Execute success! stdout is:\n{stdout}\n"
+ else:
+ return f"Execute failed! stderr is:\n{stderr}\n"
+
+ async def run_code(self,pycode:str) -> str:
+ interpreter = CodeInterpreter("python",True)
+ return interpreter.run(pycode)
+
diff --git a/src/component/agent_manager/__init__.py b/src/component/agent_manager/__init__.py
new file mode 100644
index 0000000..18058c7
--- /dev/null
+++ b/src/component/agent_manager/__init__.py
@@ -0,0 +1 @@
+from .agent_manager import AgentManager
diff --git a/src/component/agent_manager/agent_manager.py b/src/component/agent_manager/agent_manager.py
new file mode 100644
index 0000000..083b680
--- /dev/null
+++ b/src/component/agent_manager/agent_manager.py
@@ -0,0 +1,114 @@
+
+import logging
+import toml
+from typing import Any, Callable, Dict, List, Optional, Union
+
+from aios_kernel import AIAgent,AIAgentTemplete,AIStorage
+from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask
+
+logger = logging.getLogger(__name__)
+
+default_agent_cfg = """
+main = "./"
+cache = "./.agents"
+"""
+
+class AgentManager:
+ _instance = None
+
+ @classmethod
+ def get_instance(cls)->'AgentManager':
+ if cls._instance is None:
+ cls._instance = AgentManager()
+ return cls._instance
+
+ def __init__(self) -> None:
+ self.agent_templete_env : PackageEnv = None
+ self.agent_env : PackageEnv = None
+ self.db_path : str = None
+ self.loaded_agent_instance : Dict[str,AIAgent] = None
+
+ async def initial(self) -> None:
+ system_app_dir = AIStorage.get_instance().get_system_app_dir()
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+
+ self.agent_templete_env : PackageEnv = PackageEnvManager().get_env(f"{system_app_dir}/templates/templetes.cfg")
+ sys_agent_env : PackageEnv = PackageEnvManager().get_env(f"{system_app_dir}/agents/agents.cfg")
+ user_agent_config_path = f"{user_data_dir}/agents/agents.cfg"
+ await AIStorage.get_instance().try_create_file_with_default_value(user_agent_config_path,default_agent_cfg)
+ self.agent_env : PackageEnv = PackageEnvManager().get_env(user_agent_config_path)
+ self.agent_env.parent_envs.append(sys_agent_env)
+
+ self.db_path = f"{user_data_dir}/messages.db"
+ self.loaded_agent_instance = {}
+
+ return True
+
+ async def scan_all_agent(self)->None:
+ pass
+
+
+ async def get(self,agent_id:str) -> AIAgent:
+ the_agent = self.loaded_agent_instance.get(agent_id)
+ if the_agent:
+ return the_agent
+
+ # try load from disk
+ agent_media_info = self.agent_env.load(agent_id)
+ if agent_media_info is None:
+ return None
+
+ the_agent : AIAgent = await self._load_agent_from_media(agent_media_info)
+ if the_agent is None:
+ logger.warn(f"load agent {agent_id} from media failed!")
+
+ the_agent.chat_db = self.db_path
+ return the_agent
+
+ def remove(self,agent_id:str)->int:
+ pass
+
+ async def get_templete(self,templete_id) -> AIAgentTemplete:
+ template_media_info = self.agent_templete_env.get(templete_id)
+ if template_media_info is None:
+ return None
+ return self._load_templete_from_media(template_media_info)
+
+ def install(self,templete_id) -> PackageInstallTask:
+ installer = self.agent_templete_env.get_installer()
+ return installer.install(templete_id)
+
+ def uninstall(self,templete_id) -> int:
+ pass
+
+ async def _load_templete_from_media(self,templete_media:PackageMediaInfo) -> AIAgentTemplete:
+ pass
+
+ async def _load_agent_from_media(self,agent_media:PackageMediaInfo) -> AIAgent:
+ reader = self.agent_env._create_media_loader(agent_media)
+ if reader is None:
+ logger.error(f"create media loader for {agent_media} failed!")
+ return None
+
+ try:
+ config_file = await reader.read("agent.toml","r")
+ if config_file is None:
+ logger.error(f"read agent config from {agent_media} failed!")
+ return None
+
+ config_data = await config_file.read()
+ config = toml.loads(config_data)
+ result_agent = AIAgent()
+ if result_agent.load_from_config(config) is False:
+ logger.error(f"load agent from {agent_media} failed!")
+ return None
+ return result_agent
+ except Exception as e:
+ logger.error(f"read agent.toml cfg from {agent_media} failed! unexpected error occurred: {str(e)}")
+ return None
+
+
+
+ def create(self,template,agent_name,agent_last_name,agent_introduce) -> AIAgent:
+ pass
+
diff --git a/src/component/ndn_client/__init__.py b/src/component/ndn_client/__init__.py
new file mode 100644
index 0000000..23e2329
--- /dev/null
+++ b/src/component/ndn_client/__init__.py
@@ -0,0 +1,2 @@
+from .cid import ContentId
+from .ndn_client import NDN_Client
\ No newline at end of file
diff --git a/src/component/ndn_client/cid.py b/src/component/ndn_client/cid.py
new file mode 100644
index 0000000..8bb6764
--- /dev/null
+++ b/src/component/ndn_client/cid.py
@@ -0,0 +1,12 @@
+
+
+class ContentId:
+ def __init__(self) -> None:
+ pass
+
+ def as_str(self) -> str:
+ pass
+
+ @staticmethod
+ def create_from_str(cid_str:str):
+ pass
\ No newline at end of file
diff --git a/src/component/ndn_client/ndn_client.py b/src/component/ndn_client/ndn_client.py
new file mode 100644
index 0000000..e9c46a6
--- /dev/null
+++ b/src/component/ndn_client/ndn_client.py
@@ -0,0 +1,117 @@
+import asyncio,aiofiles,aiohttp
+import logging
+from typing import Optional
+
+from .cid import ContentId
+
+logger = logging.getLogger(__name__)
+
+NDN_GET_TASK_STATE_INIT = 0
+NDN_GET_TAKS_CONNECTING = 1
+NDN_GET_TASK_STATE_DOWNLOADING = 2
+NDN_GET_TASK_STATE_VERIFYING = 3
+NDN_GET_TASK_STATE_DONE = 4
+NDN_GET_TASK_STATE_ERROR = 5
+
+class NDN_GetTask:
+ def __init__(self) -> None:
+ self.cid:str = None
+ self.target_path:str = None
+ self.urls:[str] = None
+ self.options:Optional[dict] = None
+
+ self.working_task = None
+ self.state = NDN_GET_TASK_STATE_INIT
+ self.total_size = 0
+ self.recv_bytes = 0
+ self.write_bytes = 0
+ self.error_str = None
+ self.chunk_queue = None
+
+ self.retry_count = 0
+ self.used_urls = []
+ self.hash_update = None
+
+
+ def select_url(self,index:int)->str:
+ return self.urls[0]
+
+ def get_chunk_for_download(self)->bytes:
+ pass
+
+class NDN_Client:
+ def __init__(self):
+ self.cache_dir = ""
+ self.default_ndn_http_gateway = ""
+ self.all_task = {}
+ self.memory_chunk_size = 1024*1024*2
+ self.chunk_queue_size = 16
+
+ def load_config(self,config:dict):
+ if config.get("cache_dir"):
+ self.cache_dir = config.get("cache_dir")
+ if config.get("dndn_gateway"):
+ self.default_ndn_http_gateway = config.get("ndn_gateway")
+
+ def get_file(self,cid:ContentId,target_path:str,urls:{}=None,options:{}=None)->NDN_GetTask:
+ get_task = self.all_task.get(cid.as_str())
+ if get_task:
+ return get_task
+ else:
+ get_task = NDN_GetTask()
+ self.all_task[cid.as_str()] = get_task
+
+ get_task.cid = cid
+ get_task.target_path = target_path
+ get_task.urls = urls
+ get_task.options = options
+ if get_task.urls is None:
+ get_task.urls = [f"{self.default_ndn_http_gateway}/{cid.as_str()}"]
+ logger.info(f"get_file {cid.as_str()} urls is None, use {get_task.urls[0]} as default")
+
+
+ async def get_file_async():
+ target_file = aiofiles.open(target, 'wb')
+ # if file exist, check hash first
+
+ http_session = aiohttp.ClientSession()
+ resp = http_session.get(get_task.select_url(0))
+ if resp.status != 200:
+ get_task.error_str = f"get_file {cid.as_str()} failed,http status:{resp.status}"
+ return
+ get_task.total_size = resp.content_length
+
+ async def write_file_async():
+ while True:
+ chunk = await get_task.chunk_queue.pop()
+ chunk_size = len(chunk)
+ if not chunk or chunk_size == 0:
+ break
+ get_task.hash_update.update(chunk)
+ await target_file.write(chunk)
+ get_task.write_bytes += chunk_size
+
+ #verify
+ get_task.state = NDN_GET_TASK_STATE_VERIFYING
+ await target_file.close()
+ return
+
+ write_task = asyncio.create_task(write_file_async())
+ while True:
+ await get_task.chunk_queue.pop()
+ chunk = resp.content.read(self.memory_chunk_size)
+ chunk_size = len(chunk)
+ if not chunk or chunk_size == 0:
+ break
+
+ get_task.recv_bytes += len(chunk)
+ get_task.chunk_queue.push(chunk)
+
+
+ get_task.state = NDN_GET_TASK_STATE_DONE
+ await write_task
+
+ get_task.working_task = asyncio.create_task(get_file_async())
+ return get_task
+
+
\ No newline at end of file
diff --git a/src/component/package_manager/README b/src/component/package_manager/README
new file mode 100644
index 0000000..30404ce
--- /dev/null
+++ b/src/component/package_manager/README
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/src/component/package_manager/__init__.py b/src/component/package_manager/__init__.py
new file mode 100644
index 0000000..0af5eb5
--- /dev/null
+++ b/src/component/package_manager/__init__.py
@@ -0,0 +1,3 @@
+from .env import PackageEnvManager,PackageEnv
+from .pkg import PackageInfo,PackageMediaInfo
+from .installer import PackageInstallTask
\ No newline at end of file
diff --git a/src/component/package_manager/env.py b/src/component/package_manager/env.py
new file mode 100644
index 0000000..3e16c4d
--- /dev/null
+++ b/src/component/package_manager/env.py
@@ -0,0 +1,158 @@
+
+import logging
+import toml
+import os
+
+from .pkg import PackageInfo,PackageMediaInfo
+from .media_reader import MediaReader
+
+logger = logging.getLogger(__name__)
+
+
+class PackageEnv:
+ def __init__(self,cfg_path:str) -> None:
+ self.pkg_dir : str = "./pkgs/"
+ self.pkg_obj_dir : str = "./.pkgs/"
+
+ self.locked_index : str = "./pkg.lock"
+ self.is_strict : bool = True
+ self.parent_envs : list[PackageEnv] = []
+ self.index_dbs = None
+
+ self.env_dir = None
+ self.cfg_path = cfg_path
+ self._load_pkg_cfg(cfg_path)
+ pass
+
+ def load_from_config(self,config:dict) -> bool:
+ if config.get("main") is not None:
+ self.pkg_dir = os.path.abspath(self.env_dir + "/" + config["main"])
+
+ if config.get("cache") is not None:
+ self.pkg_obj_dir = os.path.abspath(self.env_dir + "/ " + config["cache"])
+
+ def load(self,pkg_name:str,search_parent=True) -> PackageMediaInfo:
+ pkg_path = None
+ pkg_id,verion_str,cid = PackageInfo.parse_pkg_name(pkg_name)
+
+ if cid is None:
+ if verion_str is None:
+ pkg_path = f"{self.pkg_dir}/{pkg_id}"
+ else:
+ #TODO fix bug about channel here
+ channel:str = self.get_pkg_channel_from_version(verion_str)
+ the_version:str = self.get_exact_version_from_installed(verion_str)
+ if the_version is None:
+ logger.warn(f"load {pkg_name} failed: no match version from {verion_str}")
+ return None
+ if channel is None:
+ pkg_path = f"{self.pkg_dir}/{pkg_id}#{the_version}"
+ else:
+ pkg_path = f"{self.pkg_dir}/{pkg_id}#{channel}#{the_version}"
+ else:
+ pkg_path = f"{self.pkg_obj_dir}/.{pkg_id}/{cid}"
+
+ media_info:PackageMediaInfo = self.try_load_pkg_media_info(pkg_path)
+ if media_info is None:
+ if search_parent is True and self.parent_envs is not None:
+ for parent_env in self.parent_envs:
+ media_info = parent_env.load(pkg_id,False)
+ if media_info is not None:
+ return media_info
+
+ if media_info is None:
+ logger.warn(f"pkg_load {pkg_id}, cid:{cid} error,not found ,search_parent={search_parent}")
+
+ return media_info
+
+ def get_exact_version_from_installed(self,verion_str:str) -> str:
+ pass
+
+ def get_pkg_channel_from_version(self,pkg_version:str) -> str:
+ args = pkg_version.split("~")
+ if len(args) == 1:
+ return None
+ else:
+ return args[0]
+
+
+ def get_pkg_media_info(self,pkg_name:str)->PackageMediaInfo:
+ pass
+
+ def try_load_pkg_media_info(self,pkg_full_path:str) -> PackageMediaInfo:
+ the_result : PackageMediaInfo = None
+ logger.debug(f"try load pkng from:{pkg_full_path}")
+ if os.path.isdir(pkg_full_path):
+ the_result = PackageMediaInfo(pkg_full_path,"dir")
+
+ return the_result
+
+ def _create_media_loader(self,media_info:PackageMediaInfo) -> MediaReader:
+ match media_info.media_type:
+ case "dir":
+ from .media_reader import FolderMediaReader
+ return FolderMediaReader(media_info.full_path)
+
+ logger.error(f"create media loader for {media_info} failed!")
+ return None
+
+ def get_installed_pkg_info(self,pkg_name:str) -> PackageInfo:
+ pass
+
+ def lookup(self,pkg_id:str,version_str:str) -> PackageInfo:
+ # to make sure pkg.cid is correct, we MUST verfiy eveything here
+ pass
+
+ @classmethod
+ def is_valied_media(pkg_full_path:str) -> bool:
+ pass
+
+ def do_pkg_media_trans(self,pkg_info:PackageInfo,source_path:str,target_path:str) -> bool:
+ pass
+
+ def _load_pkg_cfg(self,cfg_path:str):
+ if cfg_path is None:
+ return
+
+ cfg = None
+ if len(cfg_path) < 1:
+ return
+ try:
+ cfg = toml.load(cfg_path)
+ self.env_dir = os.path.abspath(os.path.dirname(cfg_path))
+ self.cfg_path = os.path.abspath(cfg_path)
+ except Exception as e:
+ logger.error(f"read pkg cfg from {cfg_path} failed! unexpected error occurred: {str(e)}")
+ return
+
+ return self.load_from_config(cfg)
+
+
+
+ def _preprocess_prefixs(self,prefixs):
+ pass
+
+class PackageEnvManager:
+ _instance = None
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = PackageEnvManager()
+ return cls._instance
+
+ def __init__(self) -> None:
+ self._pkg_envs = {}
+
+ def get_env(self,cfg_path:str) -> PackageEnv:
+ if cfg_path in self._pkg_envs:
+ return self._pkg_envs[cfg_path]
+ else:
+ pkg_env = PackageEnv(cfg_path)
+ self._pkg_envs[cfg_path] = pkg_env
+ return pkg_env
+
+ def get_user_env(self) -> PackageEnv:
+ pass
+
+ def get_system_env(self) -> PackageEnv:
+ pass
diff --git a/src/component/package_manager/index_db.py b/src/component/package_manager/index_db.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/component/package_manager/index_syncer.py b/src/component/package_manager/index_syncer.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/component/package_manager/index_syncer.py
@@ -0,0 +1 @@
+
diff --git a/src/component/package_manager/installer.py b/src/component/package_manager/installer.py
new file mode 100644
index 0000000..8ab8f5f
--- /dev/null
+++ b/src/component/package_manager/installer.py
@@ -0,0 +1,170 @@
+# installer download pkg by cid, than install it to target dir
+import logging
+import asyncio
+import aiohttp
+import aiofiles
+import os
+from typing import Tuple
+
+from ndn_client import ContentId,NDN_Client
+from .pkg import PackageInfo,PackageMediaInfo
+from .env import PackageEnv
+
+logger = logging.getLogger(__name__)
+
+INSTALL_TASK_STATE_DONE = 0
+INSTALL_TASK_STATE_CHECK_DEPENDENCY = 1
+INSTALL_TASK_STATE_INSTALL_DEPENDENCY = 2
+INSTALL_TASK_STATE_DOWNLOADING = 3
+INSTALL_TASK_STATE_INSTALLING = 4
+INSTALL_TAKS_STATE_ERROR = 5
+
+class PackageInstallTask:
+ def __init__(self,owner:PackageEnv) -> None:
+ self.owner = owner
+ self.state = INSTALL_TASK_STATE_CHECK_DEPENDENCY
+
+ self.pkg_media_info = None
+ self.working_task = None
+ self.dependency_tasks = None
+ self.error_str = None
+
+class PackageInstaller:
+ def __init__(self,owner_env:PackageEnv) -> None:
+ self.all_tasks = {}
+ self.owner_env = owner_env
+
+ def install(self,pkg_name:str,
+ install_from_dependency = False, can_upgrade = True,skip_depends = False,options = None)->Tuple[PackageInstallTask,str]:
+
+ the_pkg_info : PackageInfo = None
+ is_upgrade : bool = False
+ need_backup : bool = False
+
+ pkg_id,version_str,cid = PackageInfo.parse_pkg_name(pkg_name)
+ media_info : PackageMediaInfo = self.owner_env.get_media_info(pkg_name) # must use index-db?
+ if media_info is not None:
+ if cid is not None:
+ if can_upgrade:
+ is_upgrade = True
+ else:
+ error_str = f"{pkg_name},{cid} already installed!"
+ logger.error(error_str)
+ return None,error_str
+ else:
+ the_pkg_info = self.owner_env.lookup(pkg_id,version_str,None)
+ if the_pkg_info is None:
+ error_str = f"{pkg_name} old version exist in local but not found in index db!"
+ logger.error(error_str)
+ return None,error_str
+ else:
+ is_upgrade = True
+ need_backup = True
+
+ if the_pkg_info is None:
+ the_pkg_info = self.owner_env.lookup(pkg_id,version_str,cid)
+
+ if the_pkg_info is None:
+ error_str = f"{pkg_name} ,cid:{cid} not found in index db"
+ logger.error(error_str)
+ return None,error_str
+
+ result_task = self.all_tasks.get(the_pkg_info.cid)
+ if result_task is not None:
+ return result_task,"already installing"
+
+ logger.info(f"start download&install {pkg_name},install_from_dependency={install_from_dependency},upgrade={is_upgrade},backup={need_backup},target_pkg_info={the_pkg_info}")
+ result_task = PackageInstallTask(self.owner_env)
+ self.all_tasks[the_pkg_info.cid] = result_task
+ async def download_and_install_pkg()->int:
+ # check dependency
+ if skip_depends is False:
+ result_task.dependency_tasks = {}
+ self.get_dependency_tasks(the_pkg_info,result_task.dependency_tasks)
+ result_task.state = INSTALL_TASK_STATE_INSTALL_DEPENDENCY
+ for depend_pkg_name in result_task.dependency_tasks:
+ # check pkg in local?
+ # install miss pkg
+ pass
+
+ result_task.state = INSTALL_TASK_STATE_DOWNLOADING
+ install_full_path = ""
+ target_full_path = ""
+ old_package_full_path = ""
+ is_download_directy = False
+
+ if the_pkg_info.target_media_type == the_pkg_info.source_media_type:
+ is_download_directy = True
+ if is_upgrade:
+ target_full_path = ""
+ else:
+ target_full_path = ""
+ else:
+ pass
+
+ urls = self.owner_env.get_pkg_urls(the_pkg_info)
+ #download
+ client = NDN_Client() # set watch
+ download_result = await client.get_file(the_pkg_info.cid,urls,target_full_path,options)
+ if download_result !=0:
+ result_task.state = INSTALL_TAKS_STATE_ERROR
+ return result_task.state
+
+ result_task.state = INSTALL_TASK_STATE_INSTALLING
+ if is_download_directy is False:
+ install_media_result = False
+ install_media_result = await self.owner_env.do_pkg_media_trans(the_pkg_info,target_full_path,install_full_path)
+ if install_media_result is False:
+ result_task.state = INSTALL_TAKS_STATE_ERROR
+ result_task.error_str = "install media error,from {target_full_path} to {install_full_path}"
+ return result_task.state
+
+ # last step,save install flag : install by manual or install by dependency
+ ## save cid dir
+ if is_upgrade:
+ os.rename(old_package_full_path, old_package_full_path + ".old" )
+ os.rename(target_full_path,install_full_path)
+ ## update/create version link
+
+ ## update pkg state
+ ## remove old version
+
+ result_task.state = INSTALL_TASK_STATE_DONE
+ return result_task.state
+
+
+ result_task.working_task = asyncio.create_task(download_and_install_pkg())
+ return result_task,None
+
+
+ def uninstall(self):
+ pass
+
+ def get_dependency_tasks(self,pkg:PackageInfo,dependency_tasks):
+ pass
+
+ async def check_dependency(self,pkg:PackageInfo,task_list:{}) -> bool:
+ for depend_pkg_name in pkg.depends:
+ depend_task = task_list.get(depend_pkg_name)
+ if depend_task is not None:
+ logger.debug(f"{pkg.name}'s depend pkg {depend_pkg_name} already in task list")
+ continue
+ depend_task = PackageInstallTask(self.owner_env)
+ task_list[depend_pkg_name] = depend_task
+
+ depend_pkg_info = self.owner_env.lookup(depend_pkg_name)
+ if depend_pkg_info is None:
+ logger.warn(f"{pkg.name}'s depend pkg {depend_pkg_name} not found in index db")
+ return False
+
+ if await self.check_dependency(depend_pkg_info,task_list) is False:
+ return False
+
+ return True
+
+
+
+
+
+
+
diff --git a/src/component/package_manager/media_reader.py b/src/component/package_manager/media_reader.py
new file mode 100644
index 0000000..9c7f72e
--- /dev/null
+++ b/src/component/package_manager/media_reader.py
@@ -0,0 +1,18 @@
+from abc import ABC, abstractmethod
+import aiofiles
+
+class MediaReader(ABC):
+ @abstractmethod
+ async def read(self, inner_path:str,mode:str):
+ pass
+
+
+class FolderMediaReader(MediaReader):
+ def __init__(self, root_dir:str) -> None:
+ self.root_dir = root_dir
+ pass
+
+ async def read(self, inner_path:str,mode:str):
+ full_path = self.root_dir + "/" + inner_path
+ result_file = await aiofiles.open(full_path, mode,encoding='utf-8')
+ return result_file
\ No newline at end of file
diff --git a/src/component/package_manager/pkg.py b/src/component/package_manager/pkg.py
new file mode 100644
index 0000000..7ed8431
--- /dev/null
+++ b/src/component/package_manager/pkg.py
@@ -0,0 +1,41 @@
+from typing import Tuple
+
+
+class PackageInfo:
+ def __init__(self) -> None:
+ self.name = ""
+ self.cid = None
+ self.depends : list[str] = None
+ self.author = None
+ self.remote_urls = None
+ self.target_media_type = "dir"
+ self.source_media_type = "7z"
+
+ @staticmethod
+ def parse_pkg_name(pkg_name:str) -> Tuple[str, str, str]:
+ """parse pkg name like test-pkg#nightly~>0.2.31#sha1:323423423 to test-pkg,nightly#>0.2.31,sha1:323423423"""
+ args = pkg_name.split("#")
+ if len(args) == 1:
+ return args[0],None,None
+ elif len(args) == 2:
+ return args[0],None,arg[2]
+ elif len(args) == 3:
+ return args[0],args[1],args[2]
+ else:
+ logger.error(f"parse pkg name {pkg_name} failed!")
+ return None,None,None
+
+
+
+
+ @property
+ def cid(self) -> str:
+ return self.cid
+
+class PackageMediaInfo:
+ def __init__(self,full_path,media_type) -> None:
+ self.media_type = media_type
+ self.full_path = full_path
+
+
+
diff --git a/src/component/workflow_manager/__init__.py b/src/component/workflow_manager/__init__.py
new file mode 100644
index 0000000..de62e89
--- /dev/null
+++ b/src/component/workflow_manager/__init__.py
@@ -0,0 +1 @@
+from .workflow_manager import WorkflowManager
\ No newline at end of file
diff --git a/src/component/workflow_manager/workflow_manager.py b/src/component/workflow_manager/workflow_manager.py
new file mode 100644
index 0000000..ee5ef9e
--- /dev/null
+++ b/src/component/workflow_manager/workflow_manager.py
@@ -0,0 +1,105 @@
+import logging
+import toml
+import os
+
+from aios_kernel import Workflow,AIStorage
+from package_manager import PackageEnv,PackageEnvManager,PackageMediaInfo,PackageInstallTask
+from agent_manager import AgentManager
+logger = logging.getLogger(__name__)
+
+default_workflow_cfg = """
+main = "./"
+cache = "./.agents"
+"""
+
+class WorkflowManager:
+ _instance = None
+
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = WorkflowManager()
+ return cls._instance
+
+
+ async def initial(self) -> None:
+ self.loaded_workflow = {}
+ system_app_dir = AIStorage.get_instance().get_system_app_dir()
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+
+ sys_workflow_env = PackageEnvManager().get_env(f"{system_app_dir}/workflows/workflows.cfg")
+
+ user_workflow_config_path = f"{user_data_dir}/workflows/workflows.cfg"
+ await AIStorage.get_instance().try_create_file_with_default_value(user_workflow_config_path,default_workflow_cfg)
+ self.workflow_env = PackageEnvManager().get_env(f"{user_data_dir}/workflows/workflows.cfg")
+ self.workflow_env.parent_envs.append(sys_workflow_env)
+
+ self.db_file = os.path.abspath(f"{user_data_dir}/messages.db")
+ if self.workflow_env is None:
+ logger.error(f"load workflow env failed!")
+ return False
+
+ return True
+
+ async def get_agent_default_workflow(self,agent_id:str) -> Workflow:
+ pass
+
+
+ async def _load_workflow_agents(self,workflow:Workflow) -> bool:
+ for v in workflow.role_group.roles.values():
+ agent = await AgentManager.get_instance().get(v.agent_name)
+ if agent is None:
+ logger.error(f"load agent {v.agent_name} failed!")
+ return False
+ v.agent = agent
+
+ for sub_workflow in workflow.sub_workflows.values():
+ if await self._load_workflow_agents(sub_workflow) is False:
+ return False
+ return True
+
+ async def get_workflow(self,workflow_id:str) -> Workflow:
+ the_workflow : Workflow = self.loaded_workflow.get(workflow_id)
+ if the_workflow:
+ return the_workflow
+
+ # try load from disk
+ workflow_media_info = self.workflow_env.load(workflow_id)
+ if workflow_media_info is None:
+ return None
+
+ the_workflow = await self._load_workflow_from_media(workflow_media_info)
+ if the_workflow is None:
+ logger.warn(f"load workflow {workflow_id} from media failed!")
+ return None
+
+ if await self._load_workflow_agents(the_workflow) is False:
+ return None
+
+ return the_workflow
+
+ async def _load_workflow_from_media(self,workflow_media:PackageMediaInfo) -> Workflow:
+ reader = self.workflow_env._create_media_loader(workflow_media)
+ if reader is None:
+ logger.error(f"create media loader for {workflow_media} failed!")
+ return None
+
+ try:
+ config_file = await reader.read("workflow.toml","r")
+ if config_file is None:
+ logger.error(f"read workflow config from {workflow_media} failed!")
+ return None
+
+ config_data = await config_file.read()
+ config = toml.loads(config_data)
+ result_workflow = Workflow()
+ result_workflow.db_file = self.db_file
+
+ if result_workflow.load_from_config(config) is False:
+ logger.error(f"load workflow from {workflow_media} failed!")
+ return None
+
+ return result_workflow
+ except Exception as e:
+ logger.error(f"read workflow.toml cfg from {workflow_media} failed! unexpected error occurred: {str(e)}")
+ return None
\ No newline at end of file
diff --git a/src/knowledge/__init__.py b/src/knowledge/__init__.py
new file mode 100644
index 0000000..680c632
--- /dev/null
+++ b/src/knowledge/__init__.py
@@ -0,0 +1,5 @@
+from .object import *
+from .vector import *
+from .data import *
+from .store import KnowledgeStore
+from .core_object import *
\ No newline at end of file
diff --git a/src/knowledge/core_object/__init__.py b/src/knowledge/core_object/__init__.py
new file mode 100644
index 0000000..dee13e2
--- /dev/null
+++ b/src/knowledge/core_object/__init__.py
@@ -0,0 +1,5 @@
+from .document_object import DocumentObject, DocumentObjectBuilder
+from .image_object import ImageObject, ImageObjectBuilder
+from .video_object import VideoObject, VideoObjectBuilder
+from .rich_text_object import RichTextObject, RichTextObjectBuilder
+from .email_object import EmailObject, EmailObjectBuilder
\ No newline at end of file
diff --git a/src/knowledge/core_object/document_object.py b/src/knowledge/core_object/document_object.py
new file mode 100644
index 0000000..b69f59f
--- /dev/null
+++ b/src/knowledge/core_object/document_object.py
@@ -0,0 +1,61 @@
+from ..object import KnowledgeObject, ObjectRelationStore
+from ..data import ChunkList, ChunkListWriter
+from ..object import ObjectType
+from .. import KnowledgeStore
+
+# desc
+# meta
+# hash: "file-hash",
+# tags: {}
+# body
+# chunk_list: [chunk_id, chunk_id, ...]
+
+
+class DocumentObject(KnowledgeObject):
+ def __init__(self, meta: dict, tags: dict, chunk_list: ChunkList):
+ desc = dict()
+ body = dict()
+ desc["meta"] = meta
+ desc["tags"] = tags
+ desc["hash"] = chunk_list.hash.to_base58()
+ body["chunk_list"] = chunk_list.chunk_list
+
+ super().__init__(ObjectType.Document, desc, body)
+
+ def get_meta(self):
+ return self.desc["meta"]
+
+ def get_tags(self):
+ return self.desc["tags"]
+
+ def get_hash(self):
+ return self.desc["hash"]
+
+ def get_chunk_list(self):
+ return self.body["chunk_list"]
+
+
+class DocumentObjectBuilder:
+ def __init__(self, meta: dict, tags: dict, text: str):
+ self.meta = meta
+ self.tags = tags
+ self.text = text
+
+ def set_meta(self, meta: dict):
+ self.meta = meta
+ return self
+
+ def set_text(self, text: str):
+ self.text = text
+ return self
+
+ def build(self) -> DocumentObject:
+ chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_text(self.text)
+ doc = DocumentObject(self.meta, self.tags, chunk_list)
+ doc_id = doc.calculate_id()
+
+ # Add relation to store
+ for chunk_id in chunk_list.chunk_list:
+ KnowledgeStore().get_relation_store().add_relation(chunk_id, doc_id)
+
+ return doc
diff --git a/src/knowledge/core_object/email_object.py b/src/knowledge/core_object/email_object.py
new file mode 100644
index 0000000..108c9cc
--- /dev/null
+++ b/src/knowledge/core_object/email_object.py
@@ -0,0 +1,160 @@
+from .. import KnowledgeStore
+from .rich_text_object import RichTextObject, RichTextObjectBuilder
+from ..object import ObjectID, ObjectType, KnowledgeObject
+from .document_object import DocumentObjectBuilder
+from .image_object import ImageObjectBuilder
+from .video_object import VideoObjectBuilder
+import os
+import json
+import logging
+
+
+class EmailObject(KnowledgeObject):
+ def __init__(self, meta: dict, tags: dict, rich_text: RichTextObject):
+ desc = dict()
+ body = dict()
+ desc["meta"] = meta
+ desc["tags"] = tags
+
+ # FIXME rich text content store in desc or body? which one is better?
+ body["content"] = rich_text
+
+ super().__init__(ObjectType.Email, desc, body)
+
+ def get_meta(self) -> dict:
+ return self.desc["meta"]
+
+ def get_tags(self) -> dict:
+ return self.desc["tags"]
+
+ def get_rich_text(self) -> RichTextObject:
+ return self.body["content"]
+
+
+"""
+EmailObject folder structure:
+.
+├── email.txt
+└── meta.json
+ ├── image
+ │ ├── image1.jpg
+ │ ├── image2.jpg
+ │ └── ...
+ ├── video
+ │ ├── video1.mp4
+ │ ├── video2.mv
+ │ └── ...
+ └── audio
+ ├── audio1.m4a
+ ├── audio2.flac
+ └── ...
+EmailObjectBuilder will read the target folder and build the EmailObject
+Store meta.json to meta in EmailObject
+Store email.txt to DocumentObject and RichTextObject in EmailObject
+Store very image file in image folder to ImageObject and RichTextObject in EmailObject, etc
+"""
+
+
+class EmailObjectBuilder:
+ def __init__(self, tags: dict, folder: str):
+ self.tags = tags
+ self.folder = folder
+
+ def set_tags(self, tags: dict):
+ self.tags = tags
+ return self
+
+ def set_folder(self, folder: str):
+ self.folder = folder
+ return self
+
+ def build(self) -> EmailObject:
+
+ # Just get the object store and relation store from global KnowledgeStore
+ store = KnowledgeStore().get_object_store()
+ relation = KnowledgeStore().get_relation_store()
+
+ # Read meta.json
+ meta = {}
+ meta_file = os.path.join(self.folder, "meta.json")
+ if os.path.exists(meta_file):
+ logging.info(f"Will read meta.json {meta_file}")
+ with open(meta_file, "r", encoding="utf-8") as f:
+ meta = json.load(f)
+ else:
+ logging.info(f"Meta file missing! {meta_file}")
+
+ # Read email.txt
+ documents = {}
+ content_file = os.path.join(self.folder, "email.txt")
+ if os.path.exists(content_file):
+ logging.info(f"Will read email.txt {content_file}")
+
+ try:
+ with open(content_file, "r", encoding="utf-8") as f:
+ text = f.read()
+
+ document = DocumentObjectBuilder({}, {}, text).build()
+ document_id = document.calculate_id()
+ store.put_object(document_id, document.encode())
+ documents = {"email.txt": document_id}
+ except Exception as e:
+ logging.error(f"Failed to read email.txt {content_file} {e}")
+ else:
+ logging.info(f"Content file missing! {content_file}")
+
+ # Process image files
+ images = {}
+ image_dir = os.path.join(self.folder, "image")
+ if os.path.exists(image_dir):
+ for image_file in os.listdir(image_dir):
+ image_path = os.path.join(image_dir, image_file)
+ logging.info(f"Will read image file {image_path}")
+
+ try:
+ image = ImageObjectBuilder({}, {}, image_path).build()
+ image_id = image.calculate_id()
+ store.put_object(image_id, image.encode())
+ images[image_file] = image_id
+ except Exception as e:
+ logging.error(f"Failed to read image file {image_path} {e}")
+ continue
+
+ # Process video files
+ videos = {}
+ video_dir = os.path.join(self.folder, "video")
+ if os.path.exists(video_dir):
+ for video_file in os.listdir(video_dir):
+ video_path = os.path.join(video_dir, video_file)
+ logging.info(f"Will read video file {video_path}")
+
+ try:
+ video = VideoObjectBuilder({}, {}, video_path).build()
+ video_id = video.calculate_id()
+ store.put_object(video_id, video.encode())
+ videos[video_file] = video_id
+ except Exception as e:
+ logging.error(f"Failed to read video file {video_path} {e}")
+ continue
+
+ # Create RichTextObject
+ rich_text = RichTextObject(images, videos, documents)
+ rich_text_id = rich_text.calculate_id()
+
+ # build relations with rich_text
+ for image_id in images.values():
+ relation.add_relation(image_id, rich_text_id)
+ for video_id in videos.values():
+ relation.add_relation(video_id, rich_text_id)
+ for document_id in documents.values():
+ relation.add_relation(document_id, rich_text_id)
+
+ # Create EmailObject
+ email_object = EmailObject(meta, {}, rich_text)
+ email_object_id = email_object.calculate_id()
+ store.put_object(email_object_id, email_object.encode())
+
+ # build relations with email_object
+ relation.add_relation(rich_text_id, email_object_id)
+
+ return email_object
diff --git a/src/knowledge/core_object/image_object.py b/src/knowledge/core_object/image_object.py
new file mode 100644
index 0000000..340bcc2
--- /dev/null
+++ b/src/knowledge/core_object/image_object.py
@@ -0,0 +1,96 @@
+from ..object import KnowledgeObject
+from ..data import ChunkList, ChunkListWriter
+from ..object import ObjectType
+from .. import KnowledgeStore
+import os
+
+# desc
+# meta
+# tags
+# hash: "file-hash",
+# exif: {}
+# body
+# chunk_list: [chunk_id, chunk_id, ...]
+
+
+class ImageObject(KnowledgeObject):
+ def __init__(self, meta: dict, tags: dict, exif: dict, file_size: int, chunk_list: ChunkList):
+ desc = dict()
+ body = dict()
+ desc["meta"] = meta
+ desc["exif"] = exif
+ desc["tags"] = tags
+ desc["hash"] = chunk_list.hash.to_base58()
+ desc["file_size"] = file_size
+ body["chunk_list"] = chunk_list.chunk_list
+
+ super().__init__(ObjectType.Image, desc, body)
+
+ def get_meta(self) -> dict:
+ return self.desc["meta"]
+
+ def get_exif(self) -> dict:
+ return self.desc["exif"]
+
+ def get_tags(self) -> dict:
+ return self.desc["tags"]
+
+ def get_hash(self) -> str:
+ return self.desc["hash"]
+
+ def get_file_size(self) -> int:
+ return self.desc["file_size"]
+
+ def get_chunk_list(self) -> ChunkList:
+ return self.body["chunk_list"]
+
+
+from PIL import Image
+from PIL.ExifTags import TAGS
+
+
+def get_exif_data(image_path: str):
+ with Image.open(image_path) as image:
+ exif_data = image._getexif()
+
+ if exif_data is not None:
+ return {
+ TAGS.get(key): exif_data[key]
+ for key in exif_data.keys()
+ if key in TAGS and isinstance(exif_data[key], str)
+ }
+ else:
+ return {}
+
+
+class ImageObjectBuilder:
+ def __init__(self, meta: dict, tags: dict, image_file: str):
+ self.meta = meta
+ self.tags = tags
+ self.image_file = image_file
+ self.restore_file = False
+
+ def set_meta(self, meta: dict):
+ self.meta = meta
+ return self
+
+ def set_tags(self, tags: dict):
+ self.tags = tags
+ return self
+
+ def set_image_file(self, image_file: str):
+ self.image_file = image_file
+ return self
+
+ def set_restore_file(self, restore_file: bool):
+ self.restore_file = restore_file
+ return self
+
+ def build(self) -> ImageObject:
+
+ file_size = os.path.getsize(self.image_file)
+ chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_file(
+ self.image_file, 1024 * 1024 * 4, self.restore_file
+ )
+ exif = get_exif_data(self.image_file)
+ return ImageObject(self.meta, self.tags, exif, file_size, chunk_list)
diff --git a/src/knowledge/core_object/rich_text_object.py b/src/knowledge/core_object/rich_text_object.py
new file mode 100644
index 0000000..7791689
--- /dev/null
+++ b/src/knowledge/core_object/rich_text_object.py
@@ -0,0 +1,80 @@
+from knowledge.object.object_id import ObjectType
+from ..object import KnowledgeObject
+from ..data import ChunkList, ChunkListWriter
+from ..object import ObjectType
+from .video_object import VideoObjectBuilder, VideoObject
+from .image_object import ImageObjectBuilder, ImageObject
+from .document_object import DocumentObjectBuilder, DocumentObject
+
+class RichTextObject(KnowledgeObject):
+ def __init__(self, images: dict = {}, videos: dict = {}, documents: dict = {}, rich_texts: dict = {}):
+ desc = dict()
+ desc["images"] = images
+ desc["videos"] = videos
+ desc["documents"] = documents
+ desc["rich_texts"] = rich_texts
+
+ super().__init__(ObjectType.RichText, desc)
+
+
+ def add_image_with_key(self, key, image_object: ImageObject):
+ assert self.desc["images"][key] == None
+ self.desc["images"][key] = image_object
+
+ def add_image(self, image_object: ImageObject):
+ self.desc["images"][image_object.object_id()] = image_object
+
+ def get_image_with_key(self, key) -> ImageObject:
+ return self.desc["images"][key]
+
+ def get_images(self) -> dict:
+ return self.desc["images"]
+
+ def add_video_with_key(self, key, video_object: VideoObject):
+ assert self.desc["videos"][key] == None
+ self.desc["videos"][key] = video_object
+
+ def add_video(self, video_object: VideoObject):
+ self.desc["videos"][video_object.object_id()] = video_object
+
+ def get_video_with_key(self, key) -> VideoObject:
+ return self.desc["videos"][key]
+
+ def get_videos(self) -> dict:
+ return self.desc["videos"]
+
+
+ def add_document_with_key(self, key, document_object: DocumentObject):
+ assert self.desc["documents"][key] == None
+ self.desc["documents"][key] = document_object
+
+ def add_document(self, document_object: DocumentObject):
+ self.desc["documents"][document_object.object_id()] = document_object
+
+ def get_document_with_key(self, key) -> DocumentObject:
+ return self.desc["documents"][key]
+
+ def get_documents(self) -> dict:
+ return self.desc["documents"]
+
+ def add_rich_text_with_key(self, key, rich_text_object):
+ assert self.desc["rich_texts"][key] == None
+ self.desc["rich_texts"][key] = rich_text_object
+
+ def add_rich_text(self, rich_text_object):
+ self.desc["rich_texts"][rich_text_object.object_id()] = rich_text_object
+
+ def get_rich_text_with_key(self, key):
+ return self.desc["rich_texts"][key]
+
+ def get_rich_texts(self) -> dict:
+ return self.desc["rich_texts"]
+
+
+class RichTextObjectBuilder:
+ def __init__(self, folder: str):
+ self.folder = folder
+
+ def build(self) -> RichTextObject:
+ # TODO
+ return RichTextObject()
\ No newline at end of file
diff --git a/src/knowledge/core_object/video_object.py b/src/knowledge/core_object/video_object.py
new file mode 100644
index 0000000..56e9829
--- /dev/null
+++ b/src/knowledge/core_object/video_object.py
@@ -0,0 +1,84 @@
+from ..object import KnowledgeObject
+from ..data import ChunkList, ChunkListWriter
+from ..object import ObjectType
+from .. import KnowledgeStore
+
+# desc
+# meta
+# tags
+# hash: "file-hash",
+# info: {}
+# body
+# chunk_list: [chunk_id, chunk_id, ...]
+
+
+class VideoObject(KnowledgeObject):
+ def __init__(self, meta: dict, tags: dict, info: dict, chunk_list: ChunkList):
+ desc = dict()
+ body = dict()
+ desc["meta"] = meta
+ desc["tags"] = tags
+ desc["info"] = info
+ desc["hash"] = chunk_list.hash.to_base58()
+ body["chunk_list"] = chunk_list.chunk_list
+
+ super().__init__(ObjectType.Video, desc, body)
+
+ def get_meta(self):
+ return self.desc["meta"]
+
+ def get_tags(self):
+ return self.desc["tags"]
+
+ def get_info(self):
+ return self.desc["info"]
+
+ def get_hash(self):
+ return self.desc["hash"]
+
+ def get_chunk_list(self):
+ return self.body["chunk_list"]
+
+
+from moviepy.editor import VideoFileClip
+
+
+def get_video_info(video_path: str) -> dict:
+ clip = VideoFileClip(video_path)
+ return {
+ "duration": clip.duration, # Duration in seconds
+ "fps": clip.fps, # Frames per second
+ "nframes": clip.reader.nframes, # Total number of frames
+ "size": clip.size, # Size of the frames (width, height)
+ }
+
+
+class VideoObjectBuilder:
+ def __init__(self, meta: dict, tags: dict, video_file: str):
+ self.meta = meta
+ self.tags = tags
+ self.video_file = video_file
+ self.restore_file = False
+
+ def set_meta(self, meta: dict):
+ self.meta = meta
+ return self
+
+ def set_tags(self, tags: dict):
+ self.tags = tags
+ return self
+
+ def set_video_file(self, video_file: str):
+ self.video_file = video_file
+ return self
+
+ def set_restore_file(self, restore_file: bool):
+ self.restore_file = restore_file
+ return self
+
+ def build(self) -> VideoObject:
+ chunk_list = KnowledgeStore().get_chunk_list_writer().create_chunk_list_from_file(
+ self.video_file, 1024 * 1024 * 4, self.restore_file
+ )
+ info = get_video_info(self.video_file)
+ return VideoObject(self.meta, self.tags, info, chunk_list)
diff --git a/src/knowledge/data/__init__.py b/src/knowledge/data/__init__.py
new file mode 100644
index 0000000..3f470f0
--- /dev/null
+++ b/src/knowledge/data/__init__.py
@@ -0,0 +1,6 @@
+from .chunk import ChunkID, PositionType, PositionFileRange
+from .tracker import ChunkTracker
+from .chunk_store import ChunkStore
+from .writer import ChunkListWriter
+from .chunk_list import ChunkList
+from .reader import ChunkReader, Chunk
\ No newline at end of file
diff --git a/src/knowledge/data/chunk.py b/src/knowledge/data/chunk.py
new file mode 100644
index 0000000..76f2058
--- /dev/null
+++ b/src/knowledge/data/chunk.py
@@ -0,0 +1,43 @@
+from enum import IntEnum
+from ..object import ObjectID
+
+ChunkID = ObjectID
+
+class PositionType(IntEnum):
+ Unknown = 1
+ Device = 2
+ File = 3
+ FileRange = 4
+ ChunkStore = 5
+
+
+class PositionFileRange:
+ def __init__(self, path: str, range_begin: int, range_end: int):
+ self.path = path
+ self.range_begin = range_begin
+ self.range_end = range_end
+
+ def encode(self):
+ return f"{self.range_begin}:{self.range_end}:{self.path}"
+
+ @staticmethod
+ def decode(value: str):
+ parts = value.split(":")
+ if len(parts) < 3:
+ raise ValueError("Invalid input string")
+
+ try:
+ range_begin = int(parts[0])
+ range_end = int(parts[1])
+ except ValueError as e:
+ raise ValueError("Invalid range_begin or range_end string") from e
+
+ path = ":".join(parts[2:])
+ return PositionFileRange(path, range_begin, range_end)
+
+ def __str__(self):
+ return self.encode()
+
+ @staticmethod
+ def from_string(value: str):
+ return PositionFileRange.decode(value)
diff --git a/src/knowledge/data/chunk_list.py b/src/knowledge/data/chunk_list.py
new file mode 100644
index 0000000..969b03f
--- /dev/null
+++ b/src/knowledge/data/chunk_list.py
@@ -0,0 +1,14 @@
+from ..object import HashValue
+from .chunk import ChunkID
+from typing import List
+
+class ChunkList:
+ def __init__(self, chunk_list: List[ChunkID], hash: HashValue):
+ self.chunk_list = chunk_list
+ self.hash = hash
+
+ def __str__(self):
+ return self.hash.to_base58()
+
+ def __repr__(self):
+ return f"chunk_list: {self.chunk_list}, hash: {self.hash}"
\ No newline at end of file
diff --git a/src/knowledge/data/chunk_store.py b/src/knowledge/data/chunk_store.py
new file mode 100644
index 0000000..666b027
--- /dev/null
+++ b/src/knowledge/data/chunk_store.py
@@ -0,0 +1,28 @@
+import os
+import logging
+from ..object import FileBlobStorage
+from .chunk import ChunkID
+
+
+class ChunkStore:
+ def __init__(self, root_dir: str):
+ logging.info(f"will init chunk store, root_dir={root_dir}")
+
+ if not os.path.exists(root_dir):
+ os.makedirs(root_dir)
+
+ self.root = root_dir
+ self.blob = FileBlobStorage(root_dir)
+
+ def put_chunk(self, chunk_id: ChunkID, contents: bytes):
+ self.blob.put(chunk_id, contents)
+
+ def get_chunk(self, chunk_id: ChunkID) -> bytes:
+ return self.blob.get(chunk_id)
+
+ def delete_chunk(self, chunk_id: ChunkID):
+ self.blob.delete(chunk_id)
+
+ def get_chunk_file_path(self, chunk_id: ChunkID) -> str:
+ return self.blob.get_full_path(chunk_id, False)
+
\ No newline at end of file
diff --git a/src/knowledge/data/reader.py b/src/knowledge/data/reader.py
new file mode 100644
index 0000000..ede2648
--- /dev/null
+++ b/src/knowledge/data/reader.py
@@ -0,0 +1,95 @@
+from .chunk import ChunkID, PositionType, PositionFileRange
+from .chunk_store import ChunkStore
+from .tracker import ChunkTracker
+from ..object import HashValue
+import logging
+from typing import List
+import hashlib
+
+class Chunk:
+ def __init__(self, file_path: str, range_start: int, size: int = -1):
+ self.file_path = file_path
+ self.range_start = range_start
+ self.size = size
+
+ def read(self) -> bytes:
+ with open(self.file_path, 'rb') as f:
+ f.seek(self.range_start)
+ return f.read(self.size)
+
+
+
+class ChunkReader:
+ def __init__(self, chunk_store: ChunkStore, chunk_tracker: ChunkTracker):
+ self.chunk_store = chunk_store
+ self.chunk_tracker = chunk_tracker
+
+ def get_chunk(self, chunk_id: ChunkID) -> Chunk:
+ positions = self.chunk_tracker.get_position(chunk_id)
+ logging.info(f"chunk positions: {chunk_id}, {positions}")
+
+ if positions is None:
+ logging.warning(f"chunk not found: {chunk_id}")
+ return None
+
+ if len(positions) == 0:
+ logging.warning(f"chunk not found: {chunk_id}")
+ return None
+
+ for pos in positions:
+ [position, position_type] = pos
+ logging.info(f"chunk position: {chunk_id}, {position}, {position_type}")
+ if position_type == PositionType.ChunkStore:
+ file_path = self.chunk_store.get_chunk_file_path(chunk_id)
+ return Chunk(file_path, 0, -1)
+ elif position_type == PositionType.File:
+ return Chunk(position, 0, -1)
+ elif position_type == PositionType.FileRange:
+ file_range = PositionFileRange.decode(position)
+ return Chunk(file_range.path, file_range.range_begin, file_range.range_end - file_range.range_begin)
+ else:
+ raise ValueError(f"invalid position type: {position_type}")
+
+ logging.error(f"chunk not found: {chunk_id}")
+ return None
+
+ def get_chunk_list(self, chunk_list: List[ChunkID]) -> List[Chunk]:
+ return [self.get_chunk(chunk_id) for chunk_id in chunk_list]
+
+ def read_chunk_list(self, chunk_ids: List[ChunkID]) -> bytes:
+ for chunk_id in chunk_ids:
+ chunk = self.get_chunk(chunk_id)
+ if chunk is None:
+ raise ValueError(f"chunk not found: {chunk_id}")
+
+ yield chunk.read()
+
+ def read_chunk_list_to_single_bytes(self, chunk_ids: List[ChunkID]) -> bytes:
+ chunks = []
+ for chunk in self.read_chunk_list(chunk_ids):
+ chunks.append(chunk)
+
+ image_data = b''.join(chunks)
+ return image_data
+
+ def read_text_chunk_list(self, chunk_ids: List[ChunkID]) -> str:
+ for chunk_id in chunk_ids:
+ chunk = self.get_chunk(chunk_id)
+ if chunk is None:
+ raise ValueError(f"text chunk not found: {chunk_id}")
+
+ yield chunk.read().decode("utf-8")
+
+ def calc_file_hash(self, file_path: str) -> HashValue:
+ hash_obj = hashlib.sha256()
+ with open(file_path, "rb") as file:
+ while True:
+ chunk = file.read(1024 * 1024)
+ if not chunk:
+ break
+ hash_obj.update(chunk)
+ return HashValue(hash_obj.digest())
+
+ def calc_text_hash(self, text: str) -> HashValue:
+ hash_obj = hashlib.sha256()
+ hash_obj.update(text.encode("utf-8"))
\ No newline at end of file
diff --git a/src/knowledge/data/tracker.py b/src/knowledge/data/tracker.py
new file mode 100644
index 0000000..66024c9
--- /dev/null
+++ b/src/knowledge/data/tracker.py
@@ -0,0 +1,71 @@
+import sqlite3
+import time
+import logging
+import os
+from .chunk import ChunkID, PositionType, PositionFileRange
+from typing import List, Tuple
+
+class ChunkTracker:
+ def __init__(self, root_dir: str):
+ if not os.path.exists(root_dir):
+ os.makedirs(root_dir)
+ file = os.path.join(root_dir, "chunk_tracker.db")
+ logging.info(f"will init chunk tracker, db={file}")
+
+ self.conn = sqlite3.connect(file)
+ self.cursor = self.conn.cursor()
+ self.cursor.execute(
+ """
+ CREATE TABLE IF NOT EXISTS chunks (
+ id TEXT NOT NULL,
+ pos TEXT NOT NULL,
+ pos_type TINYINT NOT NULL,
+ insert_time UNSIGNED BIG INT NOT NULL,
+ update_time UNSIGNED BIG INT NOT NULL,
+ flags INTEGER DEFAULT 0,
+ PRIMARY KEY(id, pos, pos_type)
+ )
+ """
+ )
+ self.conn.commit()
+
+ def add_position(
+ self, chunk_id: ChunkID, position: str, position_type: PositionType
+ ):
+ logging.debug(f"add chunk position: {chunk_id}, {position}, {position_type}")
+
+ insert_time = update_time = int(time.time())
+ self.cursor.execute(
+ """
+ INSERT OR REPLACE INTO chunks (id, pos, pos_type, insert_time, update_time)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (
+ str(chunk_id),
+ position,
+ position_type.value,
+ insert_time,
+ update_time,
+ ),
+ )
+ self.conn.commit()
+
+ def remove_position(self, chunk_id: ChunkID):
+ logging.info(f"remove chunk position: {chunk_id}")
+
+ self.cursor.execute(
+ """
+ DELETE FROM chunks WHERE id = ?
+ """,
+ (str(chunk_id),),
+ )
+ self.conn.commit()
+
+ def get_position(self, chunk_id: ChunkID) -> List[Tuple[str, PositionType]]:
+ self.cursor.execute(
+ """
+ SELECT pos, pos_type FROM chunks WHERE id = ?
+ """,
+ (str(chunk_id),),
+ )
+ return self.cursor.fetchmany()
diff --git a/src/knowledge/data/writer.py b/src/knowledge/data/writer.py
new file mode 100644
index 0000000..0381bc6
--- /dev/null
+++ b/src/knowledge/data/writer.py
@@ -0,0 +1,214 @@
+import os
+import hashlib
+import re
+import tiktoken
+import logging
+from typing import Callable, Iterable, Optional, Tuple, List
+from .chunk_store import ChunkStore
+from .chunk import ChunkID, PositionFileRange, PositionType
+from ..object import HashValue
+from .tracker import ChunkTracker
+from .chunk_list import ChunkList
+
+def _join_docs(docs: List[str], separator: str) -> Optional[str]:
+ text = separator.join(docs)
+ text = text.strip()
+ if text == "":
+ return None
+ else:
+ return text
+
+def _merge_splits(
+ splits: Iterable[str],
+ separator: str,
+ chunk_size: int,
+ chunk_overlap: int,
+ length_function: Callable[[str], int]
+ ) -> List[str]:
+ # We now want to combine these smaller pieces into medium size
+ # chunks to send to the LLM.
+ separator_len = length_function(separator)
+
+ docs = []
+ current_doc: List[str] = []
+ total = 0
+ for d in splits:
+ _len = length_function(d)
+ if (
+ total + _len + (separator_len if len(current_doc) > 0 else 0)
+ > chunk_size
+ ):
+ if total > chunk_size:
+ logging.warning(
+ f"Created a chunk of size {total}, "
+ f"which is longer than the specified {self._chunk_size}"
+ )
+ if len(current_doc) > 0:
+ doc = _join_docs(current_doc, separator)
+ if doc is not None:
+ docs.append(doc)
+ # Keep on popping if:
+ # - we have a larger chunk than in the chunk overlap
+ # - or if we still have any chunks and the length is long
+ while total > chunk_overlap or (
+ total + _len + (separator_len if len(current_doc) > 0 else 0)
+ > chunk_size
+ and total > 0
+ ):
+ total -= length_function(current_doc[0]) + (
+ separator_len if len(current_doc) > 1 else 0
+ )
+ current_doc = current_doc[1:]
+ current_doc.append(d)
+ total += _len + (separator_len if len(current_doc) > 1 else 0)
+ doc = _join_docs(current_doc, separator)
+ if doc is not None:
+ docs.append(doc)
+ return docs
+
+
+def _split_text_with_regex(
+ text: str, separator: str, keep_separator: bool
+) -> List[str]:
+ # Now that we have the separator, split the text
+ if separator:
+ if keep_separator:
+ # The parentheses in the pattern keep the delimiters in the result.
+ _splits = re.split(f"({separator})", text)
+ splits = [_splits[i] + _splits[i + 1] for i in range(1, len(_splits), 2)]
+ if len(_splits) % 2 == 0:
+ splits += _splits[-1:]
+ splits = [_splits[0]] + splits
+ else:
+ splits = re.split(separator, text)
+ else:
+ splits = list(text)
+ return [s for s in splits if s != ""]
+
+
+def _split_text(
+ text: str,
+ separators: List[str],
+ chunk_size: int,
+ chunk_overlap: int,
+ length_function: Callable[[str], int]
+ ) -> List[str]:
+
+ """Split incoming text and return chunks."""
+ final_chunks = []
+ # Get appropriate separator to use
+ separator = separators[-1]
+ new_separators = []
+ for i, _s in enumerate(separators):
+ _separator = re.escape(_s)
+ if _s == "":
+ separator = _s
+ break
+ if re.search(_separator, text):
+ separator = _s
+ new_separators = separators[i + 1 :]
+ break
+
+ keep_separator = True
+ _separator = re.escape(separator)
+ splits = _split_text_with_regex(text, _separator, keep_separator)
+
+ # Now go merging things, recursively splitting longer texts.
+ _good_splits = []
+ _separator = "" if keep_separator else separator
+ for s in splits:
+ if length_function(s) < chunk_size:
+ _good_splits.append(s)
+ else:
+ if _good_splits:
+ merged_text = _merge_splits(_good_splits, _separator, chunk_size, chunk_overlap, length_function)
+ final_chunks.extend(merged_text)
+ _good_splits = []
+ if not new_separators:
+ final_chunks.append(s)
+ else:
+ other_info = _split_text(s, new_separators, chunk_size, chunk_overlap, length_function)
+ final_chunks.extend(other_info)
+ if _good_splits:
+ merged_text = _merge_splits(_good_splits, _separator, chunk_size, chunk_overlap, length_function)
+ final_chunks.extend(merged_text)
+ return final_chunks
+
+class ChunkListWriter:
+ def __init__(self, chunk_store: ChunkStore, chunk_tracker: ChunkTracker):
+ self.chunk_store = chunk_store
+ self.chunk_tracker = chunk_tracker
+
+ def create_chunk_list_from_file(
+ self, file_path: str, chunk_size: int, restore: bool
+ ) -> ChunkList:
+ assert (
+ chunk_size % (1024 * 1024) == 0
+ ), "chunk size should be an integral multiple of 1MB"
+ chunk_list = []
+ hash_obj = hashlib.sha256()
+
+ with open(file_path, "rb") as file:
+ while True:
+ chunk = file.read(chunk_size)
+ if not chunk:
+ break
+
+ chunk_len = len(chunk)
+ chunk_id = ChunkID.hash_data(chunk)
+ chunk_list.append(chunk_id)
+
+ hash_obj.update(chunk)
+
+ if restore:
+ self.chunk_tracker.add_position(
+ chunk_id, file_path, PositionType.ChunkStore
+ )
+ self.chunk_store.put_chunk(chunk_id, chunk)
+ else:
+ pos = file.tell()
+ file_range = PositionFileRange(
+ file_path, pos - chunk_len, pos
+ )
+ self.chunk_tracker.add_position(
+ chunk_id, str(file_range), PositionType.FileRange
+ )
+
+ file_hash = HashValue(hash_obj.digest())
+ # print(f"calc file hash: {file_path}, {file_hash}")
+
+ return ChunkList(chunk_list, file_hash)
+
+ def create_chunk_list_from_text(
+ self,
+ text: str,
+ chunk_size: int = 4000,
+ chunk_overlap: int = 200,
+ separators: str = ["\n\n", "\n", " ", ""]
+ ) -> ChunkList:
+ enc = tiktoken.encoding_for_model("gpt-3.5-turbo")
+
+ def length_function(text: str) -> int:
+ return len(
+ enc.encode(
+ text,
+ allowed_special=set(),
+ disallowed_special="all",
+ )
+ )
+
+ text_list = _split_text(text, separators, chunk_size, chunk_overlap, length_function)
+ chunk_list = []
+ hash_obj = hashlib.sha256()
+
+ for text in text_list:
+ chunk_bytes = text.encode("utf-8")
+ hash_obj.update(chunk_bytes)
+
+ chunk_id = ChunkID.hash_data(chunk_bytes)
+ chunk_list.append(chunk_id)
+ self.chunk_tracker.add_position(chunk_id, "", PositionType.ChunkStore)
+ self.chunk_store.put_chunk(chunk_id, chunk_bytes)
+
+ hash = HashValue(hash_obj.digest())
+ return ChunkList(chunk_list, hash)
\ No newline at end of file
diff --git a/src/knowledge/object/__init__.py b/src/knowledge/object/__init__.py
new file mode 100644
index 0000000..291715f
--- /dev/null
+++ b/src/knowledge/object/__init__.py
@@ -0,0 +1,6 @@
+from .object import KnowledgeObject
+from .blob import FileBlobStorage
+from .hash import HashValue, hash_data
+from .relation import ObjectRelationStore
+from .object_store import ObjectStore
+from .object_id import ObjectID, ObjectType
diff --git a/src/knowledge/object/blob.py b/src/knowledge/object/blob.py
new file mode 100644
index 0000000..08fb0bf
--- /dev/null
+++ b/src/knowledge/object/blob.py
@@ -0,0 +1,63 @@
+import os
+import shutil
+from .object import ObjectID
+import logging
+logger = logging.getLogger(__name__)
+
+
+class FileBlobStorage:
+ def __init__(self, root):
+ self.root = root
+
+ def get_full_path(self, object_id: ObjectID, auto_create: bool = True):
+ if os.name == "nt": # Windows
+ hash_str = object_id.to_base36()
+ len = 3
+ else:
+ hash_str = str(object_id)
+ len = 2
+
+ tmp, first = hash_str[:-len], hash_str[-len:]
+ second = tmp[-len:]
+
+ if os.name == "nt": # Windows
+ if second in ["con", "aux", "nul", "prn"]:
+ second = tmp[-(len + 1) :]
+ if first in ["con", "aux", "nul", "prn"]:
+ first = f"{first}_"
+
+ path = os.path.join(self.root, first, second)
+ if auto_create and not os.path.exists(path):
+ os.makedirs(path)
+
+ path = os.path.join(path, hash_str)
+
+ return path
+
+ def write_sync(self, path: str, contents: bytes):
+ with open(path, "wb") as f:
+ f.write(contents)
+
+ def put(self, object_id: ObjectID, contents: bytes):
+ full_path = self.get_full_path(object_id)
+ if os.path.exists(full_path):
+ logger.warning(f"will replace object: {object_id}")
+
+ self.write_sync(full_path, contents)
+
+ def get(self, object_id: ObjectID) -> bytes:
+ full_path = self.get_full_path(object_id)
+ if not os.path.exists(full_path):
+ return None
+
+ with open(full_path, "rb") as f:
+ return f.read()
+
+ def delete(self, object_id: ObjectID):
+ full_path = self.get_full_path(object_id)
+ if os.path.exists(full_path):
+ os.remove(full_path)
+
+ def exists(self, object_id: ObjectID) -> bool:
+ full_path = self.get_full_path(object_id)
+ return os.path.exists(full_path)
diff --git a/src/knowledge/object/hash.py b/src/knowledge/object/hash.py
new file mode 100644
index 0000000..5063814
--- /dev/null
+++ b/src/knowledge/object/hash.py
@@ -0,0 +1,42 @@
+import hashlib
+import base58
+import base36
+
+class HashValue:
+ def __init__(self, value: bytes):
+ assert len(value) == 32, "HashValue must be 32 bytes long"
+ self.value = value
+
+ def __str__(self) -> str:
+ return self.to_base58()
+
+ @staticmethod
+ def hash_data(data):
+ return hash_data(data)
+
+ def to_base58(self):
+ return base58.b58encode(self.value).decode()
+
+ @staticmethod
+ def from_base58(s):
+ return HashValue(base58.b58decode(s))
+
+ def to_base36(self):
+ # Convert the bytes to int before encoding
+ num = int.from_bytes(self.value, 'big')
+ return base36.dumps(num)
+
+ @staticmethod
+ def from_base36(s):
+ # Decode to int and then convert to bytes
+ num = base36.loads(s)
+ return HashValue(num.to_bytes((num.bit_length() + 7) // 8, 'big'))
+
+
+HASH_VALUE_LEN = 32
+
+
+def hash_data(data: bytes):
+ sha256 = hashlib.sha256()
+ sha256.update(data)
+ return HashValue(sha256.digest())
diff --git a/src/knowledge/object/object.py b/src/knowledge/object/object.py
new file mode 100644
index 0000000..ac6f2af
--- /dev/null
+++ b/src/knowledge/object/object.py
@@ -0,0 +1,67 @@
+# define a object type enum
+from __future__ import annotations
+from abc import ABC, abstractmethod
+from enum import Enum
+
+from .object_id import ObjectID, ObjectType
+import hashlib
+import json
+import pickle
+from typing import Any
+
+
+class ObjectEnhancedJSONEncoder(json.JSONEncoder):
+ def default(self, o: Any) -> Any:
+ if isinstance(o, ObjectID):
+ return o.to_base58()
+
+ return super().default(o)
+
+
+class KnowledgeObject(ABC):
+ def __init__(self, object_type: ObjectType, desc: dict = {}, body: dict = {}):
+ self.desc = desc
+ self.body = body
+ self.object_type = object_type
+
+ def get_object_type(self) -> ObjectType:
+ return self.object_type
+
+ def object_id(self) -> ObjectID:
+ return self.calculate_id()
+
+ def set_desc_with_key_value(self, key, value):
+ self.desc[key] = value
+
+ def get_desc_with_key(self, key):
+ return self.desc.get(key)
+
+ def get_desc(self) -> dict:
+ return self.desc
+
+ def set_body_with_key_value(self, key, value):
+ self.body[key] = value
+
+ def get_body_with_key(self, key):
+ return self.body.get(key)
+
+ def get_body(self) -> dict:
+ return self.body
+
+ def calculate_id(self):
+ # Convert the object_type and desc to string and compute the SHA256 hash
+ data = json.dumps(
+ {"object_type": self.object_type, "desc": self.desc},
+ cls=ObjectEnhancedJSONEncoder,
+ )
+ sha256 = hashlib.sha256()
+ sha256.update(data.encode())
+ hash_bytes = sha256.digest()
+ return ObjectID(bytes([self.object_type]) + hash_bytes[1:])
+
+ def encode(self) -> bytes:
+ return pickle.dumps(self)
+
+ @staticmethod
+ def decode(data: bytes) -> "ImageObject":
+ return pickle.loads(data)
diff --git a/src/knowledge/object/object_id.py b/src/knowledge/object/object_id.py
new file mode 100644
index 0000000..46955f4
--- /dev/null
+++ b/src/knowledge/object/object_id.py
@@ -0,0 +1,58 @@
+# define a object type enum
+from abc import ABC, abstractmethod
+from enum import IntEnum
+from .hash import HashValue
+import base58
+import base36
+
+
+class ObjectType(IntEnum):
+ Chunk = 7
+ Image = 101
+ Video = 102
+ Document = 103
+ RichText = 104
+ Email = 105
+
+
+# define a object ID class to identify a object
+class ObjectID: # pylint: disable=too-few-public-methods
+ def __init__(self, value: bytes):
+ assert len(value) == 32, "ObjectID must be 32 bytes long"
+ self.value = value
+
+ def __str__(self):
+ return self.to_base58()
+
+ def to_base58(self):
+ return base58.b58encode(self.value).decode()
+
+ @staticmethod
+ def from_base58(s):
+ return ObjectID(base58.b58decode(s))
+
+ def to_base36(self):
+ # Convert the bytes to int before encoding
+ num = int.from_bytes(self.value, "big")
+ return base36.dumps(num)
+
+ @staticmethod
+ def from_base36(s):
+ # Decode to int and then convert to bytes
+ num = base36.loads(s)
+ return ObjectID(num.to_bytes((num.bit_length() + 7) // 8, "big"))
+
+ @staticmethod
+ def new_chunk_id(chunk_hash: HashValue):
+ assert len(chunk_hash.value) == 32, "ObjectID must be 32 bytes long"
+ return ObjectID(bytes([ObjectType.Chunk]) + chunk_hash.value[1:])
+
+ def get_object_type(self) -> ObjectType:
+ return ObjectType(self.value[0])
+
+ @staticmethod
+ def hash_data(data: bytes):
+ return ObjectID.new_chunk_id(HashValue.hash_data(data))
+
+ def __eq__(self, other) -> bool:
+ return self.value == other.value
\ No newline at end of file
diff --git a/src/knowledge/object/object_store.py b/src/knowledge/object/object_store.py
new file mode 100644
index 0000000..db93251
--- /dev/null
+++ b/src/knowledge/object/object_store.py
@@ -0,0 +1,25 @@
+import os
+import logging
+from .blob import FileBlobStorage
+from .object_id import ObjectID
+
+
+class ObjectStore:
+ def __init__(self, root_dir: str):
+ logging.info(f"will init object blob store, root_dir={root_dir}")
+
+ blob_dir = os.path.join(root_dir, "blob")
+ if not os.path.exists(blob_dir):
+ logging.info(f"will create blob dir: {blob_dir}")
+ os.makedirs(blob_dir)
+ self.blob = FileBlobStorage(blob_dir)
+
+ def put_object(self, object_id: ObjectID, contents: bytes):
+ logging.info(f"will put object: {object_id}")
+ self.blob.put(object_id, contents)
+
+ def get_object(self, object_id: ObjectID) -> bytes:
+ return self.blob.get(object_id)
+
+ def delete_object(self, object_id: ObjectID):
+ self.blob.delete(object_id)
diff --git a/src/knowledge/object/relation.py b/src/knowledge/object/relation.py
new file mode 100644
index 0000000..f273279
--- /dev/null
+++ b/src/knowledge/object/relation.py
@@ -0,0 +1,104 @@
+# define a relation store class
+from .object_id import ObjectID
+import sqlite3
+from typing import List, Tuple, Optional
+import logging
+import os
+from enum import IntEnum
+
+
+class ObjectRelationType(IntEnum):
+ Parent = 1
+
+
+class ObjectRelationStore:
+ def __init__(self, root_dir: str):
+ if not os.path.exists(root_dir):
+ os.makedirs(root_dir)
+ file = os.path.join(root_dir, "relation.db")
+ logging.info(f"will init object relation store, db={file}")
+
+ self.conn = sqlite3.connect(file)
+ self.cursor = self.conn.cursor()
+ self.cursor.execute(
+ """
+ CREATE TABLE IF NOT EXISTS relations (
+ object_id TEXT,
+ assoc_id TEXT,
+ relation_type TEXT,
+ PRIMARY KEY (object_id, assoc_id, relation_type)
+ )
+ """
+ )
+
+ def add_relation(
+ self,
+ object_id: ObjectID,
+ assoc_id: ObjectID,
+ relation_type: ObjectRelationType = ObjectRelationType.Parent,
+ ):
+ if relation_type == None:
+ relation_type = ObjectRelationType.Parent
+
+ self.cursor.execute(
+ """
+ INSERT OR IGNORE INTO relations (object_id, assoc_id, relation_type)
+ VALUES (?, ?, ?)
+ """,
+ (str(object_id), str(assoc_id), relation_type.value),
+ )
+ self.conn.commit()
+
+ def get_related_objects(
+ self, object_id: ObjectID, relation_type: Optional[ObjectRelationType] = None
+ ) -> List[ObjectID]:
+ if relation_type:
+ self.cursor.execute(
+ """
+ SELECT assoc_id FROM relations WHERE object_id = ? AND relation_type = ?
+ """,
+ (str(object_id), relation_type.value),
+ )
+ else:
+ self.cursor.execute(
+ """
+ SELECT assoc_id FROM relations WHERE object_id = ?
+ """,
+ (str(object_id),),
+ )
+ return [ObjectID.from_base58(row[0]) for row in self.cursor.fetchall()]
+
+ def get_related_root_objects(
+ self, object_id: ObjectID, relation_type: Optional[ObjectRelationType] = None
+ ) -> List[ObjectID]:
+ root_objects = []
+ related_objects = self.get_related_objects(object_id, relation_type)
+ history = []
+ history.append(object_id)
+
+ while related_objects:
+ for obj in related_objects:
+ next_related_objects = self.get_related_objects(obj, relation_type)
+ if not next_related_objects:
+ if obj not in root_objects:
+ root_objects.append(obj)
+ else:
+ for related_object in next_related_objects:
+ if obj not in history:
+ related_objects.append(related_object)
+ else:
+ logging.warning(
+ f"loop detected: {obj} <-> {related_object}"
+ )
+ related_objects = next_related_objects
+
+ return root_objects
+
+ def delete_relation(self, object_id: ObjectID):
+ self.cursor.execute(
+ """
+ DELETE FROM relations WHERE object_id = ?
+ """,
+ (str(object_id),),
+ )
+ self.conn.commit()
diff --git a/src/knowledge/store.py b/src/knowledge/store.py
new file mode 100644
index 0000000..d8b4bbb
--- /dev/null
+++ b/src/knowledge/store.py
@@ -0,0 +1,66 @@
+import os
+
+from .object import ObjectStore, ObjectRelationStore
+from .data import ChunkStore, ChunkTracker, ChunkListWriter, ChunkReader
+from .vector import ChromaVectorStore, VectorBase
+import logging
+
+
+# KnowledgeStore class, which aggregates ChunkStore, ChunkTracker, and ObjectStore, and is a global singleton that makes it easy to use these three built-in store examples
+class KnowledgeStore:
+ _instance = None
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+
+ import aios_kernel
+ knowledge_dir = aios_kernel.storage.AIStorage().get_myai_dir() / "knowledge"
+
+ if not os.path.exists(knowledge_dir):
+ os.makedirs(knowledge_dir)
+
+ cls._instance.__singleton_init__(knowledge_dir)
+
+ return cls._instance
+
+ def __singleton_init__(self, root_dir: str):
+ logging.info(f"will init knowledge store, root_dir={root_dir}")
+
+ self.root = root_dir
+
+ relation_store_dir = os.path.join(root_dir, "relation")
+ self.relation_store = ObjectRelationStore(relation_store_dir)
+
+ object_store_dir = os.path.join(root_dir, "object")
+ self.object_store = ObjectStore(object_store_dir)
+
+ chunk_store_dir = os.path.join(root_dir, "chunk")
+ self.chunk_store = ChunkStore(chunk_store_dir)
+ self.chunk_tracker = ChunkTracker(chunk_store_dir)
+ self.chunk_list_writer = ChunkListWriter(self.chunk_store, self.chunk_tracker)
+ self.chunk_reader = ChunkReader(self.chunk_store, self.chunk_tracker)
+ self.vector_store = {}
+
+ def get_relation_store(self) -> ObjectRelationStore:
+ return self.relation_store
+
+ def get_object_store(self) -> ObjectStore:
+ return self.object_store
+
+ def get_chunk_store(self) -> ChunkStore:
+ return self.chunk_store
+
+ def get_chunk_tracker(self) -> ChunkTracker:
+ return self.chunk_tracker
+
+ def get_chunk_list_writer(self) -> ChunkListWriter:
+ return self.chunk_list_writer
+
+ def get_chunk_reader(self) -> ChunkReader:
+ return self.chunk_reader
+
+ def get_vector_store(self, model_name: str) -> VectorBase:
+ if model_name not in self.vector_store:
+ self.vector_store[model_name] = ChromaVectorStore(self.root, model_name)
+ return self.vector_store[model_name]
diff --git a/src/knowledge/vector/__init__.py b/src/knowledge/vector/__init__.py
new file mode 100644
index 0000000..b3f0d78
--- /dev/null
+++ b/src/knowledge/vector/__init__.py
@@ -0,0 +1,2 @@
+from .vector_base import VectorBase
+from .chroma_store import ChromaVectorStore
diff --git a/src/knowledge/vector/chroma_store.py b/src/knowledge/vector/chroma_store.py
new file mode 100644
index 0000000..8cd3084
--- /dev/null
+++ b/src/knowledge/vector/chroma_store.py
@@ -0,0 +1,51 @@
+from .vector_base import VectorBase
+from ..object import ObjectID
+import chromadb
+import logging
+import os
+
+
+class ChromaVectorStore(VectorBase):
+ def __init__(self, root_dir, model_name: str) -> None:
+ super().__init__(model_name)
+
+ logging.info(
+ "will init chroma vector store, model={}".format(model_name)
+ )
+
+ directory = os.path.join(root_dir, "vector")
+ logging.info("will use vector store: {}".format(directory))
+
+ client = chromadb.PersistentClient(
+ path=directory, settings=chromadb.Settings(anonymized_telemetry=False)
+ )
+ # client = chromadb.Client()
+
+ collection_name = "coll_{}".format(model_name)
+ logging.info("will init chroma colletion: %s", collection_name)
+
+ collection = client.get_or_create_collection(collection_name)
+ self.collection = collection
+
+ async def insert(self, vector: [float], id: ObjectID):
+ logging.info(f"will insert vector: {len(vector)} id: {str(id)}")
+ logging.debug(f"vector is {vector}")
+ self.collection.add(
+ embeddings=vector,
+ ids=str(id),
+ )
+
+ async def query(self, vector: [float], top_k: int) -> [ObjectID]:
+ ret = self.collection.query(
+ query_embeddings=vector,
+ n_results=top_k,
+ )
+ logging.info(f"query result {ret}")
+ if len(ret['ids']) == 0:
+ return []
+ return list(map(ObjectID.from_base58, ret["ids"][0]))
+
+ async def delete(self, id: ObjectID):
+ self.collection.delete(
+ ids=id,
+ )
diff --git a/src/knowledge/vector/vector_base.py b/src/knowledge/vector/vector_base.py
new file mode 100644
index 0000000..83276ed
--- /dev/null
+++ b/src/knowledge/vector/vector_base.py
@@ -0,0 +1,16 @@
+# import the ObjectID class
+from ..object import ObjectID
+
+# define a vector base class
+class VectorBase:
+ def __init__(self, model_name) -> None:
+ self.model_name = model_name
+
+ async def insert(self, vector: [float], id: ObjectID):
+ pass
+
+ async def query(self, vector: [float], top_k: int) -> [ObjectID]:
+ pass
+
+ async def delete(self, id: ObjectID):
+ pass
\ No newline at end of file
diff --git a/src/requirements.txt b/src/requirements.txt
new file mode 100644
index 0000000..b83e564
--- /dev/null
+++ b/src/requirements.txt
@@ -0,0 +1,140 @@
+aiofiles>=23.2.1
+aiohttp>=3.8.5
+aioimaplib>=1.0.1
+aiosignal>=1.3.1
+aiosmtplib>=2.0.2
+anyio>=4.0.0
+async-timeout>=4.0.3
+attrs>=23.1.0
+backoff>=2.2.1
+base36>=0.1.1
+base58>=2.1.1
+beautifulsoup4>=4.12.2
+cachetools>=5.3.1
+certifi>=2023.7.22
+charset-normalizer>=3.2.0
+chroma-hnswlib>=0.7.1
+chromadb>=0.4.0
+click>=8.1.7
+colorama>=0.4.6
+coloredlogs>=15.0.1
+decorator>=4.4.2
+fastapi>=0.99.1
+filelock>=3.12.3
+flatbuffers>=23.5.26
+frozenlist>=1.4.0
+fsspec>=2023.9.0
+google>=3.0.0
+google-api-core>=2.11.1
+google-auth>=2.23.0
+google-cloud>=0.34.0
+google-cloud-texttospeech>=2.14.1
+googleapis-common-protos>=1.60.0
+h11>=0.14.0
+httpcore>=0.17.3
+httptools>=0.6.0
+httpx>=0.24.1
+huggingface-hub>=0.16.4
+humanfriendly>=10.0
+idna>=3.4
+imageio>=2.31.3
+imageio-ffmpeg>=0.4.8
+importlib-resources>=6.0.1
+mail-parser>=3.15.0
+monotonic>=1.6
+moviepy>=1.0.0
+mpmath>=1.3.0
+multidict>=6.0.4
+numpy>=1.25.2
+onnxruntime>=1.15.1
+openai>=0.28.0
+overrides>=7.4.0
+packaging>=23.1
+pandas>=2.1.0
+Pillow>=10.0.0
+posthog>=3.0.2
+proglog>=0.1.10
+prompt-toolkit>=3.0.39
+proto-plus>=1.22.3
+pulsar-client>=3.3.0
+pyasn1>=0.5.0
+pyasn1-modules>=0.3.0
+pydantic>=1.10.12
+PyPika>=0.48.9
+pyreadline3>=3.4.1
+python-dateutil>=2.8.2
+python-dotenv>=1.0.0
+python-telegram-bot>=20.5
+pytz>=2023.3.post1
+PyYAML>=6.0.1
+requests>=2.31.0
+rsa>=4.9
+simplejson>=3.19.1
+six>=1.16.0
+sniffio>=1.3.0
+soupsieve>=2.5
+starlette>=0.27.0
+sympy>=1.12
+tokenizers>=0.14.0
+toml>=0.10.0
+protobuf
+grpcio
+grpcio-status
+h11==0.14.0
+httpcore==0.17.3
+httptools==0.6.0
+httpx==0.24.1
+huggingface-hub==0.16.4
+humanfriendly==10.0
+idna==3.4
+imageio==2.31.3
+imageio-ffmpeg==0.4.8
+importlib-resources==6.0.1
+mail-parser==3.15.0
+monotonic==1.6
+moviepy==1.0.0
+mpmath==1.3.0
+multidict==6.0.4
+numpy==1.25.2
+onnxruntime==1.15.1
+openai==0.28.0
+overrides==7.4.0
+packaging==23.1
+pandas==2.1.0
+Pillow==10.0.0
+posthog==3.0.2
+proglog==0.1.10
+prompt-toolkit==3.0.39
+proto-plus==1.22.3
+protobuf
+pulsar-client==3.3.0
+pyasn1==0.5.0
+pyasn1-modules==0.3.0
+pydantic==1.10.12
+PyPika==0.48.9
+pyreadline3==3.4.1
+python-dateutil==2.8.2
+python-dotenv==1.0.0
+python-telegram-bot==20.5
+pytz==2023.3.post1
+PyYAML==6.0.1
+requests==2.31.0
+rsa==4.9
+simplejson==3.19.1
+six==1.16.0
+sniffio==1.3.0
+soupsieve==2.5
+starlette==0.27.0
+sympy==1.12
+telegram==0.0.1
+tokenizers==0.14.0
+toml==0.10.0
+pysocks
+chardet
+pydub
+aiosqlite
+python-telegram-bot
+pydub
+stability_sdk
+sentence-transformers==2.2.2
+tiktoken
\ No newline at end of file
diff --git a/src/service/aios_shell/aios_shell.py b/src/service/aios_shell/aios_shell.py
new file mode 100644
index 0000000..b10ac84
--- /dev/null
+++ b/src/service/aios_shell/aios_shell.py
@@ -0,0 +1,782 @@
+# aiso shell like bash for linux
+import asyncio
+import sys
+import os
+import logging
+import re
+import toml
+import shlex
+from logging.handlers import RotatingFileHandler
+
+from typing import Any, Optional, TypeVar, Tuple, Sequence
+import argparse
+
+
+from prompt_toolkit import HTML, PromptSession, prompt,print_formatted_text
+from prompt_toolkit.formatted_text import FormattedText
+from prompt_toolkit.selection import SelectionState
+from prompt_toolkit.history import FileHistory
+from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
+from prompt_toolkit.completion import WordCompleter
+from prompt_toolkit.styles import Style
+
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../../')
+
+
+
+import proxy
+from aios_kernel import *
+
+
+sys.path.append(directory + '/../../component/')
+from agent_manager import AgentManager
+from workflow_manager import WorkflowManager
+
+
+logger = logging.getLogger(__name__)
+
+shell_style = Style.from_dict({
+ 'title': '#87d7ff bold', #RGB
+ 'content': '#007f00', # resp content
+ 'prompt': '#00FF00',
+ 'error': '#8F0000 bold'
+})
+
+
+class AIOS_Shell:
+ def __init__(self,username:str) -> None:
+ self.username = username
+ self.current_target = "_"
+ self.current_topic = "default"
+ self.is_working = True
+
+ def declare_all_user_config(self):
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+ contact_config_path =os.path.abspath(f"{user_data_dir}/contacts.toml")
+ cm = ContactManager.get_instance(contact_config_path)
+ cm.load_data()
+
+ user_config = AIStorage.get_instance().get_user_config()
+ user_config.add_user_config("username","username is your full name when using AIOS",False,None)
+ user_config.add_user_config("telegram","Your telgram username",False,None)
+ user_config.add_user_config("email","Your email",False,None)
+
+ user_config.add_user_config("feature.llama","enable Local-llama feature",True,"False")
+ user_config.add_user_config("feature.aigc","enable AIGC feature",True,"False")
+
+ openai_node = OpenAI_ComputeNode.get_instance()
+ openai_node.declare_user_config()
+
+ user_config.add_user_config("shell.current","last opened target and topic",True,"default@Jarvis")
+ proxy.declare_user_config()
+
+ google_text_to_speech = GoogleTextToSpeechNode.get_instance()
+ google_text_to_speech.declare_user_config()
+
+ Local_Stability_ComputeNode.declare_user_config()
+
+ #Stability_ComputeNode.declare_user_config()
+
+
+
+ async def _handle_no_target_msg(self,bus:AIBus,target_id:str) -> bool:
+ agent : AIAgent = await AgentManager.get_instance().get(target_id)
+ if agent is not None:
+ bus.register_message_handler(target_id,agent._process_msg)
+ return True
+
+ a_workflow = await WorkflowManager.get_instance().get_workflow(target_id)
+ if a_workflow is not None:
+ bus.register_message_handler(target_id,a_workflow._process_msg)
+ return True
+
+ return False
+
+ async def is_agent(self,target_id:str) -> bool:
+ agent : AIAgent = await AgentManager.get_instance().get(target_id)
+ if agent is not None:
+ return True
+ else:
+ return False
+
+ async def initial(self) -> bool:
+ cm = ContactManager.get_instance()
+ owenr = cm.find_contact_by_name(self.username)
+ if owenr is None:
+ owenr = Contact(self.username)
+ owenr.added_by = self.username
+ owenr.is_family_member = True
+ owenr.email = AIStorage.get_instance().get_user_config().get_value("email")
+ owenr.telegram = AIStorage.get_instance().get_user_config().get_value("telegram")
+
+ cm.add_family_member(self.username,owenr)
+
+ knowledge_env = KnowledgeEnvironment("knowledge")
+ Environment.set_env_by_id("knowledge",knowledge_env)
+
+ cal_env = CalenderEnvironment("calender")
+ await cal_env.start()
+ Environment.set_env_by_id("calender",cal_env)
+
+ workspace_env = WorkspaceEnvironment("bash")
+ Environment.set_env_by_id("bash",workspace_env)
+
+ paint_env = PaintEnvironment("paint")
+ Environment.set_env_by_id("paint",paint_env)
+
+ if await AgentManager.get_instance().initial() is not True:
+ logger.error("agent manager initial failed!")
+ return False
+ if await WorkflowManager.get_instance().initial() is not True:
+ logger.error("workflow manager initial failed!")
+ return False
+
+ open_ai_node = OpenAI_ComputeNode.get_instance()
+ if await open_ai_node.initial() is not True:
+ logger.error("openai node initial failed!")
+ return False
+ ComputeKernel.get_instance().add_compute_node(open_ai_node)
+
+ llama_nodes = ComputeNodeConfig.get_instance().initial()
+ for llama_node in llama_nodes:
+ llama_node.start()
+ ComputeKernel.get_instance().add_compute_node(llama_node)
+
+ if await AIStorage.get_instance().is_feature_enable("llama"):
+ llama_ai_node = LocalLlama_ComputeNode()
+ if await llama_ai_node.initial() is True:
+ await llama_ai_node.start()
+ ComputeKernel.get_instance().add_compute_node(llama_ai_node)
+ else:
+ logger.error("llama node initial failed!")
+ await AIStorage.get_instance().set_feature_init_result("llama",False)
+
+
+ if await AIStorage.get_instance().is_feature_enable("aigc"):
+ try:
+ google_text_to_speech_node = GoogleTextToSpeechNode.get_instance()
+ google_text_to_speech_node.init()
+ ComputeKernel.get_instance().add_compute_node(google_text_to_speech_node)
+ except Exception as e:
+ logger.error(f"google text to speech node initial failed! {e}")
+ await AIStorage.get_instance.set_feature_init_result("aigc",False)
+
+ # stability_api_node = Stability_ComputeNode()
+ # if await stability_api_node.initial() is not True:
+ # logger.error("stability api node initial failed!")
+ # ComputeKernel.get_instance().add_compute_node(stability_api_node)
+
+
+
+ local_st_text_compute_node = LocalSentenceTransformer_Text_ComputeNode()
+ if local_st_text_compute_node.initial() is not True:
+ logger.error("local sentence transformer text embedding node initial failed!")
+ else:
+ ComputeKernel.get_instance().add_compute_node(local_st_text_compute_node)
+
+ local_st_image_compute_node = LocalSentenceTransformer_Image_ComputeNode()
+ if local_st_image_compute_node.initial() is not True:
+ logger.error("local sentence transformer image embedding node initial failed!")
+ else:
+ ComputeKernel.get_instance().add_compute_node(local_st_image_compute_node)
+
+
+ await ComputeKernel.get_instance().start()
+
+ AIBus().get_default_bus().register_unhandle_message_handler(self._handle_no_target_msg)
+ AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg)
+ KnowledgePipline.get_instance().initial()
+
+ TelegramTunnel.register_to_loader()
+ EmailTunnel.register_to_loader()
+ user_data_dir = str(AIStorage.get_instance().get_myai_dir())
+ tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml")
+ tunnel_config = None
+ try:
+ tunnel_config = toml.load(tunnels_config_path)
+ if tunnel_config is not None:
+ await AgentTunnel.load_all_tunnels_from_config(tunnel_config)
+ except Exception as e:
+ logger.warning(f"load tunnels config from {tunnels_config_path} failed!")
+
+
+ return True
+
+
+ def get_version(self) -> str:
+ return "0.5.1"
+
+ async def send_msg(self,msg:str,target_id:str,topic:str,sender:str = None) -> str:
+ agent_msg = AgentMsg()
+ agent_msg.set(sender,target_id,msg)
+ agent_msg.topic = topic
+ resp = await AIBus.get_default_bus().send_message(agent_msg)
+ if resp is not None:
+ if resp.msg_type != AgentMsgType.TYPE_SYSTEM:
+ return resp.body
+ else:
+ return f"Process Message Error: {resp.body} Please check logs/aios.log for more details!"
+ else:
+ return "System Error: Timeout, no resopnse! Please check logs/aios.log for more details!"
+
+ async def _user_process_msg(self,msg:AgentMsg) -> AgentMsg:
+ pass
+
+
+ async def get_tunnel_config_from_input(self,tunnel_target,tunnel_type):
+ tunnel_config = {}
+ tunnel_config["tunnel_id"] = f"{tunnel_type}_2_{tunnel_target}"
+ tunnel_config["target"] = tunnel_target
+ input_table = {}
+ tunnel_introduce : str = ""
+ match tunnel_type:
+ case "telegram":
+ tunnel_config["type"] = "TelegramTunnel"
+ input_table["token"] = UserConfigItem("telegram bot token")
+ input_table["allow"] = UserConfigItem("allow group (default is member,you can choose contact or guest)")
+ case "email":
+ tunnel_config["type"] = "EmailTunnel"
+ case _:
+ error_text = FormattedText([("class:error", f"tunnel type {tunnel_type}not support!")])
+ print_formatted_text(error_text,style=shell_style)
+ return None
+
+ intro_text = FormattedText([("class:prompt", tunnel_introduce)])
+ print_formatted_text(intro_text,style=shell_style)
+ for key,item in input_table.items():
+ user_input = await try_get_input(f"{key} : {item.desc}")
+ if user_input is None:
+ return None
+
+ tunnel_config[key] = user_input
+
+ return tunnel_config
+
+ async def append_tunnel_config(self,tunnel_config):
+ user_data_dir = AIStorage.get_instance().get_myai_dir()
+ tunnels_config_path = os.path.abspath(f"{user_data_dir}/etc/tunnels.cfg.toml")
+ all_tunnels = None
+ try:
+ all_tunnels = toml.load(tunnels_config_path)
+ except Exception as e:
+ logger.warning(f"load tunnels config for append from {tunnels_config_path} failed! {e}")
+
+ if all_tunnels is None:
+ all_tunnels = {}
+
+ all_tunnels[tunnel_config["tunnel_id"]] = tunnel_config
+ try:
+ f = open(tunnels_config_path,"w")
+ if f:
+ toml.dump(all_tunnels,f)
+ logger.info(f"append tunnel config to {tunnels_config_path} success!")
+ else:
+ logger.warning(f"append tunnel config to {tunnels_config_path} failed!")
+ except Exception as e:
+ logger.warning(f"append tunnels config from {tunnels_config_path} failed! {e}")
+
+ async def handle_contact_commands(self,args):
+ cm = ContactManager.get_instance()
+ if len(args) < 1:
+ return FormattedText([("class:error", f'/contact $contact_name, Like /contact "Jim Green"')])
+ contact_name = args[0]
+ contact = cm.find_contact_by_name(contact_name)
+ is_update = False
+ if contact is not None:
+ #show old info and ask user to update or remove
+ is_update = True
+ op_str = await try_get_input(f"Contact {contact_name} already exist, update or remove? (u/r)")
+ if op_str is None:
+ return None
+ if op_str == "r":
+ cm.remove_contact(contact_name)
+ return FormattedText([("class:title", f"remove {contact_name} success!")])
+ else:
+ print(f"old info: {contact}")
+ else:
+ contact = Contact(contact_name)
+
+ contact.is_family_member = False
+ is_family_member = await try_get_input(f"Is {contact_name} your family member? (y/n)")
+ if is_family_member is not None:
+ if is_family_member == "y" or is_family_member == "Y":
+ contact.is_family_member = True
+ else:
+ return None
+
+ contact_telegram = await try_get_input(f"Input {contact_name}'s telegram username:")
+ if contact_telegram is None:
+ return None
+ contact.telegram = contact_telegram
+
+ contact_email = await try_get_input(f"Input {contact_name}'s email:")
+ if contact_email is None:
+ return None
+ contact.email = contact_email
+
+ contact_phone = await try_get_input(f"Input {contact_name}'s phone (optional):")
+ if contact_phone is not None:
+ contact.phone = contact_phone
+
+ contact_note = await try_get_input(f"Input {contact_name}'s note (optional):")
+ if contact_note is not None:
+ contact.note = contact_note
+
+ contact.added_by = self.username
+ if is_update:
+ cm.set_contact(contact_name,contact)
+ else:
+ cm.add_contact(contact_name,contact)
+
+ async def handle_knowledge_commands(self, args):
+ show_text = FormattedText([("class:title", "sub command not support!\n"
+ "/knowledge add email | dir\n"
+ "/knowledge journal [$topn]\n"
+ "/knowledge query $object_id\n")])
+ if len(args) < 1:
+ return show_text
+ sub_cmd = args[0]
+ if sub_cmd == "add":
+ if len(args) < 2:
+ return show_text
+ if args[1] == "email":
+ config = dict()
+ for key, item in KnowledgeEmailSource.user_config_items():
+ user_input = await try_get_input(f"{key} : {item}")
+ if user_input is None:
+ return show_text
+ config[key] = user_input
+ error = KnowledgePipline.get_instance().add_email_source(KnowledgeEmailSource(config))
+ if error is not None:
+ return FormattedText([("class:title", f"/knowledge add email failed {error}\n")])
+ else:
+ KnowledgePipline.get_instance().save_cosnfig()
+ if args[1] == "dir":
+ config = dict()
+ for key, item in KnowledgeDirSource.user_config_items():
+ user_input = await try_get_input(f"{key} : {item}")
+ if user_input is None:
+ return show_text
+ config[key] = user_input
+ error = KnowledgePipline.get_instance().add_dir_source(KnowledgeDirSource(config))
+ if error is not None:
+ return FormattedText([("class:title", f"/knowledge add dir failed {error}\n")])
+ else:
+ KnowledgePipline.get_instance().save_config()
+ else:
+ return show_text
+ if sub_cmd == "journal":
+ topn = 10 if len(args) == 1 else int(args[1])
+ journals = [str(journal) for journal in KnowledgePipline.get_instance().get_latest_journals(topn)]
+ print_formatted_text("\r\n".join(journals))
+ if sub_cmd == "query":
+ if len(args) < 2:
+ return show_text
+ from knowledge import ObjectID, ObjectType
+ object_id = ObjectID.from_base58(args[1])
+ if object_id.get_object_type() == ObjectType.Image:
+ from PIL import Image
+ import io
+ image = KnowledgeBase().load_object(object_id)
+ image_data = KnowledgeBase().bytes_from_object(image)
+ image = Image.open(io.BytesIO(image_data))
+ image.show()
+
+ async def handle_node_commands(self, args):
+ show_text = FormattedText([("class:title", "sub command not support!\n"
+ "/node add llama $model_name $url\n"
+ "/node rm llama $model_name $url\n"
+ "/node list\n")])
+ if len(args) < 1:
+ return show_text
+ sub_cmd = args[0]
+ if sub_cmd == "add":
+ if len(args) < 2:
+ return show_text
+ if args[1] == "llama":
+ if len(args) < 4:
+ return show_text
+
+ model_name = args[2]
+ url = args[3]
+ ComputeNodeConfig.get_instance().add_node("llama", url, model_name)
+ ComputeNodeConfig.get_instance().save()
+ node = LocalLlama_ComputeNode(url, model_name)
+ node.start()
+ ComputeKernel.get_instance().add_compute_node(node)
+ else:
+ return show_text
+ elif sub_cmd == "rm":
+ if len(args) < 2:
+ return show_text
+ if args[1] == "llama":
+ if len(args) < 4:
+ return show_text
+
+ model_name = args[3]
+ url = args[4]
+ ComputeNodeConfig.get_instance().remove_node("llama", url, model_name)
+ ComputeNodeConfig.get_instance().save()
+ else:
+ return show_text
+ elif sub_cmd == "list":
+ print_formatted_text(ComputeNodeConfig.get_instance().list())
+
+ async def call_func(self,func_name, args):
+ match func_name:
+ case 'send':
+ show_text = FormattedText([("class:error", f'send args error,/send Tracy "Hello! It is a good day!" default')])
+ if len(args) == 3:
+ target_id = args[0]
+ msg_content = args[1]
+ topic = args[2]
+ resp = await self.send_msg(msg_content,target_id,topic,self.username)
+ show_text = FormattedText([("class:title", f"{self.current_topic}@{self.current_target} >>> "),
+ ("class:content", resp)])
+ return show_text
+ case 'set_config':
+ show_text = FormattedText([("class:error", f"set config args error,/set_config $config_item! ")])
+ if len(args) == 1:
+ key = args[0]
+ config_item = AIStorage.get_instance().get_user_config().get_config_item(key)
+ old_value = AIStorage.get_instance().get_user_config().get_value(key)
+
+ if config_item is not None:
+ value = await session.prompt_async(f"{key} : {config_item.desc} \nCurrent : {old_value}\nPlease input new value:",style=shell_style)
+ AIStorage.get_instance().get_user_config().set_value(key,value)
+ await AIStorage.get_instance().get_user_config().save_to_user_config()
+ show_text = FormattedText([("class:title", f"set {key} to {value} success!")])
+ else:
+ show_text = FormattedText([("class:error", f"set config failed! config item {key} not found!")])
+
+ return show_text
+ case 'connect':
+ show_text = FormattedText([("class:error", "args error, /connect $target")])
+ if len(args) < 1:
+ return show_text
+ tunnel_target = args[0]
+ if len(args) < 2:
+ tunnel_type = "telegram"
+ else:
+ tunnel_type = args[1]
+
+ tunnel_config = await self.get_tunnel_config_from_input(tunnel_target,tunnel_type)
+ if tunnel_config:
+ if await AgentTunnel.load_tunnel_from_config(tunnel_config):
+ # append
+ await self.append_tunnel_config(tunnel_config)
+ show_text = FormattedText([("class:title", f"connect to {tunnel_target} success!")])
+
+ return show_text
+ case 'knowledge':
+ return await self.handle_knowledge_commands(args)
+ case 'contact':
+ return await self.handle_contact_commands(args)
+ case 'think':
+ if len(args) >= 1:
+ target_id = args[0]
+ the_agent = await AgentManager.get_instance().get(target_id)
+ if the_agent is not None:
+ await the_agent._do_think()
+ case 'open':
+ if len(args) >= 1:
+ target_id = args[0]
+ else:
+ show_text = FormattedText([("class:error", "/open Need Target Agent/Workflow ID! like /open Jarvis default")])
+ return show_text
+
+ if len(args) >= 2:
+ topic = args[1]
+ else:
+ topic = "default"
+
+ self.current_target = target_id
+ self.current_topic = topic
+ show_text = FormattedText([("class:title", f"current session switch to {topic}@{target_id}")])
+ AIStorage.get_instance().get_user_config().set_value("shell.current",f"{self.current_topic}@{self.current_target}")
+ await AIStorage.get_instance().get_user_config().save_to_user_config()
+ return show_text
+ case 'enable':
+ if len(args) >= 1:
+ feature = args[0]
+ else:
+ show_text = FormattedText([("class:error", "/enable Need Feature Name! like /enable llama")])
+ return show_text
+
+ if await AIStorage.get_instance().is_feature_enable(feature):
+ show_text = FormattedText([("class:title", f"Feature {feature} already enabled!")])
+ return show_text
+
+ await AIStorage.get_instance().enable_feature(feature)
+ show_text = FormattedText([("class:title", f"Feature {feature} enabled!")])
+ return show_text
+ case 'disable':
+ if len(args) >= 1:
+ feature = args[0]
+ else:
+ show_text = FormattedText([("class:error", "/disable Need Feature Name! like /disable llama")])
+ return show_text
+
+ if not await AIStorage.get_instance().is_feature_enable(feature):
+ show_text = FormattedText([("class:title", f"Feature {feature} already disabled!")])
+ return show_text
+
+ await AIStorage.get_instance().disable_feature(feature)
+ show_text = FormattedText([("class:title", f"Feature {feature} disabled!")])
+ return show_text
+ #case 'login':
+ # if len(args) >= 1:
+ # self.username = args[0]
+ # AIBus().get_default_bus().register_message_handler(self.username,self._user_process_msg)
+
+ # return self.username + " login success!"
+ case 'history':
+ num = 10
+ offset = 0
+ if args is not None:
+ if len(args) >= 1:
+ num = args[0]
+ if len(args) >= 2:
+ offset = args[1]
+
+ db_path = ""
+ if await self.is_agent(self.current_target):
+ db_path = AgentManager.get_instance().db_path
+ else:
+ db_path = WorkflowManager.get_instance().db_file
+ chatsession:AIChatSession = AIChatSession.get_session(self.current_target,f"{self.username}#{self.current_topic}",db_path,False)
+ if chatsession is not None:
+ msgs = chatsession.read_history(num,offset)
+ format_texts = []
+ for msg in msgs:
+ format_texts.append(("class:content",f"{msg.sender} >>> {msg.body}"))
+ format_texts.append(("",f"\n-------------------\n"))
+ return FormattedText(format_texts)
+ return FormattedText([("class:title", f"chatsession not found")])
+ case 'node':
+ return await self.handle_node_commands(args)
+ case 'exit':
+ os._exit(0)
+ case 'help':
+ return FormattedText([("class:title", f"GO to https://github.com/fiatrete/OpenDAN-Personal-AI-OS/issues ^_^")])
+
+
+##########################################################################################################################
+history = FileHistory('aios_shell_history.txt')
+session = PromptSession(history=history)
+
+def parse_function_call(func_string):
+ if len(func_string) > 2:
+ if func_string[0] == '/' and func_string[1] != '/':
+ str_list = shlex.split(func_string[1:])
+ func_name = str_list[0]
+ params = str_list[1:]
+ return func_name, params
+ else:
+ return None
+
+async def try_get_input(desc:str,mutil_line:bool = False,check_func:callable = None) -> str:
+ user_input = await session.prompt_async(f"{desc} \nType /exit to abort. \nPlease input:",style=shell_style)
+ err_str = ""
+ if check_func is None:
+ if len(user_input) > 0:
+ if user_input != "/exit":
+ if mutil_line is False:
+ user_input = user_input.strip()
+ return user_input
+ else:
+ return None
+
+ else:
+ is_ok,err_str = check_func(user_input)
+ if is_ok:
+ return user_input
+
+ error_text = FormattedText([("class:error", err_str)])
+ print_formatted_text(error_text,style=shell_style)
+ return await try_get_input(desc,check_func)
+
+async def get_user_config_from_input(check_result:dict) -> bool:
+ for key,item in check_result.items():
+ user_input = await try_get_input(f"System config {key} ({item.desc}) not define!")
+ if user_input is None:
+ if item.is_optional:
+ continue
+ else:
+ True
+
+ if len(user_input) > 0:
+ AIStorage.get_instance().get_user_config().set_value(key,user_input)
+
+ await AIStorage.get_instance().get_user_config().save_to_user_config()
+ return True
+
+async def main_daemon_loop(shell:AIOS_Shell):
+ while shell.is_working:
+ await asyncio.sleep(1)
+
+ return 0
+
+def print_welcome_screen():
+ print("\033[1;31m")
+ logo = """
+\t _______ ____________________ __
+\t __ __ \______________________ __ \__ |__ | / /
+\t _ / / /__ __ \ _ \_ __ \_ / / /_ /| |_ |/ /
+\t / /_/ /__ /_/ / __/ / / / /_/ /_ ___ | /| /
+\t \____/ _ .___/\___//_/ /_//_____/ /_/ |_/_/ |_/
+\t /_/
+
+ """
+ print(logo)
+ print("\033[0m")
+
+ print("\033[1;32m \t\tWelcome to OpenDAN - Your Personal AI OS\033[0m\n")
+
+ introduce = """
+\tOpenDAN (Open and Do Anything Now with AI) is revolutionizing the
+\tAI landscape with its Personal AI Operating System. Designed for
+\tseamless integration of diverse AI modules, it ensures unmatched
+\tinteroperability. OpenDAN empowers users to craft powerful AI agents:
+\tfrom butlers and assistants to personal tutors and digital companions.
+\tAll while retaining control. These agents can team up to tackle complex
+\tchallenges, integrate with existing services, and command IoT devices.
+\t
+\tWith OpenDAN, we're putting AI in your hands, making life simpler and smarter.
+\t
+\t================ AIOS Shell Handbook ================
+
+\033[1;94m\tUnderstand the Shell Prompt :\033[0m [current_username]<->[current_topic]@[current_target]$
+\033[1;94m\tTalk with Agent/Workflow :\033[0m Directly input and wait.
+\033[1;94m\tTalk with another Agent/Workflow :\033[0m /open $target_name [$topic_name]
+\033[1;94m\tInstall new Agent/Workflow :\033[0m /install $agent_name (Not support at 0.5.1)
+\t\t(For Developer) Download and unzip Agent to ~/myai/agents or ~/myai/workflows
+\033[1;94m\tView chat History :\033[0m /history
+\033[1;94m\tChange AIOS Owner's telegram username :\033[0m /set_config telegram
+\033[1;94m\tChange OpenAI API Token :\033[0m /set_config $openai_api_key
+\033[1;94m\tGive your Agent a Telegram account :\033[0m /connect $agent_name
+\033[1;94m\tAdd personal files to the AI Knowledge Base. \033[0m
+\t\t1) Copy your file to ~/myai/data
+\t\t2) /knowlege add dir
+\033[1;94m\tSearch your knowledge base :\033[0m /open Mia
+\033[1;94m\tCheck the progress of AI reading personal data :\033[0m /knowledge journal
+\033[1;94m\tQuery object with ID in knowledge base :\033[0m /knowledge query $object_id
+\033[1;94m\tOpen AI Bash (For Developer Only):\033[0m /open ai_bash
+\033[1;94m\tEnable AIGC Feature :\033[0m /enable aigc
+\033[1;94m\tEnable llama (Local LLM Kernel) :\033[0m /enable llama
+"""
+ print(introduce)
+
+ print(f"\033[1;34m \t\tVersion: {AIOS_Version}\n\033")
+ print("\033[1;33m \tOpenDAN is an open-source project, let's define the future of Humans and AI together.\033[0m")
+ print("\033[1;33m \tGithub\t: https://github.com/fiatrete/OpenDAN-Personal-AI-OS\033[0m")
+ print("\033[1;33m \tWebsite\t: https://www.opendan.ai\033[0m")
+ print("\n\n")
+
+
+async def main():
+ print_welcome_screen()
+ print("Booting...")
+
+ if os.path.isdir(f"{directory}/../../../rootfs"):
+ AIStorage.get_instance().is_dev_mode = True
+ else:
+ AIStorage.get_instance().is_dev_mode = False
+
+
+ if AIStorage.get_instance().is_dev_mode:
+ logging.basicConfig(filename="aios_shell.log",filemode="w",encoding='utf-8',force=True,
+ level=logging.INFO,
+ format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s')
+ else:
+ dir_path = f"{AIStorage.get_instance().get_myai_dir()}/logs"
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+ log_file = f"{AIStorage.get_instance().get_myai_dir()}/logs/aios.log"
+ handler = RotatingFileHandler(log_file, maxBytes=50*1024*1024, backupCount=100)
+
+ logging.basicConfig(handlers=[handler],
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+
+ is_daemon = False
+ logger.info(f"Check Host OS :{os.name}")
+ if os.name != 'nt':
+ is_daemon = os.fstat(0) != os.fstat(1) or os.fstat(0) != os.fstat(2)
+
+ shell = AIOS_Shell("user")
+ shell.declare_all_user_config()
+ await AIStorage.get_instance().initial()
+ check_result = AIStorage.get_instance().get_user_config().check_config()
+ if check_result is not None:
+ if is_daemon:
+ logger.error(check_result)
+ return 1
+ else:
+ #Remind users to enter necessary configurations.
+ if await get_user_config_from_input(check_result) is False:
+ return 1
+ shell.username = AIStorage.get_instance().get_user_config().get_value("username")
+ init_result = await shell.initial()
+ proxy.apply_storage()
+
+ if init_result is False:
+ if is_daemon:
+ logger.error("aios shell initial failed!")
+ return 1
+ else:
+ print("aios shell initial failed!")
+ return 1
+
+ print(f"aios shell {shell.get_version()} ready. Daemon:{is_daemon}")
+ logger.info(f"aios shell {shell.get_version()} ready. Daemon:{is_daemon}")
+ if is_daemon:
+ return await main_daemon_loop(shell)
+
+ completer = WordCompleter(['/send $target $msg $topic',
+ '/open $target $topic',
+ '/history $num $offset',
+ '/connect $target',
+ '/contact $name',
+ '/knowledge add email | dir',
+ '/knowledge journal [$topn]',
+ '/knowledge query $object_id',
+ '/set_config $key',
+ '/enable $feature',
+ '/disable $feature',
+ '/node add llama $model_name $url',
+ '/node rm llama $model_name $url',
+ '/show',
+ '/exit',
+ '/help'], ignore_case=True)
+
+ current = AIStorage.get_instance().get_user_config().get_value("shell.current")
+ current = current.split("@")
+ shell.current_target = current[1]
+ shell.current_topic = current[0]
+
+ await asyncio.sleep(0.2)
+ while True:
+ user_input = await session.prompt_async(f"{shell.username}<->{shell.current_topic}@{shell.current_target}$ ",completer=completer,style=shell_style)
+ if len(user_input) <= 1:
+ continue
+
+ func_call = parse_function_call(user_input)
+ show_text = None
+ if func_call:
+ show_text = await shell.call_func(func_call[0], func_call[1])
+ else:
+ resp = await shell.send_msg(user_input,shell.current_target,shell.current_topic,shell.username)
+ show_text = FormattedText([
+ ("class:title", f"{shell.current_topic}@{shell.current_target} >>> "),
+ ("class:content", resp)
+ ])
+
+ print_formatted_text(show_text,style=shell_style)
+ #print_formatted_text(f"{shell.username}<->{shell.current_topic}@{shell.current_target} >>> {resp}",style=shell_style)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+
diff --git a/src/service/aios_shell/proxy.py b/src/service/aios_shell/proxy.py
new file mode 100644
index 0000000..f8b124c
--- /dev/null
+++ b/src/service/aios_shell/proxy.py
@@ -0,0 +1,44 @@
+
+import sys
+import os
+import logging
+import socket
+import socks
+import logging
+
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../../')
+
+from aios_kernel import AIStorage
+
+logger = logging.getLogger(__name__)
+
+def apply_storage():
+ proxy_cfg = AIStorage.get_instance().get_user_config().get_config_item("proxy")
+ if proxy_cfg is None:
+ return
+
+ host_url = proxy_cfg.value
+ if host_url is not None and len(host_url) > 3:
+ url_fields = host_url.split("@")
+ proxy_type, host, username, password = url_fields[0], None, None, None
+ if len(url_fields) > 1:
+ host = url_fields[1]
+ if len(url_fields) > 2:
+ username = url_fields[2]
+ if len(url_fields) > 3:
+ password = url_fields[3]
+
+ match proxy_type:
+ case "socks5":
+ (host, port) = host.split(":")
+ socks.set_default_proxy(socks.SOCKS5, host, int(port), username = username, password = password)
+ socket.socket = socks.socksocket
+ logger.info(f"proxy {host_url} will be used.")
+ case _:
+ logger.error(f"the proxy type ({proxy_type}) has not support. proxy will not be used.")
+
+
+def declare_user_config():
+ user_config = AIStorage.get_instance().get_user_config()
+ user_config.add_user_config("proxy", "set your proxy service as 'proxy_type@host:port@username@password', 'proxy_type' = 'socks5'", True, None)
diff --git a/src/service/app_manager/README b/src/service/app_manager/README
new file mode 100644
index 0000000..30404ce
--- /dev/null
+++ b/src/service/app_manager/README
@@ -0,0 +1 @@
+TODO
\ No newline at end of file
diff --git a/src/service/email_spider/converter.py b/src/service/email_spider/converter.py
new file mode 100644
index 0000000..193b220
--- /dev/null
+++ b/src/service/email_spider/converter.py
@@ -0,0 +1,24 @@
+from aios_kernel.knowledge import KnowledgeBase, EmailObject
+
+# define a email converter class
+
+class EmailConverter:
+ # define init method
+ def __init__(self, local_dir, knowledge_base: KnowledgeBase) -> None:
+ pass
+
+ async def run(self):
+ # convert the email to knowledge object
+ for email_dir in self._next():
+ # convert the email to knowledge object
+ knowledge_object = self._convert(email_dir)
+ # insert the knowledge object to knowledge base
+ await self.knowledge_base.insert(knowledge_object)
+
+ def _next(self) -> str:
+ pass
+
+ def _convert(self, email_dir) -> EmailObject:
+ pass
+
+
\ No newline at end of file
diff --git a/src/service/email_spider/main.py b/src/service/email_spider/main.py
new file mode 100644
index 0000000..cb75abd
--- /dev/null
+++ b/src/service/email_spider/main.py
@@ -0,0 +1,12 @@
+import asyncio
+from .spider import EmailSpider, EmailConverter
+
+
+if __name__ == "__main__":
+ spider = EmailSpider("smtp.163.com","user","pwd","./email")
+ asyncio.run(spider.run())
+
+ converter = EmailConverter("./email",KnowledgeBase())
+ asyncio.run(converter.run())
+
+
diff --git a/src/service/email_spider/spider.py b/src/service/email_spider/spider.py
new file mode 100644
index 0000000..0b5a212
--- /dev/null
+++ b/src/service/email_spider/spider.py
@@ -0,0 +1,17 @@
+# define a email spider class
+
+class EmailSpider:
+ def __init__(self, address, account, pwd, local_dir) -> None:
+ pass
+
+ async def run(self):
+ # spide the email from the email server
+ for email_link in self._next():
+ # save the email to local directory
+ self._save(email_link)
+
+ def _next(self):
+ pass
+
+ def _save(self, email_link) -> str:
+ pass
\ No newline at end of file
diff --git a/src/service/spider/email_spider.py b/src/service/spider/email_spider.py
new file mode 100644
index 0000000..cd44097
--- /dev/null
+++ b/src/service/spider/email_spider.py
@@ -0,0 +1,171 @@
+"""
+Capture your email locally, and parse out the pictures in the email body and the pictures, videos and other files in the attachment. Subsequently, it supports vectorized analysis of your personal data and serves as a knowledge base to enable large language model answers. Better results.
+
+An example of a local file is as follows:
+├── data
+│ └── alex0072@gmail.com
+│ └── 5de3e52f3a6b90cabe6cbdd4ae3a5c5b
+│ ├── email.txt
+│ ├── meta.json
+│ ├── image
+│ │ ├── 0648B869@99C03070.DB94B354.jpg
+│ └── body_image
+│ ├── 11044884873.jpg
+│ ├── 282985198265470.gif
+│ └── dd-login-service-min.png
+
+"""
+
+import imaplib
+import os
+import toml
+import logging
+import mailparser
+import hashlib
+import json
+import base64
+from bs4 import BeautifulSoup
+import requests
+
+class EmailSpider:
+ def __init__(self):
+ # logger config
+ self.logger = logging.getLogger('email spider')
+ self.logger.setLevel(logging.DEBUG)
+ ch = logging.StreamHandler()
+ formatter = logging.Formatter('%(asctime)s [%(name)s] [%(levelname)s] %(message)s')
+ ch.setFormatter(formatter)
+ self.logger.addHandler(ch)
+
+ # read config from toml file
+ # and read from config config.local.toml if exists (config.local.toml is ignored by git)
+ self.config = toml.load('./rootfs/email/config.toml')
+ if os.path.exists('./rootfs/email/config.local.toml'):
+ self.config = toml.load('./rootfs/email/config.local.toml')
+
+ self.client = self.email_client()
+
+ def email_client(self) -> imaplib.IMAP4_SSL:
+ self.logger.info(f"read email config from {self.config.get('EMAIL_IMAP_SERVER')}")
+ client = imaplib.IMAP4_SSL(
+ host=self.config.get('EMAIL_IMAP_SERVER'),
+ port=self.config.get('EMAIL_IMAP_PORT')
+ )
+ client.login(self.config.get('EMAIL_ADDRESS'), self.config.get('EMAIL_PASSWORD'))
+ return client
+
+ def list_box(self):
+ _, mailbox_list = self.client.list()
+ for mailbox in mailbox_list:
+ print(mailbox.decode())
+
+ def read_emails(self, folder: str = 'INBOX', imap_keyword: str = "UNSEEN"):
+ self.client.select(folder)
+ _, data = self.client.uid('search', None, imap_keyword)
+
+ # get email uid list
+ email_list = data[0].split()
+ self.logger.info(f"got {len(email_list)} emails")
+ email_list.reverse()
+ for uid in email_list:
+ if self.check_email_saved(uid):
+ self.logger.info(f"email uid {uid} already saved")
+ else:
+ self.read_and_save_email(uid)
+ self.logger.info(f"email uid {uid} saved")
+
+ def read_and_save_email(self, uid: str):
+ message_parts = "(BODY.PEEK[])"
+ _, email_data = self.client.uid('fetch', uid, message_parts)
+ mail = mailparser.parse_from_bytes(email_data[0][1])
+ self.logger.info(f"got email subject [{mail.subject}]")
+ self.save_email(mail)
+
+ def get_local_dir_name(self, mail: mailparser.MailParser) -> str:
+ dir = f"{self.config.get('LOCAL_DIR')}/{self.config.get('EMAIL_ADDRESS')}"
+ name = f"{mail.subject}__{mail.date}"
+ name = hashlib.md5(name.encode('utf-8')).hexdigest()
+ return f"{dir}/{name}"
+
+ def check_email_saved(self, uid: str):
+ message_parts = "(BODY[HEADER])"
+ _, email_data = self.client.uid('fetch', uid, message_parts)
+ mail = mailparser.parse_from_bytes(email_data[0][1])
+ self.logger.info(f"[{uid}]check email subject [{mail.subject}]")
+ dir = self.get_local_dir_name(mail)
+ self.logger.info(f"check email saved {dir}")
+ file = f"{dir}/email.txt"
+ if os.path.exists(file):
+ return False
+ return False
+
+ # save email attachment(images)
+ def save_email_attachment(self, mail: mailparser.MailParser, email_dir: str):
+ for attachment in mail.attachments:
+ if attachment['mail_content_type'] in ['image/png', 'image/jpeg', 'image/gif']:
+ print('current mail have image attachment')
+ img_dir = f"{email_dir}/image"
+ if not os.path.exists(img_dir):
+ os.makedirs(img_dir)
+ filename = attachment['filename']
+ filefullname = f"{img_dir}/{filename}"
+ image_data = attachment['payload']
+ try:
+ image_data = base64.b64decode(image_data)
+ except base64.binascii.Error:
+ image_data = image_data.encode()
+ with open(filefullname, 'wb') as f:
+ f.write(image_data)
+ self.logger.info(f"save email image {filename} success")
+
+ # save email body images(html content)
+ def save_body_images(self, html_content: str, email_dir: str):
+ # get all image urls
+ soup = BeautifulSoup(html_content, 'html.parser')
+ img_tags = soup.find_all('img')
+ img_urls = [img['src'] for img in img_tags if 'src' in img.attrs]
+ self.logger.info(f'Found {len(img_urls)} images in email body')
+
+ if not os.path.exists(email_dir):
+ os.makedirs(email_dir)
+
+ for img_url in img_urls:
+ # keep the original image filename(last of url)
+ img_filename = os.path.join(email_dir, img_url.split('/')[-1])
+ # download image
+ response = requests.get(img_url, stream=True)
+ if response.status_code == 200:
+ with open(img_filename, 'wb') as img_file:
+ for chunk in response.iter_content(1024):
+ img_file.write(chunk)
+ self.logger.info(f'Downloaded {img_url} to {img_filename}')
+ else:
+ self.logger.info(f'Failed to download {img_url}')
+
+ # save email content to local dir
+ def save_email(self, mail: mailparser.MailParser):
+ dir = f"{self.config.get('LOCAL_DIR')}/{self.config.get('EMAIL_ADDRESS')}"
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+ email_dir = self.get_local_dir_name(mail)
+ self.logger.info(f"save email to {email_dir}")
+ if not os.path.exists(email_dir):
+ os.makedirs(email_dir)
+ with open(f"{email_dir}/email.txt", "w") as f:
+ f.write(mail.body)
+ with open(f"{email_dir}/meta.json", "w", encoding='utf-8') as f:
+ mail_dict = json.loads(mail.mail_json)
+ if 'body' in mail_dict:
+ del mail_dict['body']
+ json.dump(mail_dict, f, ensure_ascii=False, indent=4)
+ self.logger.info(f"save email meta info {f.name}")
+
+ self.save_email_attachment(mail, email_dir)
+ self.save_body_images(mail.body, f"{email_dir}/body_image")
+
+
+if __name__ == "__main__":
+ spider = EmailSpider()
+ folder = 'INBOX'
+ imap_keyword = "ALL"
+ spider.read_emails(folder, imap_keyword)
\ No newline at end of file
diff --git a/test/agent_test.py b/test/agent_test.py
new file mode 100644
index 0000000..78941d4
--- /dev/null
+++ b/test/agent_test.py
@@ -0,0 +1,36 @@
+import sys
+sys.path.append('../src/component/')
+
+from agent_manager import agent_manager
+
+def clean_agent():
+ print("clean_agent")
+
+def clean_agent_templete():
+ print("clean_agent_templte")
+
+def test_agent():
+ am = agent_manager()
+ am.initial("root_dir")
+ agent = am.get("english_teacher")
+ if agent is None:
+ agent_templete = am.get_templete("english_teacher")
+
+ if agent_templete is None :
+ op = am.install("english_teacher")
+ #wait install done
+
+ agent = am.create(agent_templete,"Tracy","Wang","Tracy Wang is my english teacher")
+
+ print("Agent Tracy Wang load success!");
+ print(agent.get_introduce());
+
+
+
+ #chat_session = agent.get_default_chat_session("master");
+ #chat_session.chat("给我讲一个英文笑话!");
+ #chat_session.wait_response();
+ #print(chat_session.last_msg());
+
+if __name__ == "__main__":
+ test_agent()
\ No newline at end of file
diff --git a/test/async_test.py b/test/async_test.py
new file mode 100644
index 0000000..64cda58
--- /dev/null
+++ b/test/async_test.py
@@ -0,0 +1,66 @@
+import asyncio
+import aiohttp
+import aiofiles
+STATE_DONE = 0
+STATE_DOWNLOADING = 1
+class install_task:
+ def __init__(self) -> None:
+ self.download_task = None
+ self.state = STATE_DOWNLOADING
+ self.recv_bytes = 0
+ self.total_bytes = 0
+
+class install_task_mgr:
+ def __init__(self) -> None:
+ self.all_tasks = {}
+
+ def create_install_task(self,url:str,target:str):
+ owner = self
+ this_task = self.all_tasks.get(url)
+ if this_task is not None:
+ return this_task
+
+ this_task = install_task()
+ self.all_tasks[url] = this_task
+ async def down_and_write():
+ async with aiofiles.open(target, 'wb') as file:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url) as response:
+ while True:
+ chunk = await response.content.read(1024*1024*16)
+ this_task.recv_bytes += len(chunk)
+ if not chunk:
+ break
+ await file.write(chunk)
+ print(f"download task {url} done!")
+ this_task.state = STATE_DONE
+ del owner.all_tasks[url]
+
+ this_task.download_task = asyncio.create_task(down_and_write())
+ return this_task
+
+
+
+async def test_wait_download(mgr):
+ this_task = mgr.create_install_task("https://www.cyfs.com/download/beta/cyberchat/android/latest","test.pkg")
+ await this_task.download_task
+
+def test_timer_download(mgr):
+ this_task = mgr.create_install_task("https://www.cyfs.com/download/beta/cyberchat/android/latest","test.pkg")
+ # start timer
+ async def check_timer():
+ while this_task.state == STATE_DOWNLOADING:
+ await r = asyncio.sleep(1)
+ print(f"download bytes:{this_task.recv_bytes}")
+ print("download complete!")
+
+ asyncio.create_task(check_timer())
+
+async def test_main():
+ mgr = install_task_mgr()
+ test_timer_download(mgr)
+ await test_wait_download(mgr)
+ await asyncio.sleep(1)
+
+if __name__ == "__main__":
+ asyncio.run(test_main())
\ No newline at end of file
diff --git a/test/chatsession_test.py b/test/chatsession_test.py
new file mode 100644
index 0000000..c528556
--- /dev/null
+++ b/test/chatsession_test.py
@@ -0,0 +1,75 @@
+import unittest
+import sys
+import os
+import sqlite3
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../src')
+
+from aios_kernel import ChatSessionDB
+
+
+class TestChatDatabase(unittest.TestCase):
+
+ def setUp(self):
+ """Function to setup the test case"""
+ self.db_file = 'test_chat.db'
+ self.chat_db = ChatSessionDB(self.db_file)
+
+ def tearDown(self):
+ """Function to cleanup after the test case"""
+ self.chat_db.close()
+ os.remove(self.db_file)
+
+ def test_database_creation(self):
+ """Test if the database is created"""
+ self.assertTrue(os.path.exists(self.db_file))
+
+ def test_table_creation(self):
+ """Test if the tables are created in the database"""
+ conn = sqlite3.connect(self.db_file)
+ cursor = conn.cursor()
+
+ # Check if ChatSessions table exists
+ cursor.execute("""
+ SELECT count(name) FROM sqlite_master WHERE type='table' AND name='ChatSessions'
+ """)
+ self.assertEqual(cursor.fetchone()[0], 1)
+
+ # Check if Messages table exists
+ cursor.execute("""
+ SELECT count(name) FROM sqlite_master WHERE type='table' AND name='Messages'
+ """)
+ self.assertEqual(cursor.fetchone()[0], 1)
+
+ conn.close()
+
+ def test_insert_and_get_session(self):
+ """Test if we can insert and retrieve a session"""
+ session_id = "session1"
+ session_owner = "user1"
+ session_topic = "topic1"
+ start_time = "2023-08-28 12:00:00"
+
+ self.chat_db.insert_chatsession(session_id,session_owner, session_topic, start_time)
+ session = self.chat_db.get_chatsession_by_id(session_id)
+
+ self.assertEqual(session, (session_id,session_owner,session_topic, start_time))
+
+ def test_insert_and_get_message(self):
+ """Test if we can insert and retrieve a message"""
+ message_id = "message1"
+ session_id = "session1"
+ sender_id = "user1"
+ receiver_id = "user2"
+ timestamp = "2023-08-28 12:30:00"
+ content = "Hello, world!"
+ status = 0
+
+ self.chat_db.insert_message(message_id, session_id, sender_id, receiver_id, timestamp, content, status)
+ message = self.chat_db.get_message_by_id(message_id)
+
+ self.assertEqual(message, (message_id, session_id, sender_id, receiver_id, timestamp, content, status))
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/test/compute_task_test.py b/test/compute_task_test.py
new file mode 100644
index 0000000..a980b21
--- /dev/null
+++ b/test/compute_task_test.py
@@ -0,0 +1,12 @@
+import asyncio
+
+async def test_llm_completion_task():
+ # compute task have engouh meta info to make sure compute_kernel can run it in most suitable compute container
+ test_task = llm_completion_task()
+ # add tset_task to compute_kernel's execute queue
+ compute_kernel.run(test_task)
+ # wait for test_task
+
+
+if __name__ == "__main__":
+ asyncio.run(test_llm_completion_task())
\ No newline at end of file
diff --git a/test/daemon_test.py b/test/daemon_test.py
new file mode 100644
index 0000000..b4eff77
--- /dev/null
+++ b/test/daemon_test.py
@@ -0,0 +1,17 @@
+import daemon
+from time import sleep
+import logging
+
+logger = logging.getLogger(__name__)
+
+logging.basicConfig(filename="daemon_test.log",filemode="w",encoding='utf-8',force=True,
+ level=logging.INFO,
+ format='[%(asctime)s]%(name)s[%(levelname)s]: %(message)s')
+
+def main_program():
+ while True:
+ logger.info("hello world")
+ sleep(1)
+
+with daemon.DaemonContext():
+ main_program()
diff --git a/test/env_test.py b/test/env_test.py
new file mode 100644
index 0000000..694acbb
--- /dev/null
+++ b/test/env_test.py
@@ -0,0 +1,32 @@
+import asyncio
+import os
+import sys
+
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../src')
+
+from aios_kernel import CalenderEnvironment,WorkflowEnvironment
+
+
+async def test_buildin_envs():
+ c_env = CalenderEnvironment("calender")
+ c_env.start()
+ print(c_env.get_value("now"))
+ async def show_event(eventid,event):
+ print(event.data)
+ c_env.attach_event_handler("timer",show_event)
+
+ w_env = WorkflowEnvironment("workflow",os.path.abspath(directory + "/../rootfs/workflow_env.db"))
+ w_env.set_value("test","test_aaaa")
+ print(w_env.get_value("test"))
+
+ await asyncio.sleep(10)
+
+
+if __name__ == "__main__":
+ #test_rstr = "abc is {abc}"
+ #values = {"abc":"123"}
+ #new_str = test_rstr.format_map(values)
+ #print(new_str)
+
+ asyncio.run(test_buildin_envs())
\ No newline at end of file
diff --git a/test/pkg.cfg.toml b/test/pkg.cfg.toml
new file mode 100644
index 0000000..68c46d5
--- /dev/null
+++ b/test/pkg.cfg.toml
@@ -0,0 +1,83 @@
+# workflow实例场景
+# 能展示sub workflow
+# 能展示env的整合
+# 能展示filter的使用
+# 能展示function的调用
+# 能展示基于工作目标/KPI的sub workflow迭代流程
+# 多人场景安排
+
+# 例子:举办一个团队活动
+# 方案讨论(通过交互引导的方式收集主人的需求)与确定
+
+# 活动前:
+# 通讯员,对接管理参加活动的人的情况 (email spider)
+# 酒店预订 简单:搜索(酒店评价) 处理异常
+# 行程(票务)预订 :搜索,处理异常
+# 餐饮预订:给出方案,确定细节,预订
+
+# 活动中:
+# 进行统计和分析,调整设备
+# 安保,空调,音乐,拍照,录像
+# 响应紧急情况
+
+# 活动结束后:
+# 整理照片,视频,进行必要的二次创作,发送给相关人员
+# 对活动进行总结,提出改件意见(指导下一次活动)
+
+# 1. 人员
+# 主管,负责和客户沟通,并对每个环境的结果进行总结
+# 嘉宾对接
+# 酒店组
+# 行程组
+# 财务组
+# 多媒体组
+
+
+[filter]
+"*" = "manager"
+
+[roles.manager]
+fullname = "经理"
+agent="manager"
+[[roles.manager.prompt]]
+role="system"
+content="""你是一个活动策划公司的经理,与客户对接并向团队下达指令。你的团队分为下面几个小组:嘉宾对接组,酒店预定组,行程预订组,财务组,活动摄像组。活动策划分为四个阶段:方案讨论,活动前,活动中,活动后。你会根据客户的需求,对团队进行分工,分别完成各个阶段的工作。你的基本工作模式是:\
+1. 收到客户的明确的指令后,基于客户的已有信息和客户商量活动方案,和活动策划公司无关的业务你会回答‘与我无关’。当和客户完成活动方案的确认后,你会将拆解后的任务分配给各个小组 \
+2. 根据目前已经确认的活动方案,你要根据时间适时的检查不同小组的工作情况。当收到小组的工作情况反馈后,你会站在全局的角度判断是否需要调整活动方案,如果需要调整,你会和客户商量重新确定方案,然后再将调整后的方案分配给各个小组。\
+3. 有时工作小组会主动与你沟通,反馈一些问题。你会站在全局的角度给与指导,适当的调整工作小组的工作目标。如果反馈的问题需要你和客户沟通,你会和客户沟通后重新确定方案。再将调整后的方案分配给受到影响各个小组。\
+4. 当你决定要和工作小组通信时,请使用`send_message({小组名称},{内容}`)的形式。"""}]
+
+
+
+
+[sub_workflows]
+[sub_workflows."嘉宾对接组"]
+# 展现读取email和发送email与嘉宾沟通的能力
+[sub_workflows."嘉宾对接组".environments.email]
+new_mail = "收到来自{event.data.from},标题为{event.data.subject}的邮件,内容为{event.data.content}的电子邮件" # 这里将new_mail事件转换为了一个来自环境的message
+
+[sub_workflows."嘉宾对接组".roles.leader]
+name = "嘉宾对接组组长"
+[[sub_workflows."嘉宾对接组".roles.leader.prompt]]
+role="system"
+content="""你是一家活动策划公司的嘉宾对接组的组长,你的工作是基于已知信息,当前活动信息、公司经理的指令与嘉宾沟通,收集嘉宾的信息,然后将信息反馈给经理。在你看来,参加活动的多少有成员都是嘉宾,你可以通过你知道的信息给不同的成员进行分级。你的基本工作模式是:\
+1. 处理收到的邮件,如果邮件来自嘉宾,你会尝试从邮件的表态和内容中分享嘉宾的需要,并结合你对当前活动方案的理解判断是否需要和经理沟通,如果需要和经理沟通,你会将嘉宾的需求总结和告诉经理。不需要沟通的事项可以直接回复嘉宾。\
+2. 你总是通过`call_function(get_env,'parent.topic'`的形式查询当前的活动方案。等待函数返回后,你会根据函数的返回结果继续处理上一个对话。\
+3. 当你决定要和经理通信时,请使用`send_message(manager,{内容}`)的形式,内容的长度不超过200字。\
+4. 当你决定要回复嘉宾时,请使用`call_function(sendmail,{嘉宾邮件地址},{标题},{内容})的形式,内容的长度不超过500字。"""
+# 这里是孤立工作模式,组长只和经理沟通,也可以赋予其和其它组沟通的能力
+
+[sub_workflows."酒店预定组"]
+# 展现使用搜索引擎,并调用预订酒店的能力
+[sub_workflows."酒店预定组".environments.email]
+
+[sub_workflows."酒店预定组".roles.leader]
+name="酒店预定组组长"
+prompt = [{role="xxx",content="yyy"}]
+
+[sub_workflows."酒店预定组".roles.research]
+name="酒店搜索专家"
+
+[sub_workflows."行程预订组"]
+# 展现处理冲突并反推
+nam3="3"
diff --git a/test/spider_test.py b/test/spider_test.py
new file mode 100644
index 0000000..296f382
--- /dev/null
+++ b/test/spider_test.py
@@ -0,0 +1,23 @@
+
+
+def test_main():
+ sm = service_manager()
+
+ knownlege_base_service = sm.get('knowlege_base')
+ knownlege_base_service.start()
+
+ email_spider = sm.get('email_spider')
+ email_spider.start()
+
+ doc_embeding_service = sm.get('doc_embeding_service')
+ doc_embeding_service.start()
+
+ ia = agents_manager().get("ai_info_assistor")
+ chat_session = ia.get_default_chat_session("master");
+ chat_session.chat("Who responded to my issue last week");
+ chat_session.wait_response();
+ #print(chat_session.last_msg());
+
+if __name__ == '__main__':
+ test_main()
+
\ No newline at end of file
diff --git a/test/test_chunk.py b/test/test_chunk.py
new file mode 100644
index 0000000..0771f03
--- /dev/null
+++ b/test/test_chunk.py
@@ -0,0 +1,66 @@
+import sys
+import os
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+print(dir_path)
+
+sys.path.append("{}/../src/".format(dir_path))
+print(sys.path)
+
+
+import logging
+import sys
+
+root = logging.getLogger()
+root.setLevel(logging.DEBUG)
+handler = logging.StreamHandler(sys.stdout)
+handler.setLevel(logging.DEBUG)
+formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+handler.setFormatter(formatter)
+root.addHandler(handler)
+
+
+from knowledge import (
+ ChunkTracker,
+ ChunkID,
+ HashValue,
+ PositionType,
+ KnowledgeStore,
+ ChunkListWriter,
+)
+import asyncio
+import unittest
+
+
+class TestChunk(unittest.TestCase):
+ def test_chunk_tracker(self):
+ tracker = KnowledgeStore().get_chunk_tracker()
+
+ hash = HashValue.hash_data("1234567890".encode("utf-8"))
+ cid = ChunkID.new_chunk_id(hash)
+ print(cid)
+
+ tracker.add_position(cid, "/tmp/1", PositionType.File)
+ ret = tracker.get_position(cid)
+ print(ret[0])
+
+ tracker.remove_position(cid)
+ ret = tracker.get_position(cid)
+ self.assertEqual(ret, None)
+
+ def test_chunk(self):
+ gen = ChunkListWriter(
+ KnowledgeStore().get_chunk_store(), KnowledgeStore().get_chunk_tracker()
+ )
+ gen.create_chunk_list_from_file("H:/test", 1024 * 1024, True)
+
+ # Read the file
+ text_file = "H:/test.txt"
+ with open(text_file, "r", encoding="utf-8") as file:
+ text = file.read()
+
+ gen.create_chunk_list_from_text(text)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_contac_manager.py b/test/test_contac_manager.py
new file mode 100644
index 0000000..9dbd39d
--- /dev/null
+++ b/test/test_contac_manager.py
@@ -0,0 +1,64 @@
+import unittest
+import toml
+import os
+import sys
+
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../src')
+from aios_kernel import ContactManager, Contact, FamilyMember
+
+class TestContactManager(unittest.TestCase):
+
+ def setUp(self):
+ self.manager = ContactManager(filename="test_contacts.toml")
+ self.manager.load_data()
+
+ def tearDown(self):
+ if os.path.exists("test_contacts.toml"):
+ os.remove("test_contacts.toml")
+
+ def test_add_family_member(self):
+ new_member = FamilyMember("Alice", "123-456-7890", "sdfsd","alice@example.com")
+ self.manager.add_family_member("Alice", new_member)
+ members = self.manager.list_family_members()
+ self.assertEqual(len(members), 1)
+ self.assertEqual(members[0].name, "Alice")
+
+ def test_add_contact(self):
+ new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.")
+ self.manager.add_contact("Bob", new_contact)
+ contacts = self.manager.list_contacts()
+ self.assertEqual(len(contacts), 1)
+ self.assertEqual(contacts[0].name, "Bob")
+ self.assertEqual(contacts[0].added_by, "Alice")
+
+ def test_remove_contact(self):
+ new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.")
+ self.manager.add_contact("Bob", new_contact)
+ self.manager.remove_contact("Bob")
+ contacts = self.manager.list_contacts()
+ self.assertEqual(len(contacts), 0)
+
+ def test_find_contact_by_name(self):
+ new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.")
+ self.manager.add_contact("Bob", new_contact)
+ contact = self.manager.find_contact_by_name("Bob")
+ self.assertIsNotNone(contact)
+ self.assertEqual(contact.name, "Bob")
+
+ def test_find_contact_by_email(self):
+ new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.")
+ self.manager.add_contact("Bob", new_contact)
+ contact = self.manager.find_contact_by_email("bob@example.com")
+ self.assertIsNotNone(contact)
+ self.assertEqual(contact.email, "bob@example.com")
+
+ def test_find_contact_by_phone(self):
+ new_contact = Contact("Bob", "987-654-3210", "bob@example.com", "32323","Alice", ["Friend"], "Bob is Alice's friend.")
+ self.manager.add_contact("Bob", new_contact)
+ contact = self.manager.find_contact_by_phone("987-654-3210")
+ self.assertIsNotNone(contact)
+ self.assertEqual(contact.phone, "987-654-3210")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/test/test_embedding.py b/test/test_embedding.py
new file mode 100644
index 0000000..e6fe4d7
--- /dev/null
+++ b/test/test_embedding.py
@@ -0,0 +1,98 @@
+import sys
+import os
+import logging
+from sentence_transformers import SentenceTransformer, util
+
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+print(dir_path)
+
+sys.path.append("{}/../src/".format(dir_path))
+print(sys.path)
+
+root = logging.getLogger()
+root.setLevel(logging.DEBUG)
+handler = logging.StreamHandler(sys.stdout)
+handler.setLevel(logging.DEBUG)
+formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+handler.setFormatter(formatter)
+root.addHandler(handler)
+
+
+def test_st():
+ image_model = SentenceTransformer('clip-ViT-B-32-multilingual-v1')
+ model = SentenceTransformer("all-MiniLM-L6-v2")
+
+ # Our sentences we like to encode
+ sentences = [
+ "This framework generates embeddings for each input sentence",
+ "Sentences are passed as a list of string.",
+ "The quick brown fox jumps over the lazy dog.",
+ ]
+
+ # Sentences are encoded by calling model.encode()
+ sentence_embeddings = model.encode(sentences)
+
+ # Print the embeddings
+ for sentence, embedding in zip(sentences, sentence_embeddings):
+ print("Sentence:", sentence)
+ print("Embedding:", embedding)
+ print("")
+
+ # Single list of sentences
+
+ """
+ sentences = [
+ "The cat sits outside",
+ "A man is playing guitar",
+ "I love pasta",
+ "The new movie is awesome",
+ "The cat plays in the garden",
+ "A woman watches TV",
+ "The new movie is so great",
+ "Do you like pizza?",
+ ]
+ """
+ sentences = [
+ "猫坐在外面",
+ "狗坐在上面",
+ "狗坐在里面",
+ "一个男人在弹吉他",
+ "我爱意大利面",
+ "新电影太精彩了",
+ "猫在花园里玩耍",
+ "一个女人在看电视",
+ "新电影太棒了",
+ "你喜欢披萨吗?",
+ ]
+
+ # Compute embeddings
+ #embeddings = model.encode(sentences, convert_to_tensor=True)
+ embeddings = model.encode(sentences)
+ print("embeddings as follows: ")
+ print(embeddings)
+
+
+ # Compute cosine-similarities for each sentence with each other sentence
+ cosine_scores = util.cos_sim(embeddings, embeddings)
+
+ # Find the pairs with the highest cosine similarity scores
+ pairs = []
+ for i in range(len(cosine_scores) - 1):
+ for j in range(i + 1, len(cosine_scores)):
+ pairs.append({"index": [i, j], "score": cosine_scores[i][j]})
+
+ # Sort scores in decreasing order
+ pairs = sorted(pairs, key=lambda x: x["score"], reverse=True)
+
+ for pair in pairs[0:10]:
+ i, j = pair["index"]
+ print(
+ "{} \t\t {} \t\t Score: {:.4f}".format(
+ sentences[i], sentences[j], pair["score"]
+ )
+ )
+
+
+if __name__ == "__main__":
+ test_st()
diff --git a/test/test_knowledge_base.py b/test/test_knowledge_base.py
new file mode 100644
index 0000000..5c9d516
--- /dev/null
+++ b/test/test_knowledge_base.py
@@ -0,0 +1,50 @@
+import sys
+import os
+import logging
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+print(dir_path)
+
+sys.path.append("{}/../src/".format(dir_path))
+print(sys.path)
+
+root = logging.getLogger()
+root.setLevel(logging.DEBUG)
+handler = logging.StreamHandler(sys.stdout)
+handler.setLevel(logging.DEBUG)
+formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+handler.setFormatter(formatter)
+root.addHandler(handler)
+
+
+from knowledge import ObjectID, HashValue, EmailObjectBuilder
+from aios_kernel import KnowledgeBase, AgentPrompt, OpenAI_ComputeNode, ComputeKernel
+import asyncio
+import unittest
+
+async def test_embedding_email(test):
+ open_ai_node = OpenAI_ComputeNode()
+ open_ai_node.start()
+ ComputeKernel().add_compute_node(open_ai_node)
+
+ email_folder = os.path.join(dir_path, "../rootfs/data/email/")
+ print("explore emails in folder ", email_folder)
+ for root, dirs, files in os.walk(email_folder):
+ for dir in dirs:
+ email_object = EmailObjectBuilder({}, os.path.join(root, dir)).build()
+ await KnowledgeBase().insert_object(email_object)
+
+ msg_prompt = AgentPrompt()
+ msg_prompt.messages = [{"role":"user","content":"abcdef"}]
+
+ await KnowledgeBase().query_prompt(msg_prompt)
+
+
+
+class TestKnowledgeBase(unittest.TestCase):
+ def test_embedding(self):
+ asyncio.run(test_embedding_email(self))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_local_sd_node.py b/test/test_local_sd_node.py
new file mode 100644
index 0000000..21f8219
--- /dev/null
+++ b/test/test_local_sd_node.py
@@ -0,0 +1,59 @@
+import os
+import time
+import uuid
+import io
+import asyncio
+import sys
+import logging
+import pytest
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../src')
+from aios_kernel.local_stability_node import Local_Stability_ComputeNode
+from aios_kernel.compute_task import ComputeTaskType, ComputeTask, ComputeTaskState
+
+#to launch a local stability node, please check:
+#https://github.com/glen0125/stable-diffusion-webui-docker
+
+os.environ["LOCAL_STABILITY_URL"] = ""
+os.environ["TEXT2IMG_DEFAULT_MODEL"] = "v1-5-pruned-emaonly"
+os.environ["TEXT2IMG_OUTPUT_DIR"] = "./"
+
+@pytest.mark.asyncio
+async def test_local_sd_node(propmt, model):
+ node = Local_Stability_ComputeNode.get_instance()
+ if await node.initial() is not True:
+ print("node initial failed!")
+ return
+
+ task = ComputeTask()
+ task.task_type = ComputeTaskType.TEXT_2_IMAGE
+ task.create_time = time.time()
+ task.task_id = uuid.uuid4().hex
+ task.params['model_name'] = model
+ task.params['prompt'] = propmt
+ await node.push_task(task)
+
+ while True:
+ if task.state == ComputeTaskState.DONE:
+ local_file = task.result.result
+ print("local file is: ", local_file)
+ break
+ await asyncio.sleep(1)
+
+ # result = node._run_task(task)
+ # print("result is: ", result)
+
+ # if result.result is not None:
+ # local_file = result.result
+ # print("local file is: ", local_file)
+
+if __name__ == "__main__":
+ arg_len = len(os.sys.argv)
+ prompt = "a beautiful sunset"
+ model = "v1-5-pruned-emaonly"
+ if arg_len >= 2:
+ prompt = os.sys.argv[1]
+ if arg_len == 3:
+ model = os.sys.argv[2]
+
+ asyncio.run(test_local_sd_node(prompt, model))
diff --git a/test/test_object.py b/test/test_object.py
new file mode 100644
index 0000000..e464aa0
--- /dev/null
+++ b/test/test_object.py
@@ -0,0 +1,124 @@
+import sys
+import os
+import logging
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+print(dir_path)
+
+sys.path.append("{}/../src/".format(dir_path))
+print(sys.path)
+
+root = logging.getLogger()
+root.setLevel(logging.DEBUG)
+handler = logging.StreamHandler(sys.stdout)
+handler.setLevel(logging.DEBUG)
+formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+handler.setFormatter(formatter)
+root.addHandler(handler)
+
+
+from knowledge import (
+ ObjectID,
+ HashValue,
+ EmailObjectBuilder,
+ ObjectRelationStore,
+ KnowledgeStore,
+ EmailObject,
+ ImageObject,
+)
+from aios_kernel import LocalSentenceTransformer_Image_ComputeNode, ComputeTask
+import asyncio
+import unittest
+
+
+class TestVectorSTorage(unittest.IsolatedAsyncioTestCase):
+ async def test_object(self):
+ data = HashValue.hash_data("1233".encode("utf-8"))
+ print(data.to_base58())
+ print(data.to_base36())
+
+ data2 = HashValue.from_base58(data.to_base58())
+ self.assertEqual(data.to_base36(), data2.to_base36())
+
+ data2 = HashValue.from_base36(data.to_base36())
+ self.assertEqual(data.to_base58(), data2.to_base58())
+
+ email_folder = "F:\\system\\Downloads\\8081ffdb80925f5bff9c6ab9c4756c7d"
+ email_object = EmailObjectBuilder({}, email_folder).build()
+
+ id = email_object.calculate_id()
+ print(f"got email object: {id.to_base58()}")
+
+ # test encode & decode
+ ret = email_object.encode()
+ obj = EmailObject.decode(ret)
+ id2 = obj.calculate_id()
+ print(f"got email object: {id2.to_base58()}")
+ self.assertEqual(id.to_base58(), id2.to_base58())
+
+ ret2 = obj.encode()
+ self.assertEqual(ret, ret2)
+
+ images = email_object.get_rich_text().get_images()
+ image_keys = list(images.keys())
+ print("got image list: ", image_keys)
+
+ image_id = images[image_keys[1]]
+ print(f"got image object: {image_keys[1]} {image_id.to_base58()}")
+
+ node = LocalSentenceTransformer_Image_ComputeNode();
+ ret = node.initial()
+ self.assertEqual(ret, True)
+
+ task = ComputeTask()
+ task.set_image_embedding_params(image_id)
+ ret = await node.execute_task(task)
+ print(ret)
+ '''
+ buf = KnowledgeStore().get_object_store().get_object(image_id)
+ image_obj= ImageObject.decode(buf)
+ file_size = image_obj.get_file_size()
+ print(f"got image object: {image_id.to_base58()}, size: {file_size}")
+
+
+ image_data = KnowledgeStore().get_chunk_reader().read_chunk_list_to_single_bytes(image_obj.get_chunk_list())
+ self.assertEqual(file_size, len(image_data))
+
+ from PIL import Image
+ import io
+ image = Image.open(io.BytesIO(image_data))
+ image.show()
+
+ from sentence_transformers import SentenceTransformer
+ #model = SentenceTransformer('clip-ViT-B-32-multilingual-v1')
+ model = SentenceTransformer('clip-ViT-B-32')
+ model.encode(image, convert_to_tensor=True)
+ '''
+
+ def test_relation(self):
+ obj1 = ObjectID.hash_data("12345".encode("utf-8"))
+ obj2 = ObjectID.hash_data("67890".encode("utf-8"))
+ obj3 = ObjectID.hash_data("abcde".encode("utf-8"))
+ obj4 = ObjectID.hash_data("fghij".encode("utf-8"))
+ print(obj1.to_base58(), obj2.to_base58(), obj3.to_base58())
+ relation_store = KnowledgeStore().get_relation_store()
+ relation_store.add_relation(obj1, obj2)
+ relation_store.add_relation(obj1, obj2)
+ relation_store.add_relation(obj2, obj3)
+
+ relation_store.add_relation(obj1, obj3)
+ relation_store.add_relation(obj1, obj4)
+
+ objs = relation_store.get_related_objects(obj2)
+ self.assertEqual(len(objs), 1)
+ self.assertEqual(objs[0], obj3)
+
+ objs = relation_store.get_related_root_objects(obj1)
+ self.assertEqual(len(objs), 2)
+ self.assertEqual(obj3 in objs, True)
+ self.assertEqual(obj4 in objs, True)
+ # self.assertCountEqual(objs, [obj3, obj4])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_sd_api.py b/test/test_sd_api.py
new file mode 100644
index 0000000..0f4544a
--- /dev/null
+++ b/test/test_sd_api.py
@@ -0,0 +1,56 @@
+import os
+import time
+import uuid
+import io
+import asyncio
+import sys
+import logging
+import pytest
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../src')
+from aios_kernel.stability_node import Stability_ComputeNode
+from aios_kernel.compute_task import ComputeTaskType, ComputeTask, ComputeTaskState
+
+os.environ["STABILITY_API_KEY"] = ""
+os.environ["STABILITY_DEFAULT_MODEL"] = "stable-diffusion-512-v2-1"
+os.environ["TEXT2IMG_OUTPUT_DIR"] = "./"
+
+@pytest.mark.asyncio
+async def test_sd_api(propmt, model):
+ node = Stability_ComputeNode.get_instance()
+ if await node.initial() is not True:
+ print("node initial failed!")
+ return
+
+ task = ComputeTask()
+ task.task_type = ComputeTaskType.TEXT_2_IMAGE
+ task.create_time = time.time()
+ task.task_id = uuid.uuid4().hex
+ task.params['model_name'] = model
+ task.params['prompt'] = propmt
+ await node.push_task(task)
+
+ while True:
+ if task.state == ComputeTaskState.DONE:
+ local_file = task.result.result
+ print("local file is: ", local_file)
+ break
+ await asyncio.sleep(1)
+
+ # result = node._run_task(task)
+ # print("result is: ", result)
+
+ # if result.result is not None:
+ # local_file = result.result
+ # print("local file is: ", local_file)
+
+if __name__ == "__main__":
+ arg_len = len(os.sys.argv)
+ prompt = "a beautiful sunset"
+ model = "stable-diffusion-512-v2-1"
+ if arg_len >= 2:
+ prompt = os.sys.argv[1]
+ if arg_len == 3:
+ model = os.sys.argv[2]
+
+ asyncio.run(test_sd_api(prompt, model))
diff --git a/test/test_toml.py b/test/test_toml.py
new file mode 100644
index 0000000..1e19f1c
--- /dev/null
+++ b/test/test_toml.py
@@ -0,0 +1,6 @@
+import toml
+import os;
+
+directory = os.path.dirname(__file__)
+cfg = toml.load(directory + "\pkg.cfg.toml")
+print(cfg)
\ No newline at end of file
diff --git a/test/test_vector_storage.py b/test/test_vector_storage.py
new file mode 100644
index 0000000..ff4dc72
--- /dev/null
+++ b/test/test_vector_storage.py
@@ -0,0 +1,29 @@
+import sys
+import os
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+print(dir_path)
+
+sys.path.append("{}/../src/".format(dir_path))
+print(sys.path)
+
+from knowledge import ChromaVectorStore
+
+
+import asyncio
+import unittest
+
+
+async def test_vector():
+ storage = ChromaVectorStore("test")
+ await storage.insert([1, 2, 3], "test")
+ ids = await storage.query([1, 2, 3], 10)
+ print(ids)
+
+class TestVectorStorage(unittest.TestCase):
+ def test_run(self):
+ asyncio.run(test_vector())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/workflow_test.py b/test/workflow_test.py
new file mode 100644
index 0000000..3e5e586
--- /dev/null
+++ b/test/workflow_test.py
@@ -0,0 +1,60 @@
+import os
+import sys
+import asyncio
+
+directory = os.path.dirname(__file__)
+sys.path.append(directory + '/../src')
+from aios_kernel import WorkspaceEnvironment
+
+async def test_workflow():
+ env = WorkspaceEnvironment("test")
+
+ test_code ="""
+import toml
+print("hello world")
+print(100+23)
+toml.dump({"abc":"123"},open("test.toml","w"))
+ """
+ await env.run_code(test_code)
+
+
+
+async def _test_llm_parser():
+ test_llm_result = """
+# Foggie with AI Agent
+1.已经完成了基础系统改造,只要Foggie能安装docker image就可以实现集成
+2.安装后,用户需要提供OpenAI Token和TG Bot Token,就可以构建自己的私有AI机器人(也可以通过绑定email实现智能邮件客服)
+ 我们也可以用自己的OpenAI Token给用户用,但这需要设计新的商品。OpenAI Token用起来还是挺贵的
+3.已在发布前夕,目前集成测试的主要问题是对Email和个人文件的AI分析需要比较强的性能。
+
+# DMC开源挖矿软件
+正在等待解决 Order Placement Issue
+
+
+# Foggie with AI Agent
+1. We have completed the basic system transformation. As long as Foggie can install the docker image, integration can be achieved.
+2. After installation, users need to provide an OpenAI Token and TG Bot Token to build their own private AI robot. This can also be accomplished by linking an email to implement an intelligent email customer service. We could use our own OpenAI Token for users, but this would require the design of a new product. Using the OpenAI Token can be quite costly.
+3. We are on the eve of launch. The main issue in our integrated testing currently is that AI analysis of emails and personal files requires substantial performance, and the results don't seem so smart.
+
+#DMC Open Source Mining Software
+We are waiting to resolve the Order Placement Issue.
+
+##/send_msg "xxx xxx"
+abcdcdsdf
+sfsadfasdf
+# Foggie with AI Agent
+1. We have completed the basic system transformation. As long as Foggie can install the docker image, integration can be achieved.
+2. After installation, users need to provide an OpenAI Token and TG Bot Token to build their own private AI robot. This can also be accomplished by linking an email to implement an intelligent email customer service. We could use our own OpenAI Token for users, but this would require the design of a new product. Using the OpenAI Token can be quite costly.
+3. We are on the eve of launch. The main issue in our integrated testing cur
+
+##/call abcd "xxx xxx"
+"""
+ llm_result = Workflow.prase_llm_result(test_llm_result)
+ assert len(llm_result.calls) == 1
+ assert len(llm_result.send_msgs) == 1
+ print(llm_result)
+
+if __name__ == "__main__":
+ asyncio.run(test_workflow())
+ print("OK!")
+