{\n\n // Custom element to encapsulate Mermaid content.\n class MermaidDiv extends HTMLElement {\n\n /**\n * Creates a special Mermaid div shadow DOM.\n * Works around issues of shared IDs.\n * @return {void}\n */\n constructor() {\n super()\n\n // Create the Shadow DOM and attach style\n const shadow = this.attachShadow({mode: \"open\"})\n const style = document.createElement(\"style\")\n style.textContent = `\n :host {\n display: block;\n line-height: initial;\n font-size: 16px;\n }\n div.diagram {\n margin: 0;\n overflow: visible;\n }`\n shadow.appendChild(style)\n }\n }\n\n if (typeof customElements.get(\"diagram-div\") === \"undefined\") {\n customElements.define(\"diagram-div\", MermaidDiv)\n }\n\n const getFromCode = parent => {\n // Handles text extraction.\n let text = \"\"\n for (let j = 0; j < parent.childNodes.length; j++) {\n const subEl = parent.childNodes[j]\n if (subEl.tagName.toLowerCase() === \"code\") {\n for (let k = 0; k < subEl.childNodes.length; k++) {\n const child = subEl.childNodes[k]\n const whitespace = /^\\s*$/\n if (child.nodeName === \"#text\" && !(whitespace.test(child.nodeValue))) {\n text = child.nodeValue\n break\n }\n }\n }\n }\n return text\n }\n\n // We use this to determine if we want the dark or light theme.\n // This is specific for our MkDocs Material environment.\n // You should load your configs based on your own environment's needs.\n const defaultConfig = {\n startOnLoad: false,\n theme: \"default\",\n flowchart: {\n htmlLabels: false\n },\n er: {\n useMaxWidth: false\n },\n sequence: {\n useMaxWidth: false,\n noteFontWeight: \"14px\",\n actorFontSize: \"14px\",\n messageFontSize: \"16px\"\n }\n }\n mermaid.mermaidAPI.globalReset()\n // Non Material themes should just use \"default\"\n let scheme = null\n try {\n scheme = document.querySelector(\"[data-md-color-scheme]\").getAttribute(\"data-md-color-scheme\")\n } catch (err) {\n scheme = \"default\"\n }\n const config = (typeof mermaidConfig === \"undefined\") ?\n defaultConfig :\n mermaidConfig[scheme] || (mermaidConfig.default || defaultConfig)\n mermaid.initialize(config)\n\n // Find all of our Mermaid sources and render them.\n const blocks = document.querySelectorAll(`pre.${className}, diagram-div`)\n const surrogate = document.querySelector(\"html body\")\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i]\n const parentEl = (block.tagName.toLowerCase() === \"diagram-div\") ?\n block.shadowRoot.querySelector(`pre.${className}`) :\n block\n\n // Create a temporary element with the typeset and size we desire.\n // Insert it at the end of our parent to render the SVG.\n const temp = document.createElement(\"div\")\n temp.style.visibility = \"hidden\"\n temp.style.display = \"display\"\n temp.style.padding = \"0\"\n temp.style.margin = \"0\"\n temp.style.lineHeight = \"initial\"\n temp.style.fontSize = \"16px\"\n surrogate.appendChild(temp)\n\n try {\n const res = await mermaid.render(`_diagram_${i}`, getFromCode(parentEl), temp)\n const content = res.svg\n const fn = res.bindFunctions\n const el = document.createElement(\"div\")\n el.className = className\n el.innerHTML = content\n if (fn) {\n fn(el)\n }\n\n // Insert the render where we want it and remove the original text source.\n // Mermaid will clean up the temporary element.\n const shadow = document.createElement(\"diagram-div\")\n shadow.shadowRoot.appendChild(el)\n block.parentNode.insertBefore(shadow, block)\n parentEl.style.display = \"none\"\n shadow.shadowRoot.appendChild(parentEl)\n if (parentEl !== block) {\n block.parentNode.removeChild(block)\n }\n } catch (err) {} // eslint-disable-line no-empty\n\n if (surrogate.contains(temp)) {\n surrogate.removeChild(temp)\n }\n }\n}\n","import uml from \"./uml\"\nimport arithmatex from \"./arithmatex\"\n\n// Main function\n(() => {\n let umlPromise = Promise.resolve()\n let mathPromise = Promise.resolve()\n\n const observer = new MutationObserver(mutations => {\n mutations.forEach(mutation => {\n if (mutation.type === \"attributes\") {\n let scheme = mutation.target.getAttribute(\"data-md-color-scheme\")\n if (!scheme) {\n scheme = \"default\"\n }\n localStorage.setItem(\"data-md-color-scheme\", scheme)\n if (typeof mermaid !== \"undefined\") {\n uml(\"diagram\")\n }\n }\n })\n })\n\n const main = () => {\n observer.observe(document.querySelector(\"body\"), {attributeFilter: [\"data-md-color-scheme\"]})\n\n if (typeof mermaid !== \"undefined\") {\n umlPromise = umlPromise.then(() => {\n uml(\"diagram\")\n }).catch(err => {\n console.log(`UML loading failed...${err}`) // eslint-disable-line no-console\n })\n }\n\n if (typeof katex !== \"undefined\") {\n mathPromise = mathPromise.then(() => {\n arithmatex(\"arithmatex\", \"katex\")\n }).catch(err => {\n console.log(`Math loading failed...${err}`) // eslint-disable-line no-console\n })\n } else if (typeof MathJax !== \"undefined\" && 'typesetPromise' in MathJax) {\n mathPromise = mathPromise.then(() => {\n arithmatex(\"arithmatex\", \"mathjax\")\n }).catch(err => {\n console.log(`Math loading failed...${err}`) // eslint-disable-line no-console\n })\n }\n }\n\n if (window.document$) {\n // Material specific hook\n window.document$.subscribe(main)\n } else {\n // Normal non-Material specific hook\n document.addEventListener(\"DOMContentLoaded\", main)\n }\n})()\n","export default (className, mode) => {\n if (mode === 'katex') {\n const maths = document.querySelectorAll(`.${className}`)\n\n for (let i = 0; i < maths.length; i++) {\n const tex = maths[i].textContent || maths[i].innerText\n\n if (tex.startsWith('\\\\(') && tex.endsWith('\\\\)')) {\n katex.render(tex.slice(2, -2), maths[i], {'displayMode': false})\n } else if (tex.startsWith('\\\\[') && tex.endsWith('\\\\]')) {\n katex.render(tex.slice(2, -2), maths[i], {'displayMode': true})\n }\n }\n } else if (mode === 'mathjax') {\n MathJax.startup.output.clearCache()\n MathJax.typesetClear()\n MathJax.texReset()\n MathJax.typesetPromise()\n }\n}\n"],"names":["umlPromise","mathPromise","observer","main","uml","_ref","_regeneratorRuntime","mark","_callee","className","MermaidDiv","getFromCode","defaultConfig","scheme","config","blocks","surrogate","i","block","parentEl","temp","res","content","fn","el","shadow","wrap","_context","prev","next","_HTMLElement","_inherits","_super","_this","_classCallCheck","call","this","attachShadow","mode","style","document","createElement","textContent","appendChild","_wrapNativeSuper","HTMLElement","customElements","get","define","parent","text","j","childNodes","length","subEl","tagName","toLowerCase","k","child","nodeName","test","nodeValue","startOnLoad","theme","flowchart","htmlLabels","er","useMaxWidth","sequence","noteFontWeight","actorFontSize","messageFontSize","mermaid","mermaidAPI","globalReset","querySelector","getAttribute","err","mermaidConfig","initialize","querySelectorAll","concat","shadowRoot","visibility","display","padding","margin","lineHeight","fontSize","render","sent","svg","bindFunctions","innerHTML","parentNode","insertBefore","removeChild","t0","contains","stop","_x","apply","arguments","arithmatex","maths","tex","innerText","startsWith","endsWith","katex","slice","displayMode","MathJax","startup","output","clearCache","typesetClear","texReset","typesetPromise","Promise","resolve","MutationObserver","mutations","forEach","mutation","type","target","localStorage","setItem","observe","attributeFilter","then","console","log","window","document$","subscribe","addEventListener"],"mappings":"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"}
diff --git a/docs/theme/assets/pymdownx-extras/material-extra-3rdparty-E-i8w1WA.js b/docs/theme/assets/pymdownx-extras/material-extra-3rdparty-E-i8w1WA.js
new file mode 100644
index 0000000..977bf5e
--- /dev/null
+++ b/docs/theme/assets/pymdownx-extras/material-extra-3rdparty-E-i8w1WA.js
@@ -0,0 +1,2 @@
+!function(){"use strict";"mathjaxConfig"in window||(window.MathJax={tex:{inlineMath:[["\\(","\\)"]],displayMath:[["\\[","\\]"]],processEscapes:!0,processEnvironments:!0,tagSide:"right",tagIndent:".8em",multlineWidth:"85%",tags:"ams"},options:{ignoreHtmlClass:".*",processHtmlClass:"arithmatex"}}),"mermaidConfig"in window||(window.mermaidConfig={dracula:{startOnLoad:!1,theme:"base",themeCSS:" * { --drac-page-bg: hsl(233, 15%, 23%); --drac-white-fg: hsl(60, 30%, 96%); --drac-purple-fg: hsl(265, 89%, 78%); --drac-purple-bg: hsl(265, 25%, 39%); --drac-yellow-fg: hsl(65, 92%, 76%); --drac-blue-fg: hsl(225, 27%, 51%); } /* General */ [id^='_diagram'] { background-color: var(--drac-page-bg); } /* Entity Relationship */ rect.relationshipLabelBox { opacity: 0.75 !important; fill: var(--drac-purple-bg) !important; } defs marker#ZERO_OR_MORE_END circle { fill: var(--drac-page-bg) !important; stroke: var(--drac-purple-fg) !important; } defs marker#ZERO_OR_MORE_END path { stroke: var(--drac-purple-fg) !important; } defs marker#ZERO_OR_MORE_START circle{ fill: var(--drac-page-bg) !important; stroke: var(--drac-purple-fg) !important; } defs marker#ZERO_OR_MORE_START path { stroke: var(--drac-purple-fg) !important; } defs marker#ONLY_ONE_START path { stroke: var(--drac-purple-fg) !important; } defs marker#ONLY_ONE_END path { stroke: var(--drac-purple-fg) !important; } defs marker#ZERO_OR_ONE_START path { stroke: var(--drac-purple-fg) !important; } defs marker#ZERO_OR_ONE_END path { stroke: var(--drac-purple-fg) !important; } defs marker#ONE_OR_MORE_START path { stroke: var(--drac-purple-fg) !important; } defs marker#ONE_OR_MORE_END path { stroke: var(--drac-purple-fg) !important; } /* Flowchart */ .labelText, :not(.branchLabel) > .label text { fill: var(--drac-purple-fg); } .edgeLabel text { fill: var(--drac-purple-fg) !important; } .edgeLabel rect { opacity: 0.75 !important; fill: var(--drac-purple-bg) !important; } .grey rect.label-container { fill: var(--drac-purple-bg) !important; stroke: var(--drac-purple-fg) !important; } /* Sequence */ line[id^='actor'] { stroke: var(--drac-blue-fg); } .noteText { fill: var(--drac-yellow-fg); } /* Gantt */ .sectionTitle { fill: var(--drac-purple-fg) !important; } .grid .tick line { stroke: var(--drac-blue-fg) !important; } .grid .tick text { fill: var(--drac-purple-fg); } /* Class Diagram */ .statediagram-state rect.divider { fill: transparent !important; } /* State Diagram */ .stateGroup circle[style$=\"fill: black;\"] { fill: var(--drac-purple-bg) !important; stroke: var(--drac-purple-bg) !important; } .stateGroup circle[style$=\"fill: white;\"] { fill: var(--drac-purple-bg) !important; stroke: var(--drac-purple-fg) !important; } .stateGroup .composit { fill: var(--drac-page-bg); } /* Pie */ text.slice { fill: var(--drac-white-fg) !important; } /* Git Graph */ .commit-bullets .commit-reverse, .commit-bullets .commit-merge, .commit-bullets .commit-highlight-inner { fill: var(--drac-page-bg) !important; stroke: var(--drac-page-bg) !important; } ",themeVariables:{darkMode:!0,background:"#323443",mainBkg:"#604b7d",textColor:"#bf95f9",lineColor:"#bf95f9",errorBkgColor:"#802c2c",errorTextColor:"#ff5757",primaryColor:"#604b7d",primaryTextColor:"#bf95f9",primaryBorderColor:"#bf95f9",secondaryColor:"#297d3e",secondaryTextColor:"#52fa7c",secondaryBorderColor:"#52fa7c",tertiaryColor:"#303952",tertiaryTextColor:"#6071a4",tertiaryBorderColor:"#6071a4",noteBkgColor:"#797d45",noteTextColor:"#f1fa89",noteBorderColor:"#f1fa89",edgeLabelBackground:"#604b7d",edgeLabelText:"#604b7d",actorLineColor:"#6071a4",activeTaskBkgColor:"#803d63",activeTaskBorderColor:"#ff7ac6",doneTaskBkgColor:"#297d3e",doneTaskBorderColor:"#52fa7c",critBkgColor:"#802c2c",critBorderColor:"#ff5757",taskTextColor:"#bf95f9",taskTextOutsideColor:"#bf95f9",taskTextLightColor:"#bf95f9",sectionBkgColor:"#bf95f9b3",sectionBkgColor2:"#bf95f966",altSectionBkgColor:"#323443",todayLineColor:"#ff7ac6",gridColor:"#6071a4",defaultLinkColor:"#8be8fd",altBackground:"#bf95f9",classText:"#bf95f9",fillType0:"#406080",fillType1:"#46747f",fillType2:"#297d3e",fillType3:"#805c36",fillType4:"#803d63",fillType5:"#604b7d",fillType6:"#802c2c",fillType7:"#797d45",fillType8:"#7c7c79",git0:"#ff5555",git1:"#ffb86c",git2:"#f1fa8c",git3:"#50fa7b",git4:"#8be9fd",git5:"#809fff",git6:"#ff79c6",git7:"#bd93f9",gitInv0:"#ff5555",gitInv1:"#ffb86c",gitInv2:"#f1fa8c",gitInv3:"#50fa7b",gitInv4:"#8be9fd",gitInv5:"#809fff",gitInv6:"#ff79c6",gitInv7:"#bd93f9",gitBranchLabel0:"#323443",gitBranchLabel1:"#323443",gitBranchLabel2:"#323443",gitBranchLabel3:"#323443",gitBranchLabel4:"#323443",gitBranchLabel5:"#323443",gitBranchLabel6:"#323443",gitBranchLabel7:"#323443",commitLabelColor:"#52fa7c",commitLabelBackground:"#297d3e"},flowchart:{htmlLabels:!1,useMaxWidth:!1},er:{useMaxWidth:!1},sequence:{useMaxWidth:!1,noteFontWeight:"14px",actorFontSize:"14px",messageFontSize:"16px"},journey:{useMaxWidth:!1},pie:{useMaxWidth:!1},gantt:{useMaxWidth:!1},gitGraph:{useMaxWidth:!1}},default:{startOnLoad:!1,theme:"default",flowchart:{htmlLabels:!1,useMaxWidth:!1},er:{useMaxWidth:!1},sequence:{useMaxWidth:!1,noteFontWeight:"14px",actorFontSize:"14px",messageFontSize:"16px"},journey:{useMaxWidth:!1},pie:{useMaxWidth:!1},gantt:{useMaxWidth:!1},gitGraph:{useMaxWidth:!1}},slate:{startOnLoad:!1,theme:"dark",flowchart:{htmlLabels:!1,useMaxWidth:!1},er:{useMaxWidth:!1},sequence:{useMaxWidth:!1,noteFontWeight:"14px",actorFontSize:"14px",messageFontSize:"16px"},journey:{useMaxWidth:!1},pie:{useMaxWidth:!1},gantt:{useMaxWidth:!1},gitGraph:{useMaxWidth:!1}}})}();
+//# sourceMappingURL=material-extra-3rdparty-E-i8w1WA.js.map
diff --git a/docs/theme/assets/pymdownx-extras/material-extra-3rdparty-E-i8w1WA.js.map b/docs/theme/assets/pymdownx-extras/material-extra-3rdparty-E-i8w1WA.js.map
new file mode 100644
index 0000000..e1739c4
--- /dev/null
+++ b/docs/theme/assets/pymdownx-extras/material-extra-3rdparty-E-i8w1WA.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"material-extra-3rdparty-E-i8w1WA.js","sources":["material-extra-3rdparty.js"],"sourcesContent":["// MathJax configuration\n\nif (!('mathjaxConfig' in window)) {\n window.MathJax = {\n tex: {\n inlineMath: [[\"\\\\(\", \"\\\\)\"]],\n displayMath: [[\"\\\\[\", \"\\\\]\"]],\n processEscapes: true,\n processEnvironments: true,\n tagSide: \"right\",\n tagIndent: \".8em\",\n multlineWidth: \"85%\",\n tags: \"ams\"\n },\n options: {\n ignoreHtmlClass: \".*\",\n processHtmlClass: \"arithmatex\"\n }\n }\n}\n\nif (!('mermaidConfig' in window)) {\n // Our loader looks for `mermaidConfig` and will load the the appropriate\n // configuration based on our current scheme: light, dark, etc.\n window.mermaidConfig = {\n dracula: {\n startOnLoad: false,\n theme: \"base\",\n themeCSS: \"\\\n * {\\\n --drac-page-bg: hsl(233, 15%, 23%);\\\n --drac-white-fg: hsl(60, 30%, 96%);\\\n --drac-purple-fg: hsl(265, 89%, 78%);\\\n --drac-purple-bg: hsl(265, 25%, 39%);\\\n --drac-yellow-fg: hsl(65, 92%, 76%);\\\n --drac-blue-fg: hsl(225, 27%, 51%);\\\n }\\\n \\\n /* General */\\\n [id^='_diagram'] {\\\n background-color: var(--drac-page-bg);\\\n }\\\n \\\n /* Entity Relationship */\\\n rect.relationshipLabelBox {\\\n opacity: 0.75 !important;\\\n fill: var(--drac-purple-bg) !important;\\\n }\\\n defs marker#ZERO_OR_MORE_END circle {\\\n fill: var(--drac-page-bg) !important;\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ZERO_OR_MORE_END path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ZERO_OR_MORE_START circle{\\\n fill: var(--drac-page-bg) !important;\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ZERO_OR_MORE_START path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ONLY_ONE_START path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ONLY_ONE_END path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ZERO_OR_ONE_START path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ZERO_OR_ONE_END path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ONE_OR_MORE_START path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n defs marker#ONE_OR_MORE_END path {\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n \\\n /* Flowchart */\\\n .labelText,\\\n :not(.branchLabel) > .label text {\\\n fill: var(--drac-purple-fg);\\\n }\\\n .edgeLabel text {\\\n fill: var(--drac-purple-fg) !important;\\\n }\\\n .edgeLabel rect {\\\n opacity: 0.75 !important;\\\n fill: var(--drac-purple-bg) !important;\\\n }\\\n \\\n .grey rect.label-container { \\\n fill: var(--drac-purple-bg) !important;\\\n stroke: var(--drac-purple-fg) !important;\\\n } \\\n /* Sequence */\\\n line[id^='actor'] {\\\n stroke: var(--drac-blue-fg);\\\n }\\\n .noteText {\\\n fill: var(--drac-yellow-fg);\\\n }\\\n \\\n /* Gantt */\\\n .sectionTitle {\\\n fill: var(--drac-purple-fg) !important;\\\n }\\\n \\\n .grid .tick line {\\\n stroke: var(--drac-blue-fg) !important;\\\n }\\\n \\\n .grid .tick text {\\\n fill: var(--drac-purple-fg);\\\n }\\\n \\\n /* Class Diagram */\\\n .statediagram-state rect.divider {\\\n fill: transparent !important;\\\n }\\\n \\\n /* State Diagram */\\\n .stateGroup circle[style$=\\\"fill: black;\\\"] {\\\n fill: var(--drac-purple-bg) !important;\\\n stroke: var(--drac-purple-bg) !important;\\\n }\\\n \\\n .stateGroup circle[style$=\\\"fill: white;\\\"] {\\\n fill: var(--drac-purple-bg) !important;\\\n stroke: var(--drac-purple-fg) !important;\\\n }\\\n \\\n .stateGroup .composit {\\\n fill: var(--drac-page-bg);\\\n }\\\n /* Pie */\\\n text.slice {\\\n fill: var(--drac-white-fg) !important;\\\n }\\\n /* Git Graph */\\\n .commit-bullets .commit-reverse,\\\n .commit-bullets .commit-merge, \\\n .commit-bullets .commit-highlight-inner {\\\n fill: var(--drac-page-bg) !important;\\\n stroke: var(--drac-page-bg) !important;\\\n }\\\n \",\n themeVariables: {\n darkMode: true,\n background: \"#323443\",\n mainBkg: \"#604b7d\",\n textColor: \"#bf95f9\",\n lineColor: \"#bf95f9\",\n errorBkgColor: \"#802c2c\",\n errorTextColor: \"#ff5757\",\n primaryColor: \"#604b7d\",\n primaryTextColor: \"#bf95f9\",\n primaryBorderColor: \"#bf95f9\",\n secondaryColor: \"#297d3e\",\n secondaryTextColor: \"#52fa7c\",\n secondaryBorderColor: \"#52fa7c\",\n tertiaryColor: \"#303952\",\n tertiaryTextColor: \"#6071a4\",\n tertiaryBorderColor: \"#6071a4\",\n noteBkgColor: \"#797d45\",\n noteTextColor: \"#f1fa89\",\n noteBorderColor: \"#f1fa89\",\n edgeLabelBackground: \"#604b7d\",\n edgeLabelText: \"#604b7d\",\n\n actorLineColor: \"#6071a4\",\n\n activeTaskBkgColor: \"#803d63\",\n activeTaskBorderColor: \"#ff7ac6\",\n doneTaskBkgColor: \"#297d3e\",\n doneTaskBorderColor: \"#52fa7c\",\n critBkgColor: \"#802c2c\",\n critBorderColor: \"#ff5757\",\n taskTextColor: \"#bf95f9\",\n taskTextOutsideColor: \"#bf95f9\",\n taskTextLightColor: \"#bf95f9\",\n sectionBkgColor: \"#bf95f9b3\",\n sectionBkgColor2: \"#bf95f966\",\n altSectionBkgColor: \"#323443\",\n todayLineColor: \"#ff7ac6\",\n gridColor: \"#6071a4\",\n defaultLinkColor: \"#8be8fd\",\n\n altBackground: \"#bf95f9\",\n\n classText: \"#bf95f9\",\n\n fillType0: \"#406080\",\n fillType1: \"#46747f\",\n fillType2: \"#297d3e\",\n fillType3: \"#805c36\",\n fillType4: \"#803d63\",\n fillType5: \"#604b7d\",\n fillType6: \"#802c2c\",\n fillType7: \"#797d45\",\n fillType8: \"#7c7c79\",\n\n git0: \"#ff5555\",\n git1: \"#ffb86c\",\n git2: \"#f1fa8c\",\n git3: \"#50fa7b\",\n git4: \"#8be9fd\",\n git5: \"#809fff\",\n git6: \"#ff79c6\",\n git7: \"#bd93f9\",\n\n gitInv0: \"#ff5555\",\n gitInv1: \"#ffb86c\",\n gitInv2: \"#f1fa8c\",\n gitInv3: \"#50fa7b\",\n gitInv4: \"#8be9fd\",\n gitInv5: \"#809fff\",\n gitInv6: \"#ff79c6\",\n gitInv7: \"#bd93f9\",\n\n gitBranchLabel0: \"#323443\",\n gitBranchLabel1: \"#323443\",\n gitBranchLabel2: \"#323443\",\n gitBranchLabel3: \"#323443\",\n gitBranchLabel4: \"#323443\",\n gitBranchLabel5: \"#323443\",\n gitBranchLabel6: \"#323443\",\n gitBranchLabel7: \"#323443\",\n\n commitLabelColor: '#52fa7c',\n commitLabelBackground: '#297d3e'\n },\n flowchart: {\n htmlLabels: false,\n useMaxWidth: false\n },\n er: {\n useMaxWidth: false\n },\n sequence: {\n useMaxWidth: false,\n // Mermaid handles Firefox a little different.\n // For some reason, it doesn't attach font sizes to the labels in Firefox.\n // If we specify the documented defaults, font sizes are written to the labels in Firefox.\n noteFontWeight: \"14px\",\n actorFontSize: \"14px\",\n messageFontSize: \"16px\"\n },\n journey: {\n useMaxWidth: false\n },\n pie: {\n useMaxWidth: false\n },\n gantt: {\n useMaxWidth: false\n },\n gitGraph: {\n useMaxWidth: false\n }\n },\n\n default: {\n startOnLoad: false,\n theme: \"default\",\n flowchart: {\n htmlLabels: false,\n useMaxWidth: false\n },\n er: {\n useMaxWidth: false\n },\n sequence: {\n useMaxWidth: false,\n noteFontWeight: \"14px\",\n actorFontSize: \"14px\",\n messageFontSize: \"16px\"\n },\n journey: {\n useMaxWidth: false\n },\n pie: {\n useMaxWidth: false\n },\n gantt: {\n useMaxWidth: false\n },\n gitGraph: {\n useMaxWidth: false\n }\n },\n\n slate: {\n startOnLoad: false,\n theme: \"dark\",\n flowchart: {\n htmlLabels: false,\n useMaxWidth: false\n },\n er: {\n useMaxWidth: false\n },\n sequence: {\n useMaxWidth: false,\n noteFontWeight: \"14px\",\n actorFontSize: \"14px\",\n messageFontSize: \"16px\"\n },\n journey: {\n useMaxWidth: false\n },\n pie: {\n useMaxWidth: false\n },\n gantt: {\n useMaxWidth: false\n },\n gitGraph: {\n useMaxWidth: false\n }\n }\n }\n}\n"],"names":["window","MathJax","tex","inlineMath","displayMath","processEscapes","processEnvironments","tagSide","tagIndent","multlineWidth","tags","options","ignoreHtmlClass","processHtmlClass","mermaidConfig","dracula","startOnLoad","theme","themeCSS","themeVariables","darkMode","background","mainBkg","textColor","lineColor","errorBkgColor","errorTextColor","primaryColor","primaryTextColor","primaryBorderColor","secondaryColor","secondaryTextColor","secondaryBorderColor","tertiaryColor","tertiaryTextColor","tertiaryBorderColor","noteBkgColor","noteTextColor","noteBorderColor","edgeLabelBackground","edgeLabelText","actorLineColor","activeTaskBkgColor","activeTaskBorderColor","doneTaskBkgColor","doneTaskBorderColor","critBkgColor","critBorderColor","taskTextColor","taskTextOutsideColor","taskTextLightColor","sectionBkgColor","sectionBkgColor2","altSectionBkgColor","todayLineColor","gridColor","defaultLinkColor","altBackground","classText","fillType0","fillType1","fillType2","fillType3","fillType4","fillType5","fillType6","fillType7","fillType8","git0","git1","git2","git3","git4","git5","git6","git7","gitInv0","gitInv1","gitInv2","gitInv3","gitInv4","gitInv5","gitInv6","gitInv7","gitBranchLabel0","gitBranchLabel1","gitBranchLabel2","gitBranchLabel3","gitBranchLabel4","gitBranchLabel5","gitBranchLabel6","gitBranchLabel7","commitLabelColor","commitLabelBackground","flowchart","htmlLabels","useMaxWidth","er","sequence","noteFontWeight","actorFontSize","messageFontSize","journey","pie","gantt","gitGraph","default","slate"],"mappings":"yBAEM,kBAAmBA,SACvBA,OAAOC,QAAU,CACfC,IAAK,CACHC,WAAY,CAAC,CAAC,MAAO,QACrBC,YAAa,CAAC,CAAC,MAAO,QACtBC,gBAAgB,EAChBC,qBAAqB,EACrBC,QAAS,QACTC,UAAW,OACXC,cAAe,MACfC,KAAM,OAERC,QAAS,CACPC,gBAAiB,KACjBC,iBAAkB,gBAKlB,kBAAmBb,SAGvBA,OAAOc,cAAgB,CACrBC,QAAS,CACPC,aAAa,EACbC,MAAO,OACPC,SAAU,w/GA0HVC,eAAgB,CACdC,UAAU,EACVC,WAAY,UACZC,QAAS,UACTC,UAAW,UACXC,UAAW,UACXC,cAAe,UACfC,eAAgB,UAChBC,aAAc,UACdC,iBAAkB,UAClBC,mBAAoB,UACpBC,eAAgB,UAChBC,mBAAoB,UACpBC,qBAAsB,UACtBC,cAAe,UACfC,kBAAmB,UACnBC,oBAAqB,UACrBC,aAAc,UACdC,cAAe,UACfC,gBAAiB,UACjBC,oBAAqB,UACrBC,cAAe,UAEfC,eAAgB,UAEhBC,mBAAoB,UACpBC,sBAAuB,UACvBC,iBAAkB,UAClBC,oBAAqB,UACrBC,aAAc,UACdC,gBAAiB,UACjBC,cAAe,UACfC,qBAAsB,UACtBC,mBAAoB,UACpBC,gBAAiB,YACjBC,iBAAkB,YAClBC,mBAAoB,UACpBC,eAAgB,UAChBC,UAAW,UACXC,iBAAkB,UAElBC,cAAe,UAEfC,UAAW,UAEXC,UAAW,UACXC,UAAW,UACXC,UAAW,UACXC,UAAW,UACXC,UAAW,UACXC,UAAW,UACXC,UAAW,UACXC,UAAW,UACXC,UAAW,UAEXC,KAAM,UACNC,KAAM,UACNC,KAAM,UACNC,KAAM,UACNC,KAAM,UACNC,KAAM,UACNC,KAAM,UACNC,KAAM,UAENC,QAAS,UACTC,QAAS,UACTC,QAAS,UACTC,QAAS,UACTC,QAAS,UACTC,QAAS,UACTC,QAAS,UACTC,QAAS,UAETC,gBAAiB,UACjBC,gBAAiB,UACjBC,gBAAiB,UACjBC,gBAAiB,UACjBC,gBAAiB,UACjBC,gBAAiB,UACjBC,gBAAiB,UACjBC,gBAAiB,UAEjBC,iBAAkB,UAClBC,sBAAuB,WAEzBC,UAAW,CACTC,YAAY,EACZC,aAAa,GAEfC,GAAI,CACFD,aAAa,GAEfE,SAAU,CACRF,aAAa,EAIbG,eAAgB,OAChBC,cAAe,OACfC,gBAAiB,QAEnBC,QAAS,CACPN,aAAa,GAEfO,IAAK,CACHP,aAAa,GAEfQ,MAAO,CACLR,aAAa,GAEfS,SAAU,CACRT,aAAa,IAIjBU,QAAS,CACP1F,aAAa,EACbC,MAAO,UACP6E,UAAW,CACTC,YAAY,EACZC,aAAa,GAEfC,GAAI,CACFD,aAAa,GAEfE,SAAU,CACRF,aAAa,EACbG,eAAgB,OAChBC,cAAe,OACfC,gBAAiB,QAEnBC,QAAS,CACPN,aAAa,GAEfO,IAAK,CACHP,aAAa,GAEfQ,MAAO,CACLR,aAAa,GAEfS,SAAU,CACRT,aAAa,IAIjBW,MAAO,CACL3F,aAAa,EACbC,MAAO,OACP6E,UAAW,CACTC,YAAY,EACZC,aAAa,GAEfC,GAAI,CACFD,aAAa,GAEfE,SAAU,CACRF,aAAa,EACbG,eAAgB,OAChBC,cAAe,OACfC,gBAAiB,QAEnBC,QAAS,CACPN,aAAa,GAEfO,IAAK,CACHP,aAAa,GAEfQ,MAAO,CACLR,aAAa,GAEfS,SAAU,CACRT,aAAa"}
diff --git a/docs/theme/assets/pymdownx-extras/material-extra-theme-TVq-kNRT.js b/docs/theme/assets/pymdownx-extras/material-extra-theme-TVq-kNRT.js
new file mode 100644
index 0000000..ce38f61
--- /dev/null
+++ b/docs/theme/assets/pymdownx-extras/material-extra-theme-TVq-kNRT.js
@@ -0,0 +1,2 @@
+!function(){"use strict";var e;e=function(e){"true"===localStorage.getItem("data-md-prefers-color-scheme")&&document.querySelector("body").setAttribute("data-md-color-scheme",e.matches?"dracula":"default")},new MutationObserver((function(t){t.forEach((function(t){if("childList"===t.type&&t.addedNodes.length)for(var a=0;a {\n\n const preferToggle = e => {\n if (localStorage.getItem(\"data-md-prefers-color-scheme\") === \"true\") {\n document.querySelector(\"body\").setAttribute(\"data-md-color-scheme\", (e.matches) ? \"dracula\" : \"default\")\n }\n }\n\n const setupTheme = body => {\n const preferSupported = window.matchMedia(\"(prefers-color-scheme)\").media !== \"not all\"\n let scheme = localStorage.getItem(\"data-md-color-scheme\")\n let prefers = localStorage.getItem(\"data-md-prefers-color-scheme\")\n\n if (!scheme) {\n scheme = \"dracula\"\n }\n if (!prefers) {\n prefers = \"false\"\n }\n\n if (prefers === \"true\" && preferSupported) {\n scheme = (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) ? \"dracula\" : \"default\"\n } else {\n prefers = \"false\"\n }\n\n body.setAttribute(\"data-md-prefers-color-scheme\", prefers)\n body.setAttribute(\"data-md-color-scheme\", scheme)\n\n if (preferSupported) {\n const matchListener = window.matchMedia(\"(prefers-color-scheme: dark)\")\n matchListener.addListener(preferToggle)\n }\n }\n\n const observer = new MutationObserver(mutations => {\n mutations.forEach(mutation => {\n if (mutation.type === \"childList\") {\n if (mutation.addedNodes.length) {\n for (let i = 0; i < mutation.addedNodes.length; i++) {\n const el = mutation.addedNodes[i]\n\n if (el.nodeType === 1 && el.tagName.toLowerCase() === \"body\") {\n setupTheme(el)\n break\n }\n }\n }\n }\n })\n })\n\n observer.observe(document.querySelector(\"html\"), {childList: true})\n})()\n\nwindow.toggleScheme = () => {\n const body = document.querySelector(\"body\")\n const preferSupported = window.matchMedia(\"(prefers-color-scheme)\").media !== \"not all\"\n let scheme = body.getAttribute(\"data-md-color-scheme\")\n let prefer = body.getAttribute(\"data-md-prefers-color-scheme\")\n\n if (preferSupported && scheme === \"default\" && prefer !== \"true\") {\n prefer = \"true\"\n scheme = (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) ? \"dracula\" : \"default\"\n } else if (preferSupported && prefer === \"true\") {\n prefer = \"false\"\n scheme = \"dracula\"\n } else if (scheme === \"dracula\") {\n prefer = \"false\"\n scheme = \"default\"\n } else {\n prefer = \"false\"\n scheme = \"dracula\"\n }\n localStorage.setItem(\"data-md-prefers-color-scheme\", prefer)\n body.setAttribute(\"data-md-prefers-color-scheme\", prefer)\n body.setAttribute(\"data-md-color-scheme\", scheme)\n}\n"],"names":["preferToggle","e","localStorage","getItem","document","querySelector","setAttribute","matches","MutationObserver","mutations","forEach","mutation","type","addedNodes","length","i","el","nodeType","tagName","toLowerCase","body","preferSupported","scheme","prefers","window","matchMedia","media","addListener","observe","childList","toggleScheme","getAttribute","prefer","setItem"],"mappings":"yBAAA,IAEQA,IAAe,SAAAC,GAC0C,SAAzDC,aAAaC,QAAQ,iCACvBC,SAASC,cAAc,QAAQC,aAAa,uBAAyBL,EAAEM,QAAW,UAAY,YA+BjF,IAAIC,kBAAiB,SAAAC,GACpCA,EAAUC,SAAQ,SAAAC,GAChB,GAAsB,cAAlBA,EAASC,MACPD,EAASE,WAAWC,OACtB,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,EAASE,WAAWC,OAAQC,IAAK,CACnD,IAAMC,EAAKL,EAASE,WAAWE,GAE/B,GAAoB,IAAhBC,EAAGC,UAA+C,SAA7BD,EAAGE,QAAQC,cAA0B,CAlCrDC,EAmCIJ,EAlCfK,SACFC,SACAC,SAFEF,EAAwE,YAAtDG,OAAOC,WAAW,0BAA0BC,MAChEJ,EAASpB,aAAaC,QAAQ,wBAC9BoB,EAAUrB,aAAaC,QAAQ,gCAE9BmB,IACHA,EAAS,WAENC,IACHA,EAAU,SAGI,SAAZA,GAAsBF,EACxBC,EAAUE,OAAOC,WAAW,gCAAgClB,QAAW,UAAY,UAEnFgB,EAAU,QAGZH,EAAKd,aAAa,+BAAgCiB,GAClDH,EAAKd,aAAa,uBAAwBgB,GAEtCD,GACoBG,OAAOC,WAAW,gCAC1BE,YAAY3B,GAalB,KACF,CACF,CAtCW,IAAAoB,EACXC,EACFC,EACAC,CAsCJ,GACF,IAESK,QAAQxB,SAASC,cAAc,QAAS,CAACwB,WAAW,IAG/DL,OAAOM,aAAe,WACpB,IAAMV,EAAOhB,SAASC,cAAc,QAC9BgB,EAAwE,YAAtDG,OAAOC,WAAW,0BAA0BC,MAChEJ,EAASF,EAAKW,aAAa,wBAC3BC,EAASZ,EAAKW,aAAa,gCAE3BV,GAA8B,YAAXC,GAAmC,SAAXU,GAC7CA,EAAS,OACTV,EAAUE,OAAOC,WAAW,gCAAgClB,QAAW,UAAY,WAC1Ec,GAA8B,SAAXW,GAC5BA,EAAS,QACTV,EAAS,WACW,YAAXA,GACTU,EAAS,QACTV,EAAS,YAETU,EAAS,QACTV,EAAS,WAEXpB,aAAa+B,QAAQ,+BAAgCD,GACrDZ,EAAKd,aAAa,+BAAgC0B,GAClDZ,EAAKd,aAAa,uBAAwBgB,EAC5C"}
diff --git a/docs/theme/main.html b/docs/theme/main.html
new file mode 100644
index 0000000..4b5209e
--- /dev/null
+++ b/docs/theme/main.html
@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+
+{% block libs %}
+{{ super() }}
+{% include "partials/libs.html" ignore missing %}
+{% endblock %}
diff --git a/docs/theme/partials/footer.html b/docs/theme/partials/footer.html
new file mode 100644
index 0000000..5a9cdc1
--- /dev/null
+++ b/docs/theme/partials/footer.html
@@ -0,0 +1,49 @@
+
+{% import "partials/language.html" as lang with context %}
+
diff --git a/docs/theme/partials/header.html b/docs/theme/partials/header.html
new file mode 100644
index 0000000..4e21967
--- /dev/null
+++ b/docs/theme/partials/header.html
@@ -0,0 +1,88 @@
+
+{% set site_url = config.site_url | d(nav.homepage.url, true) | url %}
+{% if not config.use_directory_urls and site_url[0] == site_url[-1] == "." %}
+ {% set site_url = site_url ~ "/index.html" %}
+{% endif %}
+
diff --git a/docs/theme/partials/libs.html b/docs/theme/partials/libs.html
new file mode 100644
index 0000000..8041b12
--- /dev/null
+++ b/docs/theme/partials/libs.html
@@ -0,0 +1,2 @@
+
+
diff --git a/docs/ultilities.md b/docs/ultilities.md
new file mode 100644
index 0000000..225b443
--- /dev/null
+++ b/docs/ultilities.md
@@ -0,0 +1,172 @@
+Utilities detail can be referred in the sub-pages of this section.
+
+## Prompt engineering UI
+
+
+
+**_Important:_** despite the name prompt engineering UI, this tool allows DMs to test any kind of parameters that are exposed by AIRs. Prompt is one kind of param. There can be other type of params that DMs can tweak (e.g. top_k, temperature...).
+
+**_Note:_** For hands-on examination of how to use prompt engineering UI, refer `./examples/promptui` and `./examples/example2/`
+
+In client projects, AI developers typically build the pipeline. However, for LLM projects requiring Japanese and domain expertise in prompt creation, non-technical team members (DM, BizDev, and QALs) can be more effective. To facilitate this, "xxx" offers a user-friendly prompt engineering UI that AI developers integrate into their pipelines. This enables non-technical members to adjust prompts and parameters, run experiments, and export results for optimization.
+
+As of Sept 2023, there are 2 kinds of prompt engineering UI:
+
+- Simple pipeline: run one-way from start to finish.
+- Chat pipeline: interactive back-and-forth.
+
+### Simple pipeline
+
+For simple pipeline, the supported client project workflow looks as follow:
+
+1. [AIR] Build pipeline
+2. [AIR] Export pipeline to config: `$ kh promptui export --output `
+3. [AIR] Customize the config
+4. [AIR] Spin up prompt engineering UI: `$ kh promptui run `
+5. [DM] Change params, run inference
+6. [DM] Export to Excel
+7. [DM] Select the set of params that achieve the best output
+
+The prompt engineering UI prominently involves from step 2 to step 7 (step 1 is normal AI tasks in project, while step 7 happens exclusively in Excel file).
+
+#### Step 2 - Export pipeline to config
+
+Command:
+
+```
+$ kh promptui export --output
+```
+
+where:
+
+- `` is a dot-separated path to the pipeline. For example, if your pipeline can be accessed with `from projectA.pipelines import AnsweringPipeline`, then this value is `projectA.pipelines.AnswerPipeline`.
+- `` is the target file path that the config will be exported to. If the config file already exists, and contains information of other pipelines, the config of current pipeline will additionally be added. If it contains information of the current pipeline (in the past), the old information will be replaced.
+
+By default, all params in a pipeline (including nested params) will be export to the configuration file. For params that you do not wish to expose to the UI, you can directly remove them from the config YAML file. You can also annotate those param with `ignore_ui=True`, and they will be ignored in the config generation process. Example:
+
+```python
+class Pipeline(BaseComponent):
+ param1: str = Param(default="hello")
+ param2: str = Param(default="goodbye", ignore_ui=True)
+```
+
+Declared as above, and `param1` will show up in the config YAML file, while `param2` will not.
+
+#### Step 3 - Customize the config
+
+AIR can further edit the config file in this step to get the most suitable UI (step 4) with their tasks. The exported config will have this overall schema:
+
+```
+:
+ params:
+ ... (Detail param information to initiate a pipeline. This corresponds to the pipeline init parameters.)
+ inputs:
+ ... (Detail the input of the pipeline e.g. a text prompt, an FNOL... This corresponds to the params of `run(...)` method.)
+ outputs:
+ ... (Detail the output of the pipeline e.g. prediction, accuracy... This is the output information we wish to see in the UI.)
+ logs:
+ ... (Detail what information should show up in the log.)
+```
+
+##### Input and params
+
+The inputs section have the overall schema as follow:
+
+```
+inputs:
+ :
+ component:
+ params: # this section is optional)
+ value:
+ :
+ ... # similar to above
+params:
+ :
+ ... # similar to those in the inputs
+```
+
+The list of supported prompt UI and their corresponding gradio UI components:
+
+```
+COMPONENTS_CLASS = {
+ "text": gr.components.Textbox,
+ "checkbox": gr.components.CheckboxGroup,
+ "dropdown": gr.components.Dropdown,
+ "file": gr.components.File,
+ "image": gr.components.Image,
+ "number": gr.components.Number,
+ "radio": gr.components.Radio,
+ "slider": gr.components.Slider,
+}
+```
+
+##### Outputs
+
+The outputs are a list of variables that we wish to show in the UI. Since in Python, the function output doesn't have variable name, so output declaration is a little bit different than input and param declaration:
+
+```
+outputs:
+ - component:
+ step:
+ item:
+ - ... # similar to above
+```
+
+where:
+
+- component: the same text string and corresponding Gradio UI as in inputs & params
+- step: the pipeline step that we wish to look fetch and show output on the UI
+- item: the jsonpath mechanism to get the targeted variable from the step above
+
+##### Logs
+
+The logs show a list of sheetname and how to retrieve the desired information.
+
+```
+logs:
+ :
+ inputs:
+ - name:
+ step:
+ variable:
+ - ...
+ outputs:
+ - name:
+ step:
+ item:
+```
+
+#### Step 4 + 5 - Spin up prompt engineering UI + Perform prompt engineering
+
+Command:
+
+```
+$ kh promptui run
+```
+
+This will generate an UI as follow:
+
+
+
+where:
+
+- The tabs at the top of the UI corresponds to the pipeline to do prompt engineering.
+- The inputs and params tabs allow users to edit (these corresponds to the inputs and params in the config file).
+- The outputs panel holds the UI elements to show the outputs defined in config file.
+- The Run button: will execute pipeline with the supplied inputs and params, and render result in the outputs panel.
+- The Export button: will export the logs of all the run to an Excel files users to inspect for best set of params.
+
+#### Step 6 - Export to Excel
+
+Upon clicking export, the users can download Excel file.
+
+### Chat pipeline
+
+Chat pipeline workflow is different from simple pipeline workflow. In simple pipeline, each Run creates a set of output, input and params for users to compare. In chat pipeline, each Run is not a one-off run, but a long interactive session. Hence, the workflow is as follow:
+
+1. Set the desired parameters.
+2. Click "New chat" to start a chat session with the supplied parameters. This set of parameters will persist until the end of the chat session. During an ongoing chat session, changing the parameters will not take any effect.
+3. Chat and interact with the chat bot on the right panel. You can add any additional input (if any), and they will be supplied to the chatbot.
+4. During chat, the log of the chat will show up in the "Output" tabs. This is empty by default, so if you want to show the log here, tell the AI developers to configure the UI settings.
+5. When finishing chat, select your preference in the radio box. Click "End chat". This will save the chat log and the preference to disk.
+6. To compare the result of different run, click "Export" to get an Excel spreadsheet summary of different run.
diff --git a/docs/upload-package.md b/docs/upload-package.md
new file mode 100644
index 0000000..14926a7
--- /dev/null
+++ b/docs/upload-package.md
@@ -0,0 +1,24 @@
+Devpi server endpoint (subjected to change): https://ian_devpi.promptui.dm.cinnamon.is/root/packages
+
+Install devpi-client
+
+```bash
+pip install devpi-client
+```
+
+Login to the server
+
+```bash
+devpi use # set server endpoint provided above
+devpi login --password= # login
+```
+
+If you don't yet have an account, please contact Ian or John.
+
+Upload your package
+
+```bash
+devpi use \dev # choose the index to upload your package
+cd
+devpi upload
+```
diff --git a/knowledgehub/agents/langchain_based.py b/knowledgehub/agents/langchain_based.py
index 9138a17..3083b70 100644
--- a/knowledgehub/agents/langchain_based.py
+++ b/knowledgehub/agents/langchain_based.py
@@ -67,7 +67,7 @@ class LangchainAgent(BaseAgent):
def run(self, instruction: str) -> AgentOutput:
assert (
self.agent is not None
- ), "Lanchain AgentExecutor is not correclty initialized"
+ ), "Lanchain AgentExecutor is not correctly initialized"
# Langchain AgentExecutor call
output = self.agent(instruction)["output"]
diff --git a/knowledgehub/base/component.py b/knowledgehub/base/component.py
index bf1851f..b62658c 100644
--- a/knowledgehub/base/component.py
+++ b/knowledgehub/base/component.py
@@ -6,16 +6,16 @@ from kotaemon.base.schema import Document
class BaseComponent(Function):
- """A component is a class that can be used to compose a pipeline
+ """A component is a class that can be used to compose a pipeline.
- Benefits of component:
+ !!! tip "Benefits of component"
- Auto caching, logging
- Allow deployment
- For each component, the spirit is:
+ !!! tip "For each component, the spirit is"
- Tolerate multiple input types, e.g. str, Document, List[str], List[Document]
- Enforce single output type. Hence, the output type of a component should be
- as generic as possible.
+ as generic as possible.
"""
inflow = None
diff --git a/knowledgehub/base/schema.py b/knowledgehub/base/schema.py
index cfb5791..ee19d3a 100644
--- a/knowledgehub/base/schema.py
+++ b/knowledgehub/base/schema.py
@@ -22,6 +22,9 @@ class Document(BaseDocument):
This class accept one positional argument `content` of an arbitrary type, which will
store the raw content of the document. If specified, the class will use
`content` to initialize the base llama_index class.
+
+ Args:
+ content: the raw content of the document.
"""
content: Any
@@ -99,7 +102,7 @@ class RetrievedDocument(Document):
"""Subclass of Document with retrieval-related information
Attributes:
- score (float): score of the document (from 0.0 to 1.0)
+ score (float): score of the document (from 0.0 to 1.0)
retrieval_metadata (dict): metadata from the retrieval process, can be used
by different components in a retrieved pipeline to communicate with each
other
diff --git a/knowledgehub/llms/__init__.py b/knowledgehub/llms/__init__.py
index 521f596..e52474a 100644
--- a/knowledgehub/llms/__init__.py
+++ b/knowledgehub/llms/__init__.py
@@ -4,6 +4,7 @@ from .base import BaseLLM
from .branching import GatedBranchingPipeline, SimpleBranchingPipeline
from .chats import AzureChatOpenAI, ChatLLM
from .completions import LLM, AzureOpenAI, OpenAI
+from .cot import ManualSequentialChainOfThought, Thought
from .linear import GatedLinearPipeline, SimpleLinearPipeline
from .prompts import BasePromptComponent, PromptTemplate
@@ -28,4 +29,7 @@ __all__ = [
"GatedLinearPipeline",
"SimpleBranchingPipeline",
"GatedBranchingPipeline",
+ # chain-of-thoughts
+ "ManualSequentialChainOfThought",
+ "Thought",
]
diff --git a/knowledgehub/llms/branching.py b/knowledgehub/llms/branching.py
index 8f93caf..a9cbbe8 100644
--- a/knowledgehub/llms/branching.py
+++ b/knowledgehub/llms/branching.py
@@ -12,7 +12,8 @@ class SimpleBranchingPipeline(BaseComponent):
Attributes:
branches (List[BaseComponent]): The list of branches to be executed.
- Example Usage:
+ Example:
+ ```python
from kotaemon.llms import (
AzureChatOpenAI,
BasePromptComponent,
@@ -45,6 +46,7 @@ class SimpleBranchingPipeline(BaseComponent):
print(pipeline(condition_text="1"))
print(pipeline(condition_text="2"))
print(pipeline(condition_text="12"))
+ ```
"""
branches: List[BaseComponent] = Param(default_callback=lambda *_: [])
@@ -87,7 +89,8 @@ class GatedBranchingPipeline(SimpleBranchingPipeline):
Attributes:
branches (List[BaseComponent]): The list of branches to be executed.
- Example Usage:
+ Example:
+ ```python
from kotaemon.llms import (
AzureChatOpenAI,
BasePromptComponent,
@@ -119,6 +122,7 @@ class GatedBranchingPipeline(SimpleBranchingPipeline):
)
print(pipeline(condition_text="1"))
print(pipeline(condition_text="2"))
+ ```
"""
def run(self, *, condition_text: Optional[str] = None, **prompt_kwargs):
@@ -135,7 +139,7 @@ class GatedBranchingPipeline(SimpleBranchingPipeline):
Union[OutputType, None]: The output of the first branch that satisfies the
condition, or None if no branch satisfies the condition.
- Raise:
+ Raises:
ValueError: If condition_text is None
"""
if condition_text is None:
diff --git a/knowledgehub/llms/cot.py b/knowledgehub/llms/cot.py
index 5786f41..6721dc3 100644
--- a/knowledgehub/llms/cot.py
+++ b/knowledgehub/llms/cot.py
@@ -1,7 +1,9 @@
from copy import deepcopy
from typing import Callable, List
-from kotaemon.base import BaseComponent, Document, Node, Param
+from theflow import Function, Node, Param
+
+from kotaemon.base import BaseComponent, Document
from .chats import AzureChatOpenAI
from .completions import LLM
@@ -66,13 +68,13 @@ class Thought(BaseComponent):
prompt: str = Param(
help=(
- "The prompt template string. This prompt template has Python-like "
- "variable placeholders, that then will be subsituted with real values when "
- "this component is executed"
+ "The prompt template string. This prompt template has Python-like variable"
+ " placeholders, that then will be substituted with real values when this"
+ " component is executed"
)
)
llm: LLM = Node(AzureChatOpenAI, help="The LLM model to execute the input prompt")
- post_process: BaseComponent = Node(
+ post_process: Function = Node(
help=(
"The function post-processor that post-processes LLM output prediction ."
"It should take a string as input (this is the LLM output text) and return "
@@ -83,7 +85,7 @@ class Thought(BaseComponent):
@Node.auto(depends_on="prompt")
def prompt_template(self):
"""Automatically wrap around param prompt. Can ignore"""
- return BasePromptComponent(template=self.prompt)
+ return BasePromptComponent(self.prompt)
def run(self, **kwargs) -> Document:
"""Run the chain of thought"""
@@ -113,20 +115,19 @@ class ManualSequentialChainOfThought(BaseComponent):
**Create and run a chain of thought without "+" operator:**
- ```python
- >> from kotaemon.pipelines.cot import Thought, ManualSequentialChainOfThought
-
- >> llm = AzureChatOpenAI(...)
- >> thought1 = Thought(
- prompt="Word {word} in {language} is ",
- post_process=lambda string: {"translated": string},
- )
- >> thought2 = Thought(
- prompt="Translate {translated} to Japanese",
- post_process=lambda string: {"output": string},
- )
- >> thought = ManualSequentialChainOfThought(thoughts=[thought1, thought2], llm=llm)
- >> thought(word="hello", language="French")
+ ```pycon
+ >>> from kotaemon.pipelines.cot import Thought, ManualSequentialChainOfThought
+ >>> llm = AzureChatOpenAI(...)
+ >>> thought1 = Thought(
+ >>> prompt="Word {word} in {language} is ",
+ >>> post_process=lambda string: {"translated": string},
+ >>> )
+ >>> thought2 = Thought(
+ >>> prompt="Translate {translated} to Japanese",
+ >>> post_process=lambda string: {"output": string},
+ >>> )
+ >>> thought = ManualSequentialChainOfThought(thoughts=[thought1, thought2], llm=llm)
+ >>> thought(word="hello", language="French")
{'word': 'hello',
'language': 'French',
'translated': '"Bonjour"',
diff --git a/knowledgehub/llms/linear.py b/knowledgehub/llms/linear.py
index 4fd95e4..ac8605a 100644
--- a/knowledgehub/llms/linear.py
+++ b/knowledgehub/llms/linear.py
@@ -21,6 +21,7 @@ class SimpleLinearPipeline(BaseComponent):
post-processor component or function.
Example Usage:
+ ```python
from kotaemon.llms import AzureChatOpenAI, BasePromptComponent
def identity(x):
@@ -41,6 +42,7 @@ class SimpleLinearPipeline(BaseComponent):
post_processor=identity,
)
print(pipeline(word="lone"))
+ ```
"""
prompt: BasePromptComponent
@@ -85,7 +87,8 @@ class GatedLinearPipeline(SimpleLinearPipeline):
condition (Callable[[IO_Type], Any]): A callable function that represents the
condition.
- Example Usage:
+ Usage:
+ ```{.py3 title="Example Usage"}
from kotaemon.llms import AzureChatOpenAI, BasePromptComponent
from kotaemon.parsers import RegexExtractor
@@ -109,6 +112,7 @@ class GatedLinearPipeline(SimpleLinearPipeline):
)
print(pipeline(condition_text="some pattern", word="lone"))
print(pipeline(condition_text="other pattern", word="lone"))
+ ```
"""
condition: Callable[[IO_Type], Any]
diff --git a/knowledgehub/llms/prompts/template.py b/knowledgehub/llms/prompts/template.py
index 46692ec..aa758be 100644
--- a/knowledgehub/llms/prompts/template.py
+++ b/knowledgehub/llms/prompts/template.py
@@ -72,7 +72,7 @@ class PromptTemplate:
UserWarning,
)
- def populate(self, **kwargs):
+ def populate(self, **kwargs) -> str:
"""
Strictly populate the template with the given keyword arguments.
@@ -81,7 +81,7 @@ class PromptTemplate:
Each keyword corresponds to a placeholder in the template.
Returns:
- str: The populated template.
+ The populated template.
Raises:
ValueError: If an unknown placeholder is provided.
diff --git a/knowledgehub/loaders/base.py b/knowledgehub/loaders/base.py
index f74cc45..cb92d5b 100644
--- a/knowledgehub/loaders/base.py
+++ b/knowledgehub/loaders/base.py
@@ -4,7 +4,7 @@ from typing import Any, List, Type, Union
from llama_index import SimpleDirectoryReader, download_loader
from llama_index.readers.base import BaseReader
-from ..base import BaseComponent, Document
+from kotaemon.base import BaseComponent, Document
class AutoReader(BaseComponent):
diff --git a/knowledgehub/loaders/utils/box.py b/knowledgehub/loaders/utils/box.py
index 8b7726b..d3ff6c0 100644
--- a/knowledgehub/loaders/utils/box.py
+++ b/knowledgehub/loaders/utils/box.py
@@ -93,7 +93,7 @@ def get_rect_iou(gt_box: List[tuple], pd_box: List[tuple], iou_type=0) -> int:
# compute the intersection over union by taking the intersection
# area and dividing it by the sum of prediction + ground-truth
- # areas - the interesection area
+ # areas - the intersection area
if iou_type == 0:
iou = interArea / float(gt_area + pd_area - interArea)
elif iou_type == 1:
diff --git a/knowledgehub/loaders/utils/pdf_ocr.py b/knowledgehub/loaders/utils/pdf_ocr.py
index b978828..197e021 100644
--- a/knowledgehub/loaders/utils/pdf_ocr.py
+++ b/knowledgehub/loaders/utils/pdf_ocr.py
@@ -34,8 +34,7 @@ def read_pdf_unstructured(input_path: Union[Path, str]):
from unstructured.partition.auto import partition
except ImportError:
raise ImportError(
- "Please install unstructured PDF reader \
- `pip install unstructured[pdf]`"
+ "Please install unstructured PDF reader `pip install unstructured[pdf]`"
)
page_items = defaultdict(list)
@@ -60,7 +59,7 @@ def read_pdf_unstructured(input_path: Union[Path, str]):
def merge_ocr_and_pdf_texts(
ocr_list: List[dict], pdf_text_list: List[dict], debug_info=None
):
- """Merge PDF and OCR text using IOU overlaping location
+ """Merge PDF and OCR text using IOU overlapping location
Args:
ocr_list: List of OCR items {"text", "box", "location"}
pdf_text_list: List of PDF items {"text", "box", "location"}
@@ -115,7 +114,7 @@ def merge_ocr_and_pdf_texts(
def merge_table_cell_and_ocr(
table_list: List[dict], ocr_list: List[dict], pdf_list: List[dict], debug_info=None
):
- """Merge table items with OCR text using IOU overlaping location
+ """Merge table items with OCR text using IOU overlapping location
Args:
table_list: List of table items
"type": ("table", "cell", "text"), "text", "box", "location"}
@@ -123,7 +122,7 @@ def merge_table_cell_and_ocr(
pdf_list: List of PDF items {"text", "box", "location"}
Returns:
- all_table_cells: List of tables, each of table is reprented
+ all_table_cells: List of tables, each of table is represented
by list of cells with combined text from OCR
not_matched_items: List of PDF text which is not overlapped by table region
"""
diff --git a/knowledgehub/parsers/regex_extractor.py b/knowledgehub/parsers/regex_extractor.py
index 90ad365..f773fe9 100644
--- a/knowledgehub/parsers/regex_extractor.py
+++ b/knowledgehub/parsers/regex_extractor.py
@@ -100,11 +100,14 @@ class RegexExtractor(BaseComponent):
A list contains the output ExtractorOutput for each input
Example:
- document1 = Document(...)
- document2 = Document(...)
- document_batch = [document1, document2]
- batch_output = self(document_batch)
- # batch_output will be [output1_document1, output1_document2]
+ ```pycon
+ >>> document1 = Document(...)
+ >>> document2 = Document(...)
+ >>> document_batch = [document1, document2]
+ >>> batch_output = self(document_batch)
+ >>> print(batch_output)
+ [output1_document1, output1_document2]
+ ```
"""
# TODO: this conversion seems common
input_: list[str] = []
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..14cd154
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,105 @@
+repo_name: Cinnamon/kotaemon
+repo_url: https://github.com/Cinnamon/kotaemon
+site_name: kotaemon Docs
+edit_uri: edit/main/docs/
+
+nav:
+ - Getting Started:
+ - Quick Start: index.md
+ - Overview: overview.md
+ - Contributing: contributing.md
+ - Tutorial:
+ - Data & Data Structure Components: data-components.md
+ - Creating a Component: create-a-component.md
+ - Utilities: ultilities.md
+ # generated using gen-files + literate-nav
+ - API Reference: reference/
+ - Use Cases: examples/
+ - Misc:
+ - Upload python package to private index: upload-package.md
+
+markdown_extensions:
+ - admonition
+ - pymdownx.highlight:
+ use_pygments: true
+ anchor_linenums: true
+ line_spans: __span
+ linenums: true
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - pymdownx.details
+ - pymdownx.extra
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - toc:
+ permalink: true
+ title: Page contents
+
+plugins:
+ - search
+ - gen-files:
+ scripts:
+ - docs/scripts/generate_reference_docs.py
+ - docs/scripts/generate_examples_docs.py
+ - literate-nav:
+ nav_file: NAV.md
+ - mkdocstrings:
+ handlers:
+ python:
+ options:
+ docstring_options:
+ ignore_init_summary: false
+ filters:
+ - "!^_"
+ members_order: source
+ separate_signature: true
+ paths: [kotaemon]
+ - git-revision-date-localized:
+ enable_creation_date: true
+ type: timeago
+ fallback_to_build_date: true
+ - section-index
+
+theme:
+ features:
+ - content.action.edit
+ - content.tabs.link
+ - content.code.annotate
+ - content.code.annotations
+ - content.code.copy
+ - navigation.tabs
+ - navigation.top
+ - navigation.instant
+ - navigation.indexes
+ - toc.follow
+ - search.share
+ - search.highlight
+ - search.suggest
+ name: material
+ custom_dir: docs/theme
+ palette:
+ scheme: dracula
+ primary: deep purple
+ accent: deep purple
+ icon:
+ repo: fontawesome/brands/github
+ edit: material/pencil
+ view: material/eye
+
+extra_css:
+ - extra/css/code_select.css
+ - assets/pymdownx-extras/extra-fb5a2a1c86.css
+
+extra_javascript:
+ - assets/pymdownx-extras/extra-loader-MCFnu0Wd.js
+
+validation:
+ absolute_links: warn
+ omitted_files: warn
+ unrecognized_links: warn
diff --git a/pyproject.toml b/pyproject.toml
index 1514e08..67a2265 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,3 +70,10 @@ kh = "kotaemon.cli:main"
Homepage = "https://github.com/Cinnamon/kotaemon/"
Repository = "https://github.com/Cinnamon/kotaemon/"
Documentation = "https://github.com/Cinnamon/kotaemon/wiki"
+
+[tool.codespell]
+skip = "*.js,*.css,*.map"
+# `llm` abbreviation for large language models
+ignore-words-list = "llm,fo"
+quiet-level = 3
+check-filenames = ""
diff --git a/tests/test_vectorstore.py b/tests/test_vectorstore.py
index e086d0e..31b34f7 100644
--- a/tests/test_vectorstore.py
+++ b/tests/test_vectorstore.py
@@ -34,7 +34,7 @@ class TestChromaVectorStore:
]
assert db._collection.count() == 0, "Expected empty collection"
output = db.add(documents)
- assert len(output) == 2, "Expected outputing 2 ids"
+ assert len(output) == 2, "Expected outputting 2 ids"
assert db._collection.count() == 2, "Expected 2 added entries"
def test_delete(self, tmp_path):