+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/assets/app.Cq8-3sYi.js b/previews/PR116/assets/app.Cq8-3sYi.js
new file mode 100644
index 000000000..6ea4e722b
--- /dev/null
+++ b/previews/PR116/assets/app.Cq8-3sYi.js
@@ -0,0 +1,116 @@
+import { j as inBrowser, a8 as useUpdateHead, a9 as RouterSymbol, aa as initData, ab as dataSymbol, ac as Content, ad as ClientOnly, ae as siteDataRef, af as createSSRApp, ag as createRouter, ah as pathToFile, Y as __vitePreload, d as defineComponent, u as useData, l as onMounted, z as watchEffect, ai as usePrefetch, aj as useCopyCode, ak as useCodeGroups, a6 as h } from "./chunks/framework.DNj2-0qu.js";
+import { R as RawTheme } from "./chunks/theme.TGBeONaB.js";
+function resolveThemeExtends(theme) {
+ if (theme.extends) {
+ const base = resolveThemeExtends(theme.extends);
+ return {
+ ...base,
+ ...theme,
+ async enhanceApp(ctx) {
+ if (base.enhanceApp)
+ await base.enhanceApp(ctx);
+ if (theme.enhanceApp)
+ await theme.enhanceApp(ctx);
+ }
+ };
+ }
+ return theme;
+}
+const Theme = resolveThemeExtends(RawTheme);
+const VitePressApp = defineComponent({
+ name: "VitePressApp",
+ setup() {
+ const { site, lang, dir } = useData();
+ onMounted(() => {
+ watchEffect(() => {
+ document.documentElement.lang = lang.value;
+ document.documentElement.dir = dir.value;
+ });
+ });
+ if (site.value.router.prefetchLinks) {
+ usePrefetch();
+ }
+ useCopyCode();
+ useCodeGroups();
+ if (Theme.setup)
+ Theme.setup();
+ return () => h(Theme.Layout);
+ }
+});
+async function createApp() {
+ const router = newRouter();
+ const app = newApp();
+ app.provide(RouterSymbol, router);
+ const data = initData(router.route);
+ app.provide(dataSymbol, data);
+ app.component("Content", Content);
+ app.component("ClientOnly", ClientOnly);
+ Object.defineProperties(app.config.globalProperties, {
+ $frontmatter: {
+ get() {
+ return data.frontmatter.value;
+ }
+ },
+ $params: {
+ get() {
+ return data.page.value.params;
+ }
+ }
+ });
+ if (Theme.enhanceApp) {
+ await Theme.enhanceApp({
+ app,
+ router,
+ siteData: siteDataRef
+ });
+ }
+ return { app, router, data };
+}
+function newApp() {
+ return createSSRApp(VitePressApp);
+}
+function newRouter() {
+ let isInitialPageLoad = inBrowser;
+ let initialPath;
+ return createRouter((path) => {
+ let pageFilePath = pathToFile(path);
+ let pageModule = null;
+ if (pageFilePath) {
+ if (isInitialPageLoad) {
+ initialPath = pageFilePath;
+ }
+ if (isInitialPageLoad || initialPath === pageFilePath) {
+ pageFilePath = pageFilePath.replace(/\.js$/, ".lean.js");
+ }
+ if (false)
+ ;
+ else {
+ pageModule = __vitePreload(() => import(
+ /*@vite-ignore*/
+ pageFilePath
+ ), true ? __vite__mapDeps([]) : void 0);
+ }
+ }
+ if (inBrowser) {
+ isInitialPageLoad = false;
+ }
+ return pageModule;
+ }, Theme.NotFound);
+}
+if (inBrowser) {
+ createApp().then(({ app, router, data }) => {
+ router.go().then(() => {
+ useUpdateHead(router.route, data.site);
+ app.mount("#app");
+ });
+ });
+}
+export {
+ createApp
+};
+function __vite__mapDeps(indexes) {
+ if (!__vite__mapDeps.viteFileDeps) {
+ __vite__mapDeps.viteFileDeps = []
+ }
+ return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
+}
diff --git a/previews/PR116/assets/chunks/@localSearchIndexroot.CMA9uIl1.js b/previews/PR116/assets/chunks/@localSearchIndexroot.CMA9uIl1.js
new file mode 100644
index 000000000..d162414bd
--- /dev/null
+++ b/previews/PR116/assets/chunks/@localSearchIndexroot.CMA9uIl1.js
@@ -0,0 +1,4 @@
+const _localSearchIndexroot = '{"documentCount":161,"nextId":161,"documentIds":{"0":"/PromptingTools.jl/previews/PR116/examples/building_RAG#Building-a-Simple-Retrieval-Augmented-Generation-(RAG)-System-with-RAGTools","1":"/PromptingTools.jl/previews/PR116/examples/building_RAG#RAG-in-Two-Lines","2":"/PromptingTools.jl/previews/PR116/examples/building_RAG#Evaluations","3":"/PromptingTools.jl/previews/PR116/examples/building_RAG#Generate-Q-and-A-pairs","4":"/PromptingTools.jl/previews/PR116/examples/building_RAG#Explore-one-Q-and-A-pair","5":"/PromptingTools.jl/previews/PR116/examples/building_RAG#Evaluate-this-Q-and-A-pair","6":"/PromptingTools.jl/previews/PR116/examples/building_RAG#Evaluate-the-Whole-Set","7":"/PromptingTools.jl/previews/PR116/examples/building_RAG#What-would-we-do-next?","8":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Various-Examples","9":"/PromptingTools.jl/previews/PR116/examples/readme_examples#ai*-Functions-Overview","10":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Seamless-Integration-Into-Your-Workflow","11":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Advanced-Prompts-/-Conversations","12":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Templated-Prompts","13":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Asynchronous-Execution","14":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Model-Aliases","15":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Embeddings","16":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Classification","17":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Routing-to-Defined-Categories","18":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Data-Extraction","19":"/PromptingTools.jl/previews/PR116/examples/readme_examples#OCR-and-Image-Comprehension","20":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Experimental-Agent-Workflows-/-Output-Validation-with-airetry!","21":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Using-Ollama-models","22":"/PromptingTools.jl/previews/PR116/examples/readme_examples#Using-MistralAI-API-and-other-OpenAI-compatible-APIs","23":"/PromptingTools.jl/previews/PR116/examples/working_with_aitemplates#Using-AITemplates","24":"/PromptingTools.jl/previews/PR116/examples/working_with_custom_apis#Custom-APIs","25":"/PromptingTools.jl/previews/PR116/examples/working_with_custom_apis#Using-MistralAI","26":"/PromptingTools.jl/previews/PR116/examples/working_with_custom_apis#Using-other-OpenAI-compatible-APIs","27":"/PromptingTools.jl/previews/PR116/examples/working_with_custom_apis#Using-llama.cpp-server","28":"/PromptingTools.jl/previews/PR116/examples/working_with_custom_apis#Using-Databricks-Foundation-Models","29":"/PromptingTools.jl/previews/PR116/examples/working_with_custom_apis#Using-Together.ai","30":"/PromptingTools.jl/previews/PR116/examples/working_with_custom_apis#Using-Fireworks.ai","31":"/PromptingTools.jl/previews/PR116/examples/working_with_google_ai_studio#Working-with-Google-AI-Studio","32":"/PromptingTools.jl/previews/PR116/examples/working_with_google_ai_studio#Text-Generation-with-aigenerate","33":"/PromptingTools.jl/previews/PR116/examples/working_with_google_ai_studio#Simple-message","34":"/PromptingTools.jl/previews/PR116/examples/working_with_google_ai_studio#Advanced-Prompts","35":"/PromptingTools.jl/previews/PR116/examples/working_with_google_ai_studio#Gotchas","36":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Local-models-with-Ollama.ai","37":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Text-Generation-with-aigenerate","38":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Simple-message","39":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Standard-string-interpolation","40":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Advanced-Prompts","41":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Schema-Changes-/-Custom-models","42":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Providing-Images-with-aiscan","43":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Embeddings-with-aiembed","44":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Simple-embedding-for-one-document","45":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Multiple-documents-embedding","46":"/PromptingTools.jl/previews/PR116/examples/working_with_ollama#Using-postprocessing-function","47":"/PromptingTools.jl/previews/PR116/extra_tools/agent_tools_intro#Agent-Tools-Introduction","48":"/PromptingTools.jl/previews/PR116/extra_tools/agent_tools_intro#Highlights","49":"/PromptingTools.jl/previews/PR116/extra_tools/agent_tools_intro#Examples","50":"/PromptingTools.jl/previews/PR116/extra_tools/agent_tools_intro#Automatic-Fixing-of-AI-Calls","51":"/PromptingTools.jl/previews/PR116/extra_tools/agent_tools_intro#References","52":"/PromptingTools.jl/previews/PR116/extra_tools/api_tools_intro#APITools-Introduction","53":"/PromptingTools.jl/previews/PR116/extra_tools/api_tools_intro#Highlights","54":"/PromptingTools.jl/previews/PR116/extra_tools/api_tools_intro#References","55":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#RAG-Tools-Introduction","56":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#Highlights","57":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#Examples","58":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#RAG-Interface","59":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#System-Overview","60":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#RAG-Diagram","61":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#Passing-Keyword-Arguments","62":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#Deepdive","63":"/PromptingTools.jl/previews/PR116/extra_tools/rag_tools_intro#References","64":"/PromptingTools.jl/previews/PR116/extra_tools/text_utilities_intro#Text-Utilities","65":"/PromptingTools.jl/previews/PR116/extra_tools/text_utilities_intro#Highlights","66":"/PromptingTools.jl/previews/PR116/extra_tools/text_utilities_intro#References","67":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Frequently-Asked-Questions","68":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Why-OpenAI","69":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#What-if-I-cannot-access-OpenAI?","70":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Data-Privacy-and-OpenAI","71":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Creating-OpenAI-API-Key","72":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#getting-an-error-argumenterror-api-key-cannot-be-empty-despite-having-set-openai-api-key-getting-an-error-argumenterror-apikey-cannot-be-empty-despite-having-set-openaiapi-key","73":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Getting-an-error-"Rate-limit-exceeded"-from-OpenAI?","74":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Setting-OpenAI-Spending-Limits","75":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#How-much-does-it-cost?-Is-it-worth-paying-for?","76":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Configuring-the-Environment-Variable-for-API-Key","77":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Setting-the-API-Key-via-Preferences.jl","78":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Understanding-the-API-Keyword-Arguments-in-aigenerate-(api_kwargs)","79":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Instant-Access-from-Anywhere","80":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Open-Source-Alternatives","81":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Setup-Guide-for-Ollama","82":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Changing-the-Default-Model-or-Schema","83":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#How-to-have-Multi-turn-Conversations?","84":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#How-to-have-typed-responses?","85":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#How-to-quickly-create-a-prompt-template?","86":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#Do-we-have-a-RecursiveCharacterTextSplitter-like-Langchain?","87":"/PromptingTools.jl/previews/PR116/frequently_asked_questions#How-would-I-fine-tune-a-model?","88":"/PromptingTools.jl/previews/PR116/getting_started#Getting-Started","89":"/PromptingTools.jl/previews/PR116/getting_started#Prerequisites","90":"/PromptingTools.jl/previews/PR116/getting_started#Installation","91":"/PromptingTools.jl/previews/PR116/getting_started#Quick-Start-with-@ai_str","92":"/PromptingTools.jl/previews/PR116/getting_started#Using-aigenerate-with-placeholders","93":"/PromptingTools.jl/previews/PR116/how_it_works#How-It-Works","94":"/PromptingTools.jl/previews/PR116/how_it_works#Key-Concepts","95":"/PromptingTools.jl/previews/PR116/how_it_works#API/Model-Providers","96":"/PromptingTools.jl/previews/PR116/how_it_works#Schemas","97":"/PromptingTools.jl/previews/PR116/how_it_works#Prompts","98":"/PromptingTools.jl/previews/PR116/how_it_works#Messages","99":"/PromptingTools.jl/previews/PR116/how_it_works#Prompt-Templates","100":"/PromptingTools.jl/previews/PR116/how_it_works#ai*-Functions-Overview","101":"/PromptingTools.jl/previews/PR116/how_it_works#Walkthrough-Example-for-aigenerate","102":"/PromptingTools.jl/previews/PR116/how_it_works#Walkthrough-Example-for-aiextract","103":"/PromptingTools.jl/previews/PR116/prompts/RAG#Basic-Rag-Templates","104":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGAnswerFromContext","105":"/PromptingTools.jl/previews/PR116/prompts/RAG#Metadata-Templates","106":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGExtractMetadataLong","107":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGExtractMetadataShort","108":"/PromptingTools.jl/previews/PR116/prompts/RAG#Refinement-Templates","109":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGAnswerRefiner","110":"/PromptingTools.jl/previews/PR116/prompts/RAG#Evaluation-Templates","111":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGCreateQAFromContext","112":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGJudgeAnswerFromContext","113":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGJudgeAnswerFromContextShort","114":"/PromptingTools.jl/previews/PR116/prompts/RAG#Query-Transformations-Templates","115":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGJuliaQueryHyDE","116":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGQueryHyDE","117":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGQueryOptimizer","118":"/PromptingTools.jl/previews/PR116/prompts/RAG#Template:-RAGQuerySimplifier","119":"/PromptingTools.jl/previews/PR116/prompts/agents#Code-Fixing-Templates","120":"/PromptingTools.jl/previews/PR116/prompts/agents#Template:-CodeFixerRCI","121":"/PromptingTools.jl/previews/PR116/prompts/agents#Template:-CodeFixerShort","122":"/PromptingTools.jl/previews/PR116/prompts/agents#Template:-CodeFixerTiny","123":"/PromptingTools.jl/previews/PR116/prompts/agents#Feedback-Templates","124":"/PromptingTools.jl/previews/PR116/prompts/agents#Template:-FeedbackFromEvaluator","125":"/PromptingTools.jl/previews/PR116/prompts/classification#Classification-Templates","126":"/PromptingTools.jl/previews/PR116/prompts/classification#Template:-InputClassifier","127":"/PromptingTools.jl/previews/PR116/prompts/classification#Template:-JudgeIsItTrue","128":"/PromptingTools.jl/previews/PR116/prompts/extraction#Xml-Formatted-Templates","129":"/PromptingTools.jl/previews/PR116/prompts/extraction#Template:-ExtractDataXML","130":"/PromptingTools.jl/previews/PR116/prompts/extraction#Extraction-Templates","131":"/PromptingTools.jl/previews/PR116/prompts/extraction#Template:-ExtractData","132":"/PromptingTools.jl/previews/PR116/prompts/general#General-Templates","133":"/PromptingTools.jl/previews/PR116/prompts/general#Template:-BlankSystemUser","134":"/PromptingTools.jl/previews/PR116/prompts/general#Template:-PromptEngineerForTask","135":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Persona-Task-Templates","136":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-AnalystChaptersInTranscript","137":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-AnalystDecisionsInTranscript","138":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-AnalystThemesInResponses","139":"/PromptingTools.jl/previews/PR116/prompts/persona-task#theme-1-theme-description","140":"/PromptingTools.jl/previews/PR116/prompts/persona-task#theme-2-theme-description","141":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-AssistantAsk","142":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-DetailOrientedTask","143":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-DrafterEmailBrief","144":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaExpertAsk","145":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaExpertCoTTask","146":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaExpertTestCode","147":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaRecapCoTTask","148":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaRecapTask","149":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-StorytellerExplainSHAP","150":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Xml-Formatted-Templates","151":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaExpertAskXML","152":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaExpertCoTTaskXML","153":"/PromptingTools.jl/previews/PR116/prompts/persona-task#Template:-JuliaExpertTestCodeXML","154":"/PromptingTools.jl/previews/PR116/prompts/visual#Visual-Templates","155":"/PromptingTools.jl/previews/PR116/prompts/visual#Template:-OCRTask","156":"/PromptingTools.jl/previews/PR116/reference#Reference","157":"/PromptingTools.jl/previews/PR116/reference_agenttools#Reference-for-AgentTools","158":"/PromptingTools.jl/previews/PR116/reference_apitools#Reference-for-APITools","159":"/PromptingTools.jl/previews/PR116/reference_experimental#Reference-for-Experimental-Module","160":"/PromptingTools.jl/previews/PR116/reference_ragtools#Reference-for-RAGTools"},"fieldIds":{"title":0,"titles":1,"text":2},"fieldLength":{"0":[10,1,71],"1":[4,10,197],"2":[1,1,40],"3":[5,1,74],"4":[6,1,68],"5":[6,1,180],"6":[4,1,367],"7":[6,1,111],"8":[2,1,1],"9":[3,2,306],"10":[5,2,128],"11":[3,2,152],"12":[2,2,161],"13":[2,2,40],"14":[2,2,97],"15":[1,2,65],"16":[1,2,97],"17":[4,3,81],"18":[2,2,158],"19":[4,2,103],"20":[8,2,274],"21":[3,2,108],"22":[8,2,195],"23":[2,1,322],"24":[2,1,27],"25":[2,2,145],"26":[5,2,87],"27":[4,2,118],"28":[4,2,85],"29":[3,2,109],"30":[3,2,162],"31":[5,1,83],"32":[4,5,14],"33":[2,8,55],"34":[2,8,90],"35":[1,8,59],"36":[5,1,147],"37":[4,5,1],"38":[2,8,51],"39":[3,8,35],"40":[2,8,122],"41":[4,8,134],"42":[4,5,40],"43":[3,5,1],"44":[5,7,42],"45":[3,7,53],"46":[3,7,61],"47":[3,1,37],"48":[1,3,185],"49":[1,3,1],"50":[5,4,227],"51":[1,3,915],"52":[2,1,23],"53":[1,2,41],"54":[1,2,87],"55":[3,1,98],"56":[1,3,91],"57":[1,3,375],"58":[2,3,1],"59":[2,4,195],"60":[2,4,79],"61":[3,5,105],"62":[1,4,165],"63":[1,3,739],"64":[2,1,28],"65":[1,2,126],"66":[1,2,513],"67":[3,1,1],"68":[2,3,54],"69":[7,5,36],"70":[4,3,65],"71":[4,3,54],"72":[19,3,120],"73":[10,3,151],"74":[4,3,57],"75":[10,3,99],"76":[7,3,97],"77":[7,3,41],"78":[10,3,8],"79":[4,3,47],"80":[3,3,31],"81":[4,3,108],"82":[6,3,83],"83":[7,3,120],"84":[6,3,256],"85":[8,3,193],"86":[8,3,70],"87":[8,3,87],"88":[2,1,1],"89":[1,2,98],"90":[1,2,37],"91":[5,2,112],"92":[4,2,101],"93":[3,1,49],"94":[2,3,91],"95":[3,5,56],"96":[1,5,77],"97":[1,5,61],"98":[1,5,77],"99":[2,5,139],"100":[3,5,308],"101":[4,3,203],"102":[4,3,451],"103":[3,1,1],"104":[2,4,61],"105":[2,1,1],"106":[2,3,160],"107":[2,3,66],"108":[2,1,1],"109":[2,3,101],"110":[2,1,1],"111":[2,3,140],"112":[2,3,116],"113":[2,3,63],"114":[3,1,1],"115":[2,4,85],"116":[2,4,83],"117":[2,4,85],"118":[2,4,65],"119":[3,1,1],"120":[2,4,236],"121":[2,4,126],"122":[2,4,60],"123":[2,1,1],"124":[2,3,23],"125":[2,1,1],"126":[2,3,73],"127":[2,3,41],"128":[3,1,1],"129":[2,4,83],"130":[2,1,1],"131":[2,3,74],"132":[2,1,1],"133":[2,3,35],"134":[2,3,71],"135":[3,1,1],"136":[2,4,198],"137":[2,4,207],"138":[2,4,124],"139":[4,1,5],"140":[4,1,36],"141":[2,4,47],"142":[2,4,46],"143":[2,4,139],"144":[2,4,51],"145":[2,4,85],"146":[2,4,171],"147":[2,4,168],"148":[2,4,174],"149":[2,4,175],"150":[3,4,1],"151":[2,6,60],"152":[2,6,96],"153":[2,6,181],"154":[2,1,1],"155":[2,3,51],"156":[1,1,2357],"157":[3,1,1182],"158":[3,1,101],"159":[4,1,68],"160":[3,1,1365]},"averageFieldLength":[3.1801242236024843,3.0496894409937894,135.00000000000006],"storedFields":{"0":{"title":"Building a Simple Retrieval-Augmented Generation (RAG) System with RAGTools","titles":[]},"1":{"title":"RAG in Two Lines","titles":["Building a Simple Retrieval-Augmented Generation (RAG) System with RAGTools"]},"2":{"title":"Evaluations","titles":[]},"3":{"title":"Generate Q&A pairs","titles":["Evaluations"]},"4":{"title":"Explore one Q&A pair","titles":["Evaluations"]},"5":{"title":"Evaluate this Q&A pair","titles":["Evaluations"]},"6":{"title":"Evaluate the Whole Set","titles":["Evaluations"]},"7":{"title":"What would we do next?","titles":[]},"8":{"title":"Various Examples","titles":[]},"9":{"title":"ai* Functions Overview","titles":["Various Examples"]},"10":{"title":"Seamless Integration Into Your Workflow","titles":["Various Examples"]},"11":{"title":"Advanced Prompts / Conversations","titles":["Various Examples"]},"12":{"title":"Templated Prompts","titles":["Various Examples"]},"13":{"title":"Asynchronous Execution","titles":["Various Examples"]},"14":{"title":"Model Aliases","titles":["Various Examples"]},"15":{"title":"Embeddings","titles":["Various Examples"]},"16":{"title":"Classification","titles":["Various Examples"]},"17":{"title":"Routing to Defined Categories","titles":["Various Examples","Classification"]},"18":{"title":"Data Extraction","titles":["Various Examples"]},"19":{"title":"OCR and Image Comprehension","titles":["Various Examples"]},"20":{"title":"Experimental Agent Workflows / Output Validation with airetry!","titles":["Various Examples"]},"21":{"title":"Using Ollama models","titles":["Various Examples"]},"22":{"title":"Using MistralAI API and other OpenAI-compatible APIs","titles":["Various Examples"]},"23":{"title":"Using AITemplates","titles":[]},"24":{"title":"Custom APIs","titles":[]},"25":{"title":"Using MistralAI","titles":["Custom APIs"]},"26":{"title":"Using other OpenAI-compatible APIs","titles":["Custom APIs"]},"27":{"title":"Using llama.cpp server","titles":["Custom APIs"]},"28":{"title":"Using Databricks Foundation Models","titles":["Custom APIs"]},"29":{"title":"Using Together.ai","titles":["Custom APIs"]},"30":{"title":"Using Fireworks.ai","titles":["Custom APIs"]},"31":{"title":"Working with Google AI Studio","titles":[]},"32":{"title":"Text Generation with aigenerate","titles":["Working with Google AI Studio"]},"33":{"title":"Simple message","titles":["Working with Google AI Studio","Text Generation with aigenerate"]},"34":{"title":"Advanced Prompts","titles":["Working with Google AI Studio","Text Generation with aigenerate"]},"35":{"title":"Gotchas","titles":["Working with Google AI Studio","Text Generation with aigenerate"]},"36":{"title":"Local models with Ollama.ai","titles":[]},"37":{"title":"Text Generation with aigenerate","titles":["Local models with Ollama.ai"]},"38":{"title":"Simple message","titles":["Local models with Ollama.ai","Text Generation with aigenerate"]},"39":{"title":"Standard string interpolation","titles":["Local models with Ollama.ai","Text Generation with aigenerate"]},"40":{"title":"Advanced Prompts","titles":["Local models with Ollama.ai","Text Generation with aigenerate"]},"41":{"title":"Schema Changes / Custom models","titles":["Local models with Ollama.ai","Text Generation with aigenerate"]},"42":{"title":"Providing Images with aiscan","titles":["Local models with Ollama.ai"]},"43":{"title":"Embeddings with aiembed","titles":["Local models with Ollama.ai"]},"44":{"title":"Simple embedding for one document","titles":["Local models with Ollama.ai","Embeddings with aiembed"]},"45":{"title":"Multiple documents embedding","titles":["Local models with Ollama.ai","Embeddings with aiembed"]},"46":{"title":"Using postprocessing function","titles":["Local models with Ollama.ai","Embeddings with aiembed"]},"47":{"title":"Agent Tools Introduction","titles":[]},"48":{"title":"Highlights","titles":["Agent Tools Introduction"]},"49":{"title":"Examples","titles":["Agent Tools Introduction"]},"50":{"title":"Automatic Fixing of AI Calls","titles":["Agent Tools Introduction","Examples"]},"51":{"title":"References","titles":["Agent Tools Introduction"]},"52":{"title":"APITools Introduction","titles":[]},"53":{"title":"Highlights","titles":["APITools Introduction"]},"54":{"title":"References","titles":["APITools Introduction"]},"55":{"title":"RAG Tools Introduction","titles":[]},"56":{"title":"Highlights","titles":["RAG Tools Introduction"]},"57":{"title":"Examples","titles":["RAG Tools Introduction"]},"58":{"title":"RAG Interface","titles":["RAG Tools Introduction"]},"59":{"title":"System Overview","titles":["RAG Tools Introduction","RAG Interface"]},"60":{"title":"RAG Diagram","titles":["RAG Tools Introduction","RAG Interface"]},"61":{"title":"Passing Keyword Arguments","titles":["RAG Tools Introduction","RAG Interface","RAG Diagram"]},"62":{"title":"Deepdive","titles":["RAG Tools Introduction","RAG Interface"]},"63":{"title":"References","titles":["RAG Tools Introduction"]},"64":{"title":"Text Utilities","titles":[]},"65":{"title":"Highlights","titles":["Text Utilities"]},"66":{"title":"References","titles":["Text Utilities"]},"67":{"title":"Frequently Asked Questions","titles":[]},"68":{"title":"Why OpenAI","titles":["Frequently Asked Questions"]},"69":{"title":"What if I cannot access OpenAI?","titles":["Frequently Asked Questions","Why OpenAI"]},"70":{"title":"Data Privacy and OpenAI","titles":["Frequently Asked Questions"]},"71":{"title":"Creating OpenAI API Key","titles":["Frequently Asked Questions"]},"72":{"title":"Getting an error "ArgumentError: api_key cannot be empty" despite having set OPENAI_API_KEY? {#Getting-an-error-"ArgumentError:-apikey-cannot-be-empty"-despite-having-set-OPENAIAPI_KEY?}","titles":["Frequently Asked Questions"]},"73":{"title":"Getting an error "Rate limit exceeded" from OpenAI?","titles":["Frequently Asked Questions"]},"74":{"title":"Setting OpenAI Spending Limits","titles":["Frequently Asked Questions"]},"75":{"title":"How much does it cost? Is it worth paying for?","titles":["Frequently Asked Questions"]},"76":{"title":"Configuring the Environment Variable for API Key","titles":["Frequently Asked Questions"]},"77":{"title":"Setting the API Key via Preferences.jl","titles":["Frequently Asked Questions"]},"78":{"title":"Understanding the API Keyword Arguments in aigenerate (api_kwargs)","titles":["Frequently Asked Questions"]},"79":{"title":"Instant Access from Anywhere","titles":["Frequently Asked Questions"]},"80":{"title":"Open Source Alternatives","titles":["Frequently Asked Questions"]},"81":{"title":"Setup Guide for Ollama","titles":["Frequently Asked Questions"]},"82":{"title":"Changing the Default Model or Schema","titles":["Frequently Asked Questions"]},"83":{"title":"How to have Multi-turn Conversations?","titles":["Frequently Asked Questions"]},"84":{"title":"How to have typed responses?","titles":["Frequently Asked Questions"]},"85":{"title":"How to quickly create a prompt template?","titles":["Frequently Asked Questions"]},"86":{"title":"Do we have a RecursiveCharacterTextSplitter like Langchain?","titles":["Frequently Asked Questions"]},"87":{"title":"How would I fine-tune a model?","titles":["Frequently Asked Questions"]},"88":{"title":"Getting Started","titles":[]},"89":{"title":"Prerequisites","titles":["Getting Started"]},"90":{"title":"Installation","titles":["Getting Started"]},"91":{"title":"Quick Start with @ai_str","titles":["Getting Started"]},"92":{"title":"Using aigenerate with placeholders","titles":["Getting Started"]},"93":{"title":"How It Works","titles":[]},"94":{"title":"Key Concepts","titles":["How It Works"]},"95":{"title":"API/Model Providers","titles":["How It Works","Key Concepts"]},"96":{"title":"Schemas","titles":["How It Works","Key Concepts"]},"97":{"title":"Prompts","titles":["How It Works","Key Concepts"]},"98":{"title":"Messages","titles":["How It Works","Key Concepts"]},"99":{"title":"Prompt Templates","titles":["How It Works","Key Concepts"]},"100":{"title":"ai* Functions Overview","titles":["How It Works","Key Concepts"]},"101":{"title":"Walkthrough Example for aigenerate","titles":["How It Works"]},"102":{"title":"Walkthrough Example for aiextract","titles":["How It Works"]},"103":{"title":"Basic-Rag Templates","titles":[null]},"104":{"title":"Template: RAGAnswerFromContext","titles":[null,"Basic-Rag Templates"]},"105":{"title":"Metadata Templates","titles":[null]},"106":{"title":"Template: RAGExtractMetadataLong","titles":[null,"Metadata Templates"]},"107":{"title":"Template: RAGExtractMetadataShort","titles":[null,"Metadata Templates"]},"108":{"title":"Refinement Templates","titles":[null]},"109":{"title":"Template: RAGAnswerRefiner","titles":[null,"Refinement Templates"]},"110":{"title":"Evaluation Templates","titles":[null]},"111":{"title":"Template: RAGCreateQAFromContext","titles":[null,"Evaluation Templates"]},"112":{"title":"Template: RAGJudgeAnswerFromContext","titles":[null,"Evaluation Templates"]},"113":{"title":"Template: RAGJudgeAnswerFromContextShort","titles":[null,"Evaluation Templates"]},"114":{"title":"Query-Transformations Templates","titles":[null]},"115":{"title":"Template: RAGJuliaQueryHyDE","titles":[null,"Query-Transformations Templates"]},"116":{"title":"Template: RAGQueryHyDE","titles":[null,"Query-Transformations Templates"]},"117":{"title":"Template: RAGQueryOptimizer","titles":[null,"Query-Transformations Templates"]},"118":{"title":"Template: RAGQuerySimplifier","titles":[null,"Query-Transformations Templates"]},"119":{"title":"Code-Fixing Templates","titles":[null]},"120":{"title":"Template: CodeFixerRCI","titles":[null,"Code-Fixing Templates"]},"121":{"title":"Template: CodeFixerShort","titles":[null,"Code-Fixing Templates"]},"122":{"title":"Template: CodeFixerTiny","titles":[null,"Code-Fixing Templates"]},"123":{"title":"Feedback Templates","titles":[null]},"124":{"title":"Template: FeedbackFromEvaluator","titles":[null,"Feedback Templates"]},"125":{"title":"Classification Templates","titles":[null]},"126":{"title":"Template: InputClassifier","titles":[null,"Classification Templates"]},"127":{"title":"Template: JudgeIsItTrue","titles":[null,"Classification Templates"]},"128":{"title":"Xml-Formatted Templates","titles":[null]},"129":{"title":"Template: ExtractDataXML","titles":[null,"Xml-Formatted Templates"]},"130":{"title":"Extraction Templates","titles":[null]},"131":{"title":"Template: ExtractData","titles":[null,"Extraction Templates"]},"132":{"title":"General Templates","titles":[null]},"133":{"title":"Template: BlankSystemUser","titles":[null,"General Templates"]},"134":{"title":"Template: PromptEngineerForTask","titles":[null,"General Templates"]},"135":{"title":"Persona-Task Templates","titles":[null]},"136":{"title":"Template: AnalystChaptersInTranscript","titles":[null,"Persona-Task Templates"]},"137":{"title":"Template: AnalystDecisionsInTranscript","titles":[null,"Persona-Task Templates"]},"138":{"title":"Template: AnalystThemesInResponses","titles":[null,"Persona-Task Templates"]},"139":{"title":"Theme 1: [Theme Description]","titles":[]},"140":{"title":"Theme 2: [Theme Description]","titles":[]},"141":{"title":"Template: AssistantAsk","titles":["Theme 2: [Theme Description]",null]},"142":{"title":"Template: DetailOrientedTask","titles":["Theme 2: [Theme Description]",null]},"143":{"title":"Template: DrafterEmailBrief","titles":["Theme 2: [Theme Description]",null]},"144":{"title":"Template: JuliaExpertAsk","titles":["Theme 2: [Theme Description]",null]},"145":{"title":"Template: JuliaExpertCoTTask","titles":["Theme 2: [Theme Description]",null]},"146":{"title":"Template: JuliaExpertTestCode","titles":["Theme 2: [Theme Description]",null]},"147":{"title":"Template: JuliaRecapCoTTask","titles":["Theme 2: [Theme Description]",null]},"148":{"title":"Template: JuliaRecapTask","titles":["Theme 2: [Theme Description]",null]},"149":{"title":"Template: StorytellerExplainSHAP","titles":["Theme 2: [Theme Description]",null]},"150":{"title":"Xml-Formatted Templates","titles":["Theme 2: [Theme Description]"]},"151":{"title":"Template: JuliaExpertAskXML","titles":["Theme 2: [Theme Description]","Xml-Formatted Templates"]},"152":{"title":"Template: JuliaExpertCoTTaskXML","titles":["Theme 2: [Theme Description]","Xml-Formatted Templates"]},"153":{"title":"Template: JuliaExpertTestCodeXML","titles":["Theme 2: [Theme Description]","Xml-Formatted Templates"]},"154":{"title":"Visual Templates","titles":[null]},"155":{"title":"Template: OCRTask","titles":[null,"Visual Templates"]},"156":{"title":"Reference","titles":[]},"157":{"title":"Reference for AgentTools","titles":[]},"158":{"title":"Reference for APITools","titles":[]},"159":{"title":"Reference for Experimental Module","titles":[]},"160":{"title":"Reference for RAGTools","titles":[]}},"dirtCount":0,"index":[["θ",{"2":{"157":1}}],["β",{"2":{"157":1}}],["α",{"2":{"157":2}}],["→",{"2":{"102":1}}],["zoom",{"2":{"102":1}}],["zshrc",{"2":{"76":1}}],["zero",{"2":{"66":1,"146":1,"153":1,"156":3}}],["~300",{"2":{"156":3}}],["~0",{"2":{"75":1}}],["~",{"2":{"72":1,"76":1,"79":1}}],["~word",{"2":{"63":1,"160":1}}],["~words",{"2":{"63":1,"160":1}}],["^",{"2":{"66":2,"156":2}}],["÷",{"2":{"51":1,"63":1,"157":1,"160":1}}],["├─",{"2":{"51":9,"157":11}}],["👋",{"2":{"156":1}}],["😊",{"2":{"41":1}}],["😃",{"2":{"1":1,"51":1,"157":1}}],["93",{"2":{"157":1}}],["905",{"2":{"156":1}}],["90b690d4509e7792dd80f18b7a97800399f51f28",{"2":{"66":1,"156":5,"160":1}}],["968",{"2":{"109":1}}],["911",{"2":{"102":2}}],["911t",{"2":{"84":2}}],["94",{"2":{"51":1,"157":1}}],["9999999999999982",{"2":{"46":1}}],["99",{"2":{"27":1,"157":1}}],["9",{"2":{"21":1,"22":1,"25":1,"30":1,"156":2}}],["9examples",{"2":{"6":1}}],["|im",{"2":{"156":4}}],["|",{"2":{"20":2,"50":2,"51":1,"157":1}}],["|>",{"2":{"12":3,"19":1,"23":1,"50":1,"51":4,"66":1,"84":2,"100":1,"156":12,"157":5}}],["y`",{"2":{"147":1,"148":1}}],["yarrr",{"2":{"85":2,"156":2}}],["yay",{"2":{"51":1,"157":1}}],["y",{"2":{"51":2,"79":1,"156":2,"157":4}}],["yes",{"2":{"40":1,"86":1,"92":1}}],["yedi",{"2":{"34":1,"40":2,"156":5}}],["yet",{"2":{"31":1,"51":3,"101":1,"143":1,"156":4,"157":2,"159":1,"160":2}}],["yellow",{"2":{"20":2,"50":3,"51":5,"157":5}}],["yoda",{"2":{"11":2,"34":1,"40":1,"156":5}}],["youtube",{"2":{"136":1,"137":1}}],["young",{"2":{"11":1,"34":1,"40":1,"156":1}}],["yours",{"2":{"156":6}}],["yourself",{"2":{"40":2}}],["your",{"0":{"10":1},"2":{"1":4,"3":1,"7":1,"9":1,"10":3,"11":1,"12":3,"14":1,"21":1,"22":3,"23":9,"25":1,"26":1,"27":1,"28":1,"31":2,"34":2,"36":2,"40":1,"41":1,"51":3,"55":1,"56":1,"59":1,"60":1,"63":4,"65":2,"70":5,"71":1,"72":2,"73":2,"74":1,"75":1,"76":6,"77":2,"79":1,"81":2,"82":3,"83":2,"85":2,"87":1,"89":4,"90":1,"91":2,"94":1,"96":1,"98":1,"99":4,"100":1,"101":4,"102":3,"109":1,"112":1,"115":1,"116":1,"117":2,"120":6,"121":1,"126":1,"136":3,"137":4,"138":1,"141":2,"142":1,"143":1,"144":2,"145":4,"146":3,"149":2,"151":2,"152":4,"153":3,"156":35,"157":3,"160":7}}],["you",{"2":{"0":1,"1":3,"3":2,"4":1,"6":6,"7":1,"9":9,"10":9,"11":11,"12":7,"13":2,"14":2,"15":2,"16":3,"17":1,"18":6,"19":4,"20":6,"21":5,"22":12,"23":32,"24":1,"25":9,"26":3,"27":5,"28":5,"29":9,"30":9,"31":2,"32":1,"33":4,"34":3,"36":4,"38":6,"39":2,"40":10,"41":9,"42":1,"45":3,"48":1,"50":4,"51":25,"53":1,"54":1,"55":1,"56":2,"57":7,"59":7,"61":4,"63":15,"65":2,"66":5,"68":1,"70":2,"71":1,"72":6,"73":11,"74":3,"75":9,"76":5,"77":2,"79":1,"80":2,"81":7,"82":5,"83":4,"84":5,"85":21,"87":2,"89":5,"90":1,"91":3,"92":3,"94":4,"95":3,"96":1,"97":3,"98":1,"99":8,"100":8,"101":8,"102":19,"104":2,"106":2,"107":2,"109":2,"111":1,"118":1,"120":4,"121":2,"122":2,"126":2,"127":1,"136":3,"137":1,"138":1,"140":1,"141":2,"142":1,"144":2,"145":3,"146":3,"148":1,"149":1,"151":2,"152":2,"153":3,"156":148,"157":24,"158":1,"160":24}}],["└─",{"2":{"51":9,"157":13}}],["└",{"2":{"10":1}}],["┌",{"2":{"10":1}}],["74",{"2":{"92":1}}],["75",{"2":{"63":1,"160":1}}],["77",{"2":{"51":1,"157":1}}],["786",{"2":{"121":1}}],["78",{"2":{"30":1}}],["787",{"2":{"15":1,"156":1}}],["70b",{"2":{"28":3}}],["7",{"2":{"10":1,"51":7,"57":1,"143":1,"156":2,"157":8}}],["7examples",{"2":{"6":1}}],["`top",{"2":{"160":1}}],["`test",{"2":{"63":1,"160":1}}],["`textchunker",{"2":{"63":1,"160":1}}],["`build",{"2":{"160":1}}],["`begin`",{"2":{"147":1,"148":1}}],["`run",{"2":{"157":1}}],["`register",{"2":{"156":1}}],["`return",{"2":{"100":1}}],["`1",{"2":{"156":1}}],["`1+1`",{"2":{"156":5}}],["`2`",{"2":{"156":5}}],["`error`",{"2":{"156":1}}],["`end`",{"2":{"147":1,"148":1}}],["`example`",{"2":{"23":2}}],["`$`",{"2":{"147":1,"148":1}}],["`$a+$a`",{"2":{"39":1,"156":6}}],["`while`",{"2":{"147":1,"148":1}}],["`function`",{"2":{"147":1,"148":1}}],["`function",{"2":{"147":1,"148":1}}],["`for`",{"2":{"147":1,"148":1}}],["`false`",{"2":{"102":1}}],["`fahrenheit`",{"2":{"18":1}}],["`image",{"2":{"156":2}}],["`isx",{"2":{"147":1,"148":1}}],["`if",{"2":{"147":1,"148":1}}],["`index`",{"2":{"63":2,"160":2}}],["`innerjoin`",{"2":{"6":1}}],["`x",{"2":{"147":2,"148":2}}],["`other",{"2":{"137":1}}],["`out",{"2":{"51":1,"157":1}}],["`dict",{"2":{"147":1,"148":1}}],["`distributed`",{"2":{"57":1}}],["`data`",{"2":{"131":1}}],["`you",{"2":{"98":1}}],["`score",{"2":{"160":1}}],["`schema",{"2":{"102":1}}],["`schema`",{"2":{"26":1,"27":1}}],["`success",{"2":{"51":1,"157":1}}],["`model",{"2":{"156":1}}],["`model`",{"2":{"27":1}}],["`maybeextract",{"2":{"156":1}}],["`map`",{"2":{"73":1}}],["`message`",{"2":{"156":1}}],["`msg",{"2":{"36":1}}],["`pt",{"2":{"26":1,"27":1}}],["`pkg`",{"2":{"23":1}}],["`usermessage`",{"2":{"51":1,"157":1}}],["`using`",{"2":{"23":1}}],["`unit`",{"2":{"18":1}}],["`local",{"2":{"156":1}}],["`location`",{"2":{"18":1}}],["`last",{"2":{"20":2,"50":2,"51":2,"157":2}}],["`number`",{"2":{"147":1,"148":1}}],["`nothing`",{"2":{"102":1}}],["`n",{"2":{"20":1,"50":1,"51":1,"157":1}}],["`condition`",{"2":{"102":1}}],["`convert`",{"2":{"102":1}}],["`conversation`",{"2":{"51":1,"100":1,"157":1}}],["`config",{"2":{"51":1,"157":1}}],["`config`",{"2":{"20":1,"50":1,"51":1,"157":1}}],["`celsius`",{"2":{"18":1}}],["`abstractstring`",{"2":{"147":1,"148":1}}],["`a",{"2":{"147":1,"148":1}}],["`answerer",{"2":{"61":1}}],["`answerer`",{"2":{"61":1}}],["`answer",{"2":{"61":1}}],["`add`",{"2":{"23":1,"146":1,"153":1}}],["`aigenerate",{"2":{"51":1,"157":1}}],["`aicall`",{"2":{"20":1,"50":1,"51":2,"157":2}}],["`airag`",{"2":{"5":1,"63":1,"160":1}}],["`api",{"2":{"20":1,"36":1,"50":1,"51":1,"157":1}}],["`ask`",{"2":{"12":1,"23":2,"156":2}}],["``",{"2":{"12":1,"156":1}}],["```plaintext",{"2":{"121":1,"122":1}}],["````",{"2":{"51":1,"157":1}}],["```julia",{"2":{"23":2,"66":1,"120":2,"121":1,"146":2,"153":2,"156":3,"157":1,"160":1}}],["```sql",{"2":{"19":1,"156":2}}],["```",{"2":{"10":2,"23":2,"51":1,"112":1,"113":1,"120":1,"121":1,"122":1,"136":2,"137":2,"143":2,"146":2,"153":2,"157":2,"160":1}}],["`",{"2":{"10":2,"20":1,"23":2,"26":1,"27":1,"36":2,"50":1,"51":2,"57":1,"61":1,"63":1,"102":2,"120":2,"146":6,"147":8,"148":8,"153":6,"156":4,"157":3,"160":1}}],["│",{"2":{"6":12,"10":5,"51":14,"157":16}}],["22",{"2":{"157":1}}],["26078",{"2":{"157":3}}],["267",{"2":{"118":1}}],["29826",{"2":{"157":3}}],["29",{"2":{"156":2}}],["2900",{"2":{"66":2,"156":2}}],["21",{"2":{"156":1}}],["2190",{"2":{"137":1}}],["210",{"2":{"122":1}}],["278",{"2":{"107":1}}],["2733",{"2":{"51":4,"157":4}}],["256",{"2":{"160":2}}],["2500",{"2":{"156":7}}],["25px",{"2":{"66":1,"156":5,"160":1}}],["25",{"2":{"63":3,"160":4}}],["248",{"2":{"151":1}}],["2487",{"2":{"120":1}}],["24",{"2":{"35":1}}],["24622",{"2":{"19":1,"156":2}}],["239",{"2":{"155":1}}],["23",{"2":{"51":1,"156":1,"157":1}}],["23rd",{"2":{"30":1}}],["237",{"2":{"12":1,"23":1,"144":1,"156":2}}],["2s",{"2":{"20":1,"50":1,"51":2,"157":2}}],["2",{"0":{"140":1},"1":{"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"150":1,"151":1,"152":1,"153":1},"2":{"6":3,"10":1,"12":1,"18":1,"19":3,"20":5,"23":3,"28":3,"45":1,"46":2,"50":5,"51":19,"54":1,"57":1,"63":2,"66":7,"72":1,"73":1,"75":1,"84":1,"85":1,"86":2,"102":2,"106":2,"107":1,"111":1,"112":1,"120":3,"121":1,"126":1,"138":1,"143":1,"145":1,"146":2,"147":3,"148":3,"153":2,"156":33,"157":33,"158":1,"160":6}}],["2000",{"2":{"146":1,"153":1}}],["200",{"2":{"75":1}}],["20506",{"2":{"51":1,"157":1}}],["20737",{"2":{"51":4,"157":4}}],["2049",{"2":{"136":1}}],["20493",{"2":{"51":2,"157":2}}],["2048",{"2":{"27":1,"156":2}}],["2021",{"2":{"106":2}}],["2020",{"2":{"92":1}}],["2024",{"2":{"30":1,"156":1}}],["2023",{"2":{"14":1,"57":1}}],["20",{"2":{"6":6,"17":1,"66":2,"115":1,"116":1,"156":9}}],["2examples",{"2":{"6":1}}],["$f",{"2":{"102":1}}],["$25",{"2":{"102":1}}],["$10",{"2":{"74":1}}],["$50",{"2":{"107":1}}],["$5",{"2":{"74":1}}],["$user",{"2":{"51":1,"157":1}}],["$upper",{"2":{"51":1,"157":1}}],["$lower",{"2":{"51":1,"157":1}}],["$",{"2":{"6":3,"51":9,"66":7,"84":1,"91":1,"102":1,"156":7,"157":9}}],["$0",{"2":{"3":1,"10":1,"19":2,"22":1,"25":1,"29":1,"30":1,"57":1,"83":1,"91":2,"92":1,"156":5}}],[">tryparse",{"2":{"84":1}}],[">x",{"2":{"6":2,"160":1}}],[">",{"2":{"6":1,"12":1,"17":3,"20":1,"30":1,"45":1,"50":1,"51":5,"57":1,"61":1,"63":1,"66":5,"91":1,"102":2,"120":2,"136":2,"137":3,"147":1,"148":1,"156":26,"157":6,"160":5}}],["x123",{"2":{"156":2}}],["x^2`",{"2":{"147":1,"148":1}}],["xml",{"0":{"128":1,"150":1},"1":{"129":1,"151":1,"152":1,"153":1},"2":{"129":1,"151":1,"152":1,"153":1,"156":1}}],["x3c",{"2":{"19":1,"26":1,"51":6,"57":1,"59":1,"63":4,"66":13,"84":5,"120":4,"129":4,"136":3,"137":3,"143":2,"146":1,"151":2,"152":12,"153":17,"156":122,"157":11,"160":65}}],["xyz",{"2":{"10":3,"51":1,"75":1,"100":2,"157":3,"160":1}}],["x",{"2":{"6":4,"20":2,"45":2,"50":2,"51":6,"73":4,"79":1,"84":2,"147":2,"148":2,"156":11,"157":17,"160":3}}],["x26",{"2":{"3":1,"66":2,"111":1,"156":10,"160":2}}],["0s",{"2":{"157":1}}],["08",{"2":{"156":2}}],["02",{"2":{"156":1}}],["024",{"2":{"6":1}}],["07",{"2":{"156":1}}],["05",{"2":{"156":2,"157":1}}],["0011",{"2":{"156":1}}],["0015",{"2":{"156":3}}],["002",{"2":{"156":3}}],["000",{"2":{"63":1,"66":2,"73":3,"156":2,"160":4}}],["0001",{"2":{"29":1,"30":1,"75":2,"91":1,"92":1}}],["0002",{"2":{"10":1}}],["0045",{"2":{"19":1,"156":1}}],["0117",{"2":{"19":1,"156":2}}],["014",{"2":{"6":7}}],["015",{"2":{"6":2}}],["0dict",{"2":{"6":3}}],["0",{"2":{"5":2,"9":2,"15":2,"18":1,"21":1,"22":2,"25":2,"30":1,"31":1,"32":1,"41":1,"46":2,"51":8,"57":6,"63":22,"65":2,"66":4,"75":1,"81":2,"83":2,"85":2,"91":2,"100":1,"104":1,"107":1,"109":1,"113":1,"115":1,"116":1,"117":1,"118":1,"121":1,"122":1,"124":1,"129":1,"145":1,"146":3,"148":1,"149":1,"152":1,"153":4,"156":64,"157":29,"160":53}}],["39931",{"2":{"157":2}}],["390",{"2":{"115":1}}],["31",{"2":{"91":1}}],["34900",{"2":{"66":2,"156":2}}],["34",{"2":{"57":1,"156":1}}],["34315",{"2":{"51":1,"157":1}}],["375",{"2":{"104":1}}],["37581",{"2":{"51":1,"157":1}}],["37",{"2":{"51":1,"157":1}}],["354",{"2":{"116":1}}],["35",{"2":{"51":4,"66":2,"156":2,"157":4}}],["35603",{"2":{"51":1,"157":1}}],["32000",{"2":{"157":3}}],["32991",{"2":{"51":5,"157":5}}],["32",{"2":{"51":2,"91":1,"157":2}}],["33",{"2":{"51":5,"157":5}}],["33333dict",{"2":{"6":1}}],["3x",{"2":{"51":1,"157":1}}],["366",{"2":{"126":1}}],["36",{"2":{"51":1,"157":1}}],["362",{"2":{"19":1,"156":1}}],["3000",{"2":{"146":1,"153":1}}],["30088",{"2":{"51":2,"157":2}}],["30",{"2":{"18":2,"75":1,"115":1,"116":1,"156":7}}],["3examples",{"2":{"6":1}}],["3",{"2":{"5":2,"6":12,"9":1,"10":1,"23":1,"36":1,"51":3,"57":4,"66":3,"73":1,"86":1,"91":1,"92":3,"100":1,"102":2,"106":3,"107":2,"120":4,"121":1,"138":3,"143":1,"146":2,"147":1,"148":1,"153":2,"156":23,"157":8}}],["+",{"2":{"5":1,"51":2,"57":2,"59":1,"62":1,"102":2,"146":1,"153":1,"156":5,"157":2}}],["5th",{"2":{"160":1}}],["595",{"2":{"152":1}}],["519",{"2":{"129":1,"145":1}}],["514",{"2":{"117":1}}],["512",{"2":{"51":2,"157":2}}],["5=best",{"2":{"113":1}}],["50m",{"2":{"156":2}}],["500",{"2":{"131":1}}],["50086",{"2":{"51":4,"157":4}}],["50",{"2":{"51":4,"83":1,"157":4}}],["52910",{"2":{"51":4,"157":4}}],["55394",{"2":{"51":1,"157":1}}],["57694",{"2":{"51":1,"157":1}}],["5examples",{"2":{"6":1}}],["5",{"2":{"5":5,"10":2,"19":1,"21":3,"27":1,"29":2,"30":1,"36":3,"39":1,"46":1,"51":11,"54":1,"57":3,"61":2,"63":11,"73":1,"81":2,"83":1,"91":3,"94":1,"98":1,"101":1,"106":2,"107":1,"112":14,"113":2,"138":2,"143":2,"146":1,"149":1,"153":1,"156":36,"157":11,"158":1,"160":17}}],["84",{"2":{"157":1}}],["837",{"2":{"156":1}}],["82",{"2":{"51":1,"157":1}}],["87",{"2":{"29":1}}],["8755f69180b7ac7ee76a69ae68ec36872a116ad4",{"2":{"19":1,"156":2}}],["8x7b",{"2":{"27":1,"36":1,"102":1}}],["80k",{"2":{"160":1}}],["80kg",{"2":{"18":1,"156":3}}],["8080",{"2":{"27":1,"61":3,"63":3,"156":2,"160":3}}],["8081",{"2":{"22":1,"156":2}}],["80",{"2":{"18":1,"156":2,"160":2}}],["8examples",{"2":{"6":1}}],["8",{"2":{"5":1,"51":1,"157":1}}],["60",{"2":{"51":3,"73":1,"75":1,"156":6}}],["67",{"2":{"51":10,"157":11}}],["67dict",{"2":{"6":3}}],["69",{"2":{"21":1,"156":2}}],["66667dict",{"2":{"6":3}}],["6examples",{"2":{"6":1}}],["6",{"2":{"5":1,"6":1,"41":1,"51":6,"66":1,"73":1,"156":2,"157":7}}],["48",{"2":{"157":1}}],["48343",{"2":{"51":1,"157":1}}],["420",{"2":{"113":1}}],["4k",{"2":{"66":1,"156":1}}],["46",{"2":{"85":1,"156":1}}],["46632",{"2":{"51":1,"157":1}}],["46839",{"2":{"51":2,"157":2}}],["43094",{"2":{"51":1,"157":1}}],["43",{"2":{"51":1,"157":1}}],["44816",{"2":{"51":2,"157":2}}],["41",{"2":{"51":1,"124":1,"157":1}}],["4examples",{"2":{"6":1}}],["402",{"2":{"134":1}}],["40796033843072876",{"2":{"46":1}}],["4096×2",{"2":{"21":1,"45":1,"156":1}}],["4096",{"2":{"21":1,"44":2,"45":1,"46":1,"156":2}}],["40",{"2":{"6":8,"57":1}}],["4",{"2":{"5":3,"6":2,"14":5,"22":1,"23":1,"25":1,"51":10,"57":3,"66":1,"91":1,"92":1,"106":1,"138":1,"156":6,"157":10,"160":4}}],["1`",{"2":{"156":1}}],["1+1",{"2":{"156":1}}],["16",{"2":{"156":2}}],["1643",{"2":{"153":1}}],["16k",{"2":{"66":1,"156":1,"157":1}}],["17",{"2":{"156":2}}],["175b",{"2":{"156":3}}],["1712",{"2":{"149":1}}],["172",{"2":{"142":1}}],["1>",{"2":{"136":1}}],["184",{"2":{"141":1}}],["18",{"2":{"133":1,"156":1,"157":1}}],["180",{"2":{"18":1,"156":2}}],["180cm",{"2":{"18":1,"156":3}}],["150",{"2":{"143":1}}],["1506",{"2":{"138":1}}],["151",{"2":{"127":1}}],["1536×2",{"2":{"156":1}}],["1536",{"2":{"15":1,"156":1}}],["1=worst",{"2":{"113":1}}],["13184",{"2":{"157":2}}],["1396",{"2":{"111":1}}],["1384",{"2":{"106":1}}],["1m",{"2":{"92":1}}],["1em",{"2":{"66":1,"156":5,"160":1}}],["1examples",{"2":{"6":1}}],["1px",{"2":{"66":1,"156":5,"160":1}}],["1475",{"2":{"146":1}}],["1415",{"2":{"112":1}}],["14966",{"2":{"51":4,"157":4}}],["14",{"2":{"51":1,"157":1}}],["124",{"2":{"156":1}}],["127",{"2":{"81":1,"156":5}}],["12940",{"2":{"51":1,"157":1}}],["12",{"2":{"51":2,"73":1,"156":2,"157":2}}],["123",{"2":{"23":1,"82":1}}],["1204",{"2":{"143":1}}],["120",{"2":{"9":2,"73":1,"100":2,"156":9}}],["111",{"2":{"156":2}}],["11434",{"2":{"81":1,"156":3}}],["1143",{"2":{"66":1,"147":1,"148":1,"156":1}}],["114",{"2":{"22":1,"25":1}}],["1141",{"2":{"19":1,"156":2}}],["1106",{"2":{"14":2}}],["1928",{"2":{"111":3}}],["190",{"2":{"18":2,"156":3}}],["19",{"2":{"18":2,"57":1,"156":3}}],["10897",{"2":{"156":5}}],["10`",{"2":{"147":1,"148":1}}],["10examples",{"2":{"6":1}}],["10×8",{"2":{"6":1}}],["100k",{"2":{"160":1}}],["1000",{"2":{"75":1,"84":1,"146":1,"153":1,"156":4}}],["100x",{"2":{"73":1}}],["100",{"2":{"6":3,"51":4,"61":2,"63":5,"156":11,"157":3,"160":7}}],["10",{"2":{"5":1,"6":4,"10":1,"51":8,"57":1,"63":1,"66":1,"72":2,"73":2,"102":1,"156":15,"157":6,"160":2}}],["1024x1024",{"2":{"156":2}}],["102",{"2":{"3":1,"10":1}}],["1",{"0":{"139":1},"2":{"4":1,"5":1,"6":13,"9":2,"12":3,"15":2,"17":1,"20":6,"23":4,"27":1,"29":1,"31":1,"32":1,"36":2,"39":1,"44":1,"46":1,"50":6,"51":48,"57":6,"63":8,"65":2,"66":9,"72":1,"75":1,"81":1,"83":1,"85":4,"86":2,"91":1,"92":2,"100":3,"101":1,"102":3,"104":1,"106":4,"107":1,"109":1,"111":5,"112":17,"113":3,"115":1,"116":1,"117":1,"118":1,"120":3,"121":3,"122":1,"124":1,"126":3,"127":2,"129":1,"131":2,"133":2,"134":1,"136":5,"137":4,"138":3,"141":1,"142":2,"143":3,"144":1,"146":3,"147":6,"148":5,"149":1,"151":1,"152":1,"153":2,"155":1,"156":64,"157":63,"160":24}}],["q4",{"2":{"27":1,"36":1}}],["qaevalresult",{"2":{"5":1,"160":5}}],["qaevalitems",{"2":{"160":1}}],["qaevalitem",{"2":{"3":1,"4":1,"63":5,"160":13}}],["qa",{"2":{"3":1,"5":2,"6":5,"56":1,"63":8,"156":3,"160":38}}],["q",{"0":{"3":1,"4":1,"5":1},"2":{"2":1,"3":1,"5":1,"63":4,"160":4}}],["quarter",{"2":{"63":1,"160":1}}],["quantum",{"2":{"57":1}}],["quality=",{"2":{"156":1}}],["quality`",{"2":{"156":1}}],["quality",{"2":{"2":2,"4":1,"6":1,"7":1,"12":2,"16":1,"23":3,"86":1,"99":1,"101":2,"112":2,"113":2,"136":1,"141":1,"144":1,"151":1,"156":6,"160":2}}],["queried",{"2":{"156":2}}],["queries",{"2":{"126":1}}],["query",{"0":{"114":1},"1":{"115":1,"116":1,"117":1,"118":1},"2":{"54":3,"62":3,"66":7,"109":7,"115":7,"116":5,"117":11,"118":8,"156":15,"158":3,"160":12}}],["question=",{"2":{"160":4}}],["question>",{"2":{"151":2}}],["questions",{"0":{"67":1},"1":{"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1,"85":1,"86":1,"87":1},"2":{"2":1,"3":1,"12":1,"14":1,"22":1,"23":2,"25":1,"30":1,"33":1,"36":1,"41":1,"57":1,"63":3,"66":1,"91":1,"104":1,"115":2,"141":1,"144":1,"151":1,"156":4,"160":7}}],["question",{"2":{"1":5,"4":1,"5":4,"6":2,"7":1,"12":1,"22":1,"23":7,"25":1,"56":2,"57":8,"60":2,"61":3,"63":29,"75":1,"97":1,"98":1,"99":2,"101":2,"104":5,"111":7,"112":8,"113":6,"116":1,"136":1,"137":1,"138":4,"140":2,"141":1,"144":1,"156":2,"160":55}}],["quirks",{"2":{"84":1}}],["quicker",{"2":{"157":1}}],["quick",{"0":{"91":1},"2":{"28":1,"72":1,"85":2,"89":1,"143":1,"156":6}}],["quickly",{"0":{"85":1},"2":{"10":1,"55":1,"66":1,"136":1,"156":1}}],["quite",{"2":{"27":1,"62":1,"73":1,"86":1,"102":1,"160":1}}],["quote",{"2":{"138":1}}],["quotes",{"2":{"120":3,"136":1}}],["quot",{"0":{"72":4,"73":2},"2":{"0":4,"1":2,"5":2,"6":8,"9":8,"10":6,"12":4,"14":10,"16":8,"17":2,"20":6,"23":2,"27":2,"28":2,"29":2,"30":2,"31":2,"32":2,"33":2,"36":4,"41":10,"48":6,"50":6,"51":26,"54":6,"57":2,"59":2,"63":20,"66":52,"72":8,"73":4,"75":2,"76":4,"77":6,"79":2,"81":2,"82":10,"83":6,"84":8,"86":16,"87":6,"89":6,"91":4,"92":2,"94":2,"95":4,"96":6,"98":8,"99":6,"100":8,"101":6,"102":16,"106":2,"107":2,"111":2,"124":2,"136":2,"137":2,"138":2,"139":2,"140":2,"146":2,"148":2,"149":2,"153":2,"156":230,"157":64,"158":6,"160":37}}],["vllm",{"2":{"69":1,"156":1}}],["vocab",{"2":{"160":6}}],["vocabulary",{"2":{"63":1,"160":7}}],["voyage",{"2":{"156":4}}],["voyager",{"2":{"69":1}}],["v2",{"2":{"36":1,"63":1,"160":3}}],["v1",{"2":{"27":1,"29":1,"30":1,"72":2,"156":8,"160":2}}],["v0",{"2":{"27":1,"102":1}}],["vcat",{"2":{"11":1}}],["vscodedisplay",{"2":{"12":2,"23":2,"156":4}}],["vscode",{"2":{"10":1,"12":1,"23":1,"156":2}}],["vs",{"2":{"9":1,"51":2,"56":1,"63":1,"66":1,"94":1,"156":1,"157":2,"160":2}}],["vidid",{"2":{"156":2}}],["video",{"2":{"136":3,"137":1}}],["videos",{"2":{"136":2,"137":2}}],["vibrant",{"2":{"66":1,"156":2}}],["visits",{"2":{"157":6}}],["visible",{"2":{"155":1}}],["vision",{"2":{"9":1,"100":1,"156":4}}],["visualize",{"2":{"156":1}}],["visualization",{"2":{"57":1}}],["visual",{"0":{"154":1},"1":{"155":1},"2":{"71":1,"89":1}}],["view",{"2":{"51":1,"157":1}}],["via",{"0":{"77":1},"2":{"1":1,"5":1,"11":1,"15":1,"19":1,"22":2,"25":2,"27":1,"41":1,"51":2,"57":1,"59":1,"62":1,"63":2,"65":1,"72":1,"81":1,"89":1,"95":1,"102":3,"104":1,"109":1,"127":1,"129":1,"131":1,"156":20,"157":2,"160":7}}],["vararg",{"2":{"156":2}}],["varying",{"2":{"156":1}}],["variable",{"0":{"76":1},"2":{"29":1,"30":1,"51":2,"53":1,"76":3,"83":1,"89":3,"106":1,"149":2,"156":10}}],["variables",{"2":{"11":1,"12":1,"23":1,"28":1,"51":2,"72":1,"76":1,"85":1,"90":1,"91":1,"92":1,"106":1,"147":1,"148":1,"156":29}}],["variety",{"2":{"22":1,"25":1}}],["various",{"0":{"8":1},"1":{"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1},"2":{"51":1,"59":1,"92":1,"157":1,"160":2}}],["vanilla",{"2":{"102":1}}],["vanished",{"2":{"66":1,"156":1}}],["vast",{"2":{"66":1,"156":1}}],["valid",{"2":{"48":1,"63":1,"156":1,"157":1,"160":2}}],["validated",{"2":{"160":1}}],["validate",{"2":{"20":2,"50":1,"146":1,"153":1}}],["validation",{"0":{"20":1},"2":{"50":1}}],["value2",{"2":{"160":1}}],["value1",{"2":{"160":1}}],["value",{"2":{"6":2,"20":1,"50":1,"51":3,"106":1,"107":1,"147":1,"148":1,"149":2,"156":15,"157":8,"160":1}}],["values",{"2":{"5":1,"6":10,"51":1,"59":1,"106":1,"149":6,"156":5,"157":1}}],["valuable",{"2":{"0":1}}],["ve",{"2":{"36":1,"41":1,"73":1,"81":1,"101":1}}],["vegetable",{"2":{"16":1,"156":2}}],["versus",{"2":{"160":1}}],["version=",{"2":{"85":1,"156":1}}],["versions",{"2":{"72":1}}],["version",{"2":{"6":1,"12":2,"23":1,"41":1,"50":1,"51":1,"57":1,"63":1,"72":1,"81":1,"85":1,"92":1,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":2,"121":1,"122":1,"124":1,"126":1,"127":1,"129":1,"131":1,"133":1,"134":1,"136":2,"137":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"151":1,"152":1,"153":1,"155":1,"156":7,"157":1,"160":2}}],["verify",{"2":{"156":1}}],["verbatim",{"2":{"138":3,"140":1}}],["verbosity",{"2":{"51":2,"63":1,"157":4,"160":1}}],["verbose=2",{"2":{"157":1}}],["verbose=true",{"2":{"63":1,"160":1}}],["verbose=false",{"2":{"51":1,"157":1}}],["verbose",{"2":{"3":1,"5":1,"6":2,"9":1,"12":1,"20":1,"50":1,"51":6,"63":15,"100":1,"156":28,"157":7,"160":47}}],["very",{"2":{"9":1,"17":2,"20":1,"65":1,"73":1,"81":1,"100":1,"112":2,"145":1,"147":1,"148":1,"152":1,"156":2,"160":1}}],["vectorstore",{"2":{"118":1}}],["vectors",{"2":{"63":1,"156":3,"160":3}}],["vectorized",{"2":{"6":1}}],["vector",{"2":{"3":1,"12":2,"15":1,"18":2,"23":3,"30":1,"44":4,"46":1,"51":4,"57":1,"60":1,"63":17,"66":13,"83":1,"84":1,"85":4,"87":1,"96":1,"99":1,"101":4,"102":1,"156":126,"157":6,"160":55}}],["king",{"2":{"54":2,"158":2}}],["kinds",{"2":{"6":3}}],["knows",{"2":{"96":1}}],["knowing",{"2":{"16":1}}],["knowledge",{"2":{"12":1,"14":1,"23":4,"55":2,"104":1,"109":1,"144":1,"151":1,"156":2}}],["know",{"2":{"11":1,"22":1,"23":2,"25":1,"29":2,"30":1,"73":1,"84":1,"104":2,"109":2,"147":1,"148":1,"156":1}}],["known",{"2":{"9":1,"31":1,"66":1,"86":1,"100":1,"156":4}}],["kwargs`",{"2":{"61":1}}],["kwargs=",{"2":{"19":1,"22":1,"26":1,"27":1,"36":2,"51":3,"57":1,"59":1,"100":2,"102":2,"156":6,"157":3,"160":1}}],["kwargs",{"0":{"78":1},"2":{"9":3,"20":2,"28":1,"50":2,"51":13,"57":2,"59":2,"61":22,"63":72,"85":1,"100":3,"102":2,"156":110,"157":31,"158":2,"160":109}}],["kwarg",{"2":{"9":1,"51":1,"59":3,"72":1,"83":1,"100":1,"157":1,"160":2}}],["k=5",{"2":{"160":1}}],["k=5`",{"2":{"160":1}}],["k=100",{"2":{"63":1,"160":1}}],["k=",{"2":{"6":10}}],["kept",{"2":{"41":1}}],["key1",{"2":{"156":1}}],["keylocal",{"2":{"156":1}}],["keypreset",{"2":{"156":1}}],["key=env",{"2":{"22":1,"25":1}}],["keywords",{"2":{"106":1,"107":2,"156":1,"160":2}}],["keyword",{"0":{"61":1,"78":1},"2":{"9":5,"11":1,"13":1,"14":1,"23":2,"42":2,"48":1,"51":5,"59":2,"61":2,"63":8,"73":1,"83":2,"85":1,"92":1,"99":1,"100":5,"107":1,"133":1,"156":48,"157":11,"160":11}}],["keys",{"2":{"6":1,"22":1,"26":1,"57":1,"101":1,"102":1,"156":3,"160":3}}],["key",{"0":{"71":1,"72":3,"76":1,"77":1,"94":1},"1":{"95":1,"96":1,"97":1,"98":1,"99":1,"100":1},"2":{"5":1,"6":9,"7":1,"22":5,"23":2,"25":2,"26":3,"28":3,"29":1,"30":1,"31":3,"51":2,"53":2,"54":4,"63":2,"71":3,"72":9,"76":10,"77":5,"89":12,"93":1,"94":1,"101":1,"102":2,"115":1,"116":1,"129":1,"131":1,"136":3,"137":5,"147":3,"148":3,"156":114,"157":3,"158":5,"160":8}}],["keeping",{"2":{"157":1}}],["keeps",{"2":{"5":1,"156":2}}],["keep",{"2":{"1":1,"7":1,"20":1,"26":1,"27":1,"51":1,"106":1,"136":1,"137":1,"140":1,"156":1,"157":2,"160":2}}],["k",{"2":{"1":1,"5":2,"6":3,"27":1,"36":1,"61":2,"63":7,"160":12}}],["hd",{"2":{"156":3}}],["hh",{"2":{"136":2,"137":3}}],["hundred",{"2":{"156":2}}],["hundredth",{"2":{"75":1}}],["humans",{"2":{"156":1}}],["human",{"2":{"16":1,"136":1,"137":1,"156":3}}],["href=",{"2":{"66":1,"156":5,"160":1}}],["htmlstyler",{"2":{"156":1,"160":13}}],["html",{"2":{"57":2,"156":1,"160":15}}],["https",{"2":{"19":1,"36":1,"66":2,"102":2,"156":13,"157":2,"158":1,"160":6}}],["http",{"2":{"9":2,"22":1,"27":1,"61":3,"63":2,"73":1,"100":2,"156":50,"158":1,"160":5}}],["hcat",{"2":{"45":1,"156":2}}],["hit",{"2":{"156":1,"160":2}}],["his",{"2":{"85":2,"156":2}}],["history",{"2":{"51":2,"84":1,"97":1,"156":86,"157":6,"160":1}}],["hint",{"2":{"51":2,"157":2}}],["hints",{"2":{"51":1,"157":1}}],["hi",{"2":{"20":2,"21":1,"22":3,"25":2,"26":1,"27":1,"28":3,"29":1,"30":1,"33":3,"36":1,"38":1,"39":1,"41":2,"50":2,"51":2,"83":3,"85":3,"92":1,"143":1,"156":17,"157":12}}],["highly",{"2":{"112":2,"117":1}}],["highlevel",{"2":{"51":2,"157":2}}],["highlighted",{"2":{"156":1}}],["highlights",{"0":{"48":1,"53":1,"56":1,"65":1}}],["highlighting",{"2":{"19":1,"57":1,"156":2}}],["highlight",{"2":{"9":1,"56":1,"63":1,"120":1,"136":1,"160":1}}],["higher",{"2":{"16":1,"51":1,"59":1,"63":2,"66":1,"156":2,"157":3,"160":4}}],["highest",{"2":{"6":1,"57":1,"160":1}}],["high",{"2":{"2":1,"23":3,"57":2,"59":2,"63":2,"99":1,"101":2,"141":1,"144":1,"151":1,"156":4,"160":6}}],["hmm",{"2":{"11":1,"40":1,"156":1}}],["hyderephraser",{"2":{"156":1,"160":3}}],["hyde",{"2":{"62":1,"115":1,"116":1,"160":3}}],["hypothetical",{"2":{"7":1,"115":3,"116":2,"160":2}}],["hybrid",{"2":{"7":1,"62":1}}],["her",{"2":{"106":1}}],["here>",{"2":{"146":1,"153":1}}],["here",{"2":{"22":1,"23":1,"25":1,"28":1,"29":1,"30":2,"31":1,"33":1,"40":1,"41":1,"51":1,"59":1,"66":1,"81":1,"101":1,"156":18,"157":1}}],["hence",{"2":{"94":1,"156":3}}],["heavily",{"2":{"106":1}}],["heavy",{"2":{"102":1}}],["heavens",{"2":{"66":1,"156":1}}],["healing",{"2":{"84":1,"100":1}}],["hear",{"2":{"66":2,"156":2}}],["hearty",{"2":{"85":2,"156":2}}],["heart",{"2":{"34":1}}],["header",{"2":{"156":3}}],["headers",{"2":{"1":1,"73":1,"156":3}}],["headlines",{"2":{"143":1}}],["head",{"2":{"40":1}}],["he",{"2":{"18":1,"156":3}}],["height",{"2":{"18":2,"156":8}}],["held",{"2":{"63":1,"160":1}}],["hello",{"2":{"11":1,"13":1,"21":1,"22":1,"25":1,"29":1,"30":1,"38":1,"39":1,"41":1,"51":3,"66":3,"83":1,"156":30,"157":1}}],["helping",{"2":{"160":1}}],["helpful",{"2":{"18":1,"22":1,"25":1,"33":1,"51":4,"52":1,"83":1,"93":1,"112":3,"113":1,"120":1,"141":1,"156":6,"157":2}}],["helpfulness",{"2":{"5":1,"112":1}}],["helps",{"2":{"18":1,"156":1}}],["help",{"2":{"11":1,"18":1,"20":1,"22":3,"25":3,"29":1,"30":1,"33":1,"34":1,"38":1,"39":1,"40":2,"41":1,"50":1,"63":1,"79":1,"102":1,"136":1,"156":7,"160":1}}],["helper",{"2":{"9":1,"23":1,"36":1,"100":1,"156":2}}],["haiku",{"2":{"156":3}}],["happened",{"2":{"121":1}}],["happens",{"2":{"94":1,"136":1}}],["happening",{"2":{"57":1}}],["having",{"0":{"72":2},"2":{"66":1,"99":1,"156":1}}],["have",{"0":{"83":1,"84":1,"86":1},"2":{"4":1,"5":1,"6":6,"9":2,"10":1,"11":5,"12":1,"14":1,"18":1,"20":2,"21":1,"22":5,"23":3,"25":4,"26":1,"27":2,"29":1,"30":2,"33":1,"34":2,"35":1,"36":1,"40":4,"41":1,"48":2,"50":1,"51":1,"56":1,"57":1,"59":3,"63":3,"65":1,"73":2,"75":1,"82":1,"84":1,"85":2,"90":2,"96":2,"97":1,"99":3,"100":3,"102":4,"106":1,"107":1,"109":1,"111":1,"116":1,"142":1,"143":1,"147":1,"148":1,"156":23,"157":3,"160":10}}],["half",{"2":{"63":1,"87":1,"160":1}}],["hallucination",{"2":{"16":1}}],["hackable",{"2":{"59":1}}],["had",{"2":{"23":1,"156":1}}],["hash",{"2":{"160":1}}],["hashed",{"2":{"63":5,"156":1,"160":9}}],["hasn",{"2":{"51":2,"157":2}}],["has",{"2":{"14":1,"20":1,"27":1,"41":1,"51":4,"61":1,"73":2,"84":1,"109":1,"120":1,"121":1,"136":1,"156":11,"157":2,"160":3}}],["harder",{"2":{"63":2,"160":2}}],["hard",{"2":{"12":1,"18":1,"40":1,"74":2,"91":1,"157":1}}],["handling",{"2":{"51":2,"84":1,"156":1,"157":1}}],["handles",{"2":{"84":1}}],["handlebars",{"2":{"92":1}}],["handlebar",{"2":{"11":1,"156":1}}],["handle",{"2":{"6":1,"57":1,"63":1,"84":2,"96":1,"102":1,"156":5,"160":2}}],["handcraft",{"2":{"2":1}}],["holding",{"2":{"160":1}}],["hold",{"2":{"157":1}}],["holds",{"2":{"1":1,"63":1,"102":1,"160":2}}],["hope",{"2":{"55":1,"56":1,"84":1,"101":1,"109":1}}],["honor",{"2":{"51":1,"157":1}}],["hosting",{"2":{"28":1,"81":1}}],["host",{"2":{"28":2,"156":6}}],["hosted",{"2":{"24":1,"63":4,"69":1,"94":1,"95":1,"102":2,"156":2,"160":4}}],["how",{"0":{"75":1,"83":1,"84":1,"85":1,"87":1,"93":1},"1":{"94":1,"95":1,"96":1,"97":1,"98":1,"99":1,"100":1,"101":1,"102":1},"2":{"6":1,"7":1,"9":1,"10":1,"12":4,"20":1,"21":1,"22":2,"23":4,"25":2,"29":1,"31":1,"35":1,"36":1,"50":1,"51":4,"57":7,"59":1,"63":1,"66":2,"70":4,"73":1,"82":1,"83":2,"84":3,"93":2,"97":1,"98":1,"101":1,"102":3,"112":4,"113":1,"146":1,"153":1,"156":27,"157":5,"160":3}}],["however",{"2":{"2":2,"14":1,"23":1,"160":1}}],["hood",{"2":{"1":1,"17":1,"22":1,"25":1,"27":1,"48":1,"57":1,"93":1,"101":2,"102":1,"156":2}}],["omit",{"2":{"138":1}}],["obj",{"2":{"102":3}}],["objective",{"2":{"116":1,"117":1}}],["objects",{"2":{"85":1,"94":1,"133":1,"156":20,"160":1}}],["object>",{"2":{"84":1}}],["object",{"2":{"5":1,"11":4,"20":2,"34":1,"50":2,"51":10,"62":1,"63":6,"84":1,"91":1,"94":1,"95":1,"101":1,"102":12,"156":31,"157":15,"160":8}}],["observe",{"2":{"59":1}}],["obtained",{"2":{"156":1}}],["obtain",{"2":{"51":1,"63":2,"101":1,"157":1,"160":2}}],["ocean",{"2":{"66":4,"156":4}}],["occur",{"2":{"137":1}}],["occurrences",{"2":{"66":1,"156":1}}],["occurred",{"2":{"51":2,"157":3}}],["occursin",{"2":{"157":2}}],["occurs",{"2":{"51":1,"156":1}}],["ocrtask",{"0":{"155":1},"2":{"19":2,"156":4}}],["ocr",{"0":{"19":1},"2":{"19":1,"155":1,"156":4}}],["olama",{"2":{"156":1}}],["ollamaschema",{"2":{"36":1,"41":5,"46":1,"82":3,"156":3}}],["ollamamanagedschema",{"2":{"21":1,"41":2,"156":12}}],["ollama",{"0":{"21":1,"36":1,"81":1},"1":{"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1},"2":{"21":4,"27":1,"36":4,"41":1,"68":3,"69":1,"75":1,"80":1,"81":8,"82":2,"94":1,"95":1,"156":22}}],["oldest",{"2":{"156":1}}],["old",{"2":{"11":3}}],["overload",{"2":{"160":1}}],["overlaps",{"2":{"160":2}}],["overlap",{"2":{"63":1,"160":1}}],["overwrite",{"2":{"156":1}}],["overwritten",{"2":{"51":1,"157":1}}],["overall",{"2":{"112":2}}],["overarching",{"2":{"59":1}}],["overriden",{"2":{"156":1}}],["overrides",{"2":{"156":4}}],["override",{"2":{"51":2,"82":1,"157":2}}],["overhead",{"2":{"27":1}}],["over",{"2":{"18":1,"106":1,"111":1,"136":1,"137":1,"140":1,"146":1,"147":1,"148":1,"153":1,"156":5,"159":1}}],["overview",{"0":{"9":1,"59":1,"100":1}}],["o",{"2":{"17":2,"84":4,"156":4}}],["op",{"2":{"160":8}}],["opposite",{"2":{"160":1}}],["opposed",{"2":{"10":1}}],["opportunity",{"2":{"109":1}}],["opt",{"2":{"70":1}}],["option",{"2":{"156":2,"160":2}}],["options",{"2":{"51":1,"59":1,"63":1,"73":1,"82":1,"89":1,"157":1,"160":2}}],["options=",{"2":{"36":2}}],["optional",{"2":{"1":2,"9":3,"51":2,"57":1,"62":1,"63":1,"100":3,"156":30,"157":2,"160":1}}],["optionally",{"2":{"1":1,"63":1,"66":1,"85":1,"102":1,"156":3,"160":2}}],["optimized",{"2":{"117":2,"157":1}}],["optimizes",{"2":{"115":1}}],["optimize",{"2":{"20":1,"157":1}}],["operate",{"2":{"20":1,"50":1,"51":1,"63":1,"157":1,"160":1}}],["operations",{"2":{"13":1,"51":2,"63":2,"156":3,"160":2}}],["operation",{"2":{"6":7,"156":1,"157":1,"160":3}}],["opens",{"2":{"160":2}}],["opened",{"2":{"73":1}}],["opentagger",{"2":{"63":1,"156":1,"160":4}}],["opening",{"2":{"23":1}}],["openhermes2",{"2":{"21":3,"36":1,"39":1,"46":1,"81":2,"156":10}}],["open",{"0":{"80":1},"2":{"10":1,"22":1,"25":1,"51":1,"56":1,"57":1,"65":1,"72":1,"80":1,"81":1,"102":2,"156":1,"157":1,"160":1}}],["openaischema",{"2":{"95":1,"96":3,"101":3,"156":33}}],["openaiapi",{"0":{"72":1}}],["openai",{"0":{"22":1,"26":1,"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1},"1":{"69":1},"2":{"9":1,"15":1,"20":1,"22":3,"24":1,"25":1,"26":2,"41":1,"50":1,"51":1,"68":1,"70":6,"71":4,"72":3,"73":2,"74":3,"75":2,"76":6,"77":2,"78":1,"89":9,"94":2,"95":3,"96":3,"100":1,"101":6,"102":1,"106":1,"156":60,"157":1}}],["oriented",{"2":{"142":2}}],["orientation",{"2":{"129":1,"131":1}}],["original",{"2":{"62":1,"66":4,"109":6,"112":1,"117":1,"118":1,"120":1,"155":1,"156":5,"157":1,"160":6}}],["organization",{"2":{"156":1}}],["organize",{"2":{"120":1,"136":2,"137":1}}],["org",{"2":{"66":1,"156":1,"157":2}}],["ordered",{"2":{"66":1,"156":1}}],["ordering",{"2":{"62":1,"157":4}}],["orders",{"2":{"19":1,"156":2}}],["order",{"2":{"5":1,"66":1,"70":1,"156":1}}],["or",{"0":{"82":1},"2":{"4":3,"5":3,"6":6,"7":1,"9":8,"10":3,"12":1,"16":2,"17":4,"18":1,"19":1,"20":4,"22":3,"23":3,"25":3,"28":1,"29":1,"30":1,"33":1,"36":2,"38":1,"40":1,"41":2,"42":1,"45":1,"48":2,"50":3,"51":22,"54":1,"55":1,"56":1,"59":5,"60":3,"62":2,"63":18,"65":1,"66":8,"68":2,"70":1,"72":2,"81":1,"82":1,"84":3,"85":2,"89":1,"91":1,"94":2,"95":2,"97":2,"98":2,"99":2,"100":8,"106":1,"111":2,"117":2,"120":4,"121":1,"127":1,"136":2,"137":4,"143":1,"146":2,"147":1,"148":1,"149":1,"153":2,"156":118,"157":23,"158":1,"160":39}}],["ongoing",{"2":{"156":1}}],["online",{"2":{"71":1,"89":1,"156":4}}],["only",{"2":{"1":1,"6":8,"7":1,"9":3,"11":1,"17":1,"20":6,"22":2,"23":5,"26":2,"31":1,"41":1,"48":2,"50":5,"51":15,"53":1,"57":1,"62":1,"63":2,"66":2,"73":1,"82":1,"85":1,"99":1,"100":4,"101":3,"102":2,"104":1,"106":1,"109":3,"111":1,"117":1,"120":1,"126":1,"136":3,"137":1,"138":1,"141":1,"143":1,"144":1,"149":1,"151":1,"156":53,"157":23,"160":8}}],["on",{"2":{"6":4,"9":1,"10":1,"12":3,"16":2,"18":1,"20":3,"21":1,"22":2,"23":2,"25":1,"29":1,"36":1,"41":1,"46":1,"50":3,"51":11,"56":1,"57":7,"59":1,"62":5,"63":7,"66":4,"70":1,"71":1,"72":2,"73":1,"74":1,"76":2,"85":3,"89":1,"99":1,"100":1,"102":6,"104":2,"106":1,"109":1,"111":1,"112":3,"113":3,"120":3,"121":2,"122":1,"137":2,"143":2,"147":1,"148":1,"149":2,"156":40,"157":22,"160":13}}],["once",{"2":{"5":1,"6":1,"55":1,"81":1,"87":1,"101":1,"156":7}}],["ones",{"2":{"22":1,"23":1,"26":1,"63":2,"69":1,"106":1,"160":2}}],["one",{"0":{"4":1,"44":1},"2":{"4":1,"5":2,"9":1,"11":1,"12":1,"14":1,"20":1,"23":1,"24":1,"29":1,"30":1,"40":1,"41":1,"42":1,"44":1,"50":1,"51":4,"53":1,"57":1,"59":1,"61":1,"62":1,"63":1,"73":1,"75":2,"76":1,"84":2,"85":1,"99":1,"100":1,"121":1,"126":2,"138":2,"146":1,"153":1,"156":28,"157":5,"160":5}}],["our",{"2":{"2":1,"5":1,"6":2,"20":1,"36":1,"50":1,"51":3,"70":4,"73":2,"84":6,"87":1,"101":2,"102":3,"149":1,"156":4,"157":3,"160":3}}],["outside",{"2":{"149":1}}],["outlined",{"2":{"129":1,"131":1}}],["outline",{"2":{"120":1}}],["outcomes",{"2":{"51":1,"84":1,"157":2}}],["outcome",{"2":{"48":1,"149":5,"156":2,"157":3}}],["outer",{"2":{"5":1,"6":4}}],["outerjoin",{"2":{"5":1}}],["output`",{"2":{"20":1,"50":1,"51":1,"157":1}}],["outputs",{"2":{"6":1,"19":1,"20":2,"23":1,"50":1,"51":3,"102":1,"156":19}}],["output",{"0":{"20":1},"2":{"5":2,"6":8,"9":3,"20":4,"30":1,"48":4,"50":9,"51":42,"57":2,"63":1,"66":4,"84":7,"85":2,"98":3,"100":3,"101":1,"102":17,"129":1,"131":1,"134":1,"138":2,"156":42,"157":32,"160":4}}],["out",{"2":{"1":1,"9":1,"20":9,"50":5,"51":36,"55":1,"63":4,"70":1,"73":1,"87":1,"100":1,"101":1,"118":1,"156":2,"157":35,"160":7}}],["own",{"2":{"1":1,"14":1,"51":1,"55":1,"63":3,"156":2,"157":1,"160":4}}],["otherwise",{"2":{"51":2,"73":1,"156":18,"157":4,"160":1}}],["others",{"2":{"34":1,"102":1,"136":1}}],["other",{"0":{"22":1,"26":1},"2":{"1":1,"22":2,"26":1,"31":1,"34":1,"35":1,"48":1,"51":4,"57":1,"63":3,"66":1,"68":1,"69":1,"72":1,"76":1,"82":1,"85":2,"89":1,"95":1,"106":1,"137":2,"138":1,"145":1,"152":1,"156":9,"157":4,"160":4}}],["office",{"2":{"143":1}}],["offloaded",{"2":{"36":1}}],["offload",{"2":{"27":1}}],["off",{"2":{"14":1,"76":1,"156":1}}],["offering",{"2":{"70":1}}],["offers",{"2":{"20":1}}],["offer",{"2":{"12":1,"14":1}}],["often",{"2":{"4":1,"5":1,"6":2,"10":1,"28":1,"63":2,"156":15,"160":4}}],["of",{"0":{"50":1},"2":{"1":5,"2":3,"3":1,"4":4,"5":5,"6":28,"7":1,"9":7,"10":2,"11":3,"12":4,"13":1,"14":2,"15":1,"17":1,"18":4,"19":4,"20":5,"22":4,"23":15,"25":4,"27":1,"28":1,"29":1,"30":3,"31":1,"34":1,"35":2,"36":1,"40":1,"41":1,"44":2,"45":1,"46":1,"47":1,"48":8,"50":5,"51":35,"54":4,"55":3,"56":7,"57":13,"59":7,"62":4,"63":58,"64":2,"65":4,"66":29,"68":2,"70":2,"71":1,"72":2,"73":6,"74":2,"75":4,"80":1,"84":5,"85":2,"87":5,"90":2,"91":7,"92":6,"93":2,"94":3,"95":2,"96":3,"97":1,"98":2,"99":4,"100":6,"101":8,"102":15,"106":1,"111":4,"112":3,"117":1,"120":10,"121":2,"126":2,"136":8,"137":6,"138":2,"141":1,"143":1,"144":2,"145":1,"146":7,"147":2,"148":1,"149":4,"151":2,"152":1,"153":7,"156":294,"157":76,"158":4,"160":156}}],["wp",{"2":{"19":1,"156":2}}],["www",{"2":{"19":1,"156":2}}],["wraps",{"2":{"65":1,"160":1}}],["wrap",{"2":{"65":2,"66":2,"156":3}}],["wrapped",{"2":{"157":1}}],["wrapper",{"2":{"18":1,"48":1,"51":3,"63":1,"66":2,"91":1,"156":12,"157":3,"160":2}}],["wrapping",{"2":{"52":1,"65":1}}],["wrong",{"2":{"50":1,"51":1,"84":1,"157":1}}],["written",{"2":{"19":1,"156":2}}],["writing",{"2":{"12":1,"30":1,"35":1,"70":1,"145":1,"146":3,"152":1,"153":3,"156":2}}],["write",{"2":{"3":1,"10":1,"23":2,"98":1,"99":1,"102":4,"116":1,"120":2,"121":1,"143":1,"145":1,"146":2,"152":1,"153":2,"156":1,"157":2}}],["won",{"2":{"26":1,"27":1,"82":1}}],["wonders",{"2":{"7":1}}],["worth",{"0":{"75":1},"2":{"75":1}}],["worst",{"2":{"66":1,"156":1}}],["worry",{"2":{"36":1}}],["words",{"2":{"57":1,"63":1,"65":5,"66":13,"86":2,"106":1,"107":1,"115":1,"116":1,"143":2,"156":14,"160":2}}],["word",{"2":{"19":1,"20":4,"50":4,"51":3,"63":2,"65":1,"75":1,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":3,"121":3,"122":1,"124":1,"126":2,"127":1,"129":1,"131":1,"133":1,"134":1,"136":1,"137":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"151":1,"152":1,"153":1,"155":1,"156":2,"157":3,"160":9}}],["wordcount",{"2":{"12":1,"23":1,"85":1,"156":3}}],["world",{"2":{"11":1,"12":1,"13":1,"23":4,"51":3,"66":3,"98":1,"99":1,"101":2,"102":1,"104":1,"106":2,"109":1,"111":1,"115":1,"116":1,"117":1,"126":1,"129":1,"131":1,"134":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"151":1,"152":1,"153":1,"155":1,"156":21,"157":1}}],["workaround",{"2":{"102":1}}],["workload",{"2":{"57":1}}],["workspace",{"2":{"156":2}}],["works",{"0":{"93":1},"1":{"94":1,"95":1,"96":1,"97":1,"98":1,"99":1,"100":1,"101":1,"102":1},"2":{"22":2,"25":1,"66":1,"76":1,"93":1,"101":1,"102":2,"156":3,"160":1}}],["workflows",{"0":{"20":1},"2":{"15":1,"20":1,"47":1,"82":1,"100":1,"159":1}}],["workflow",{"0":{"10":1},"2":{"48":1,"51":1,"156":1}}],["working",{"0":{"31":1},"1":{"32":1,"33":1,"34":1,"35":1},"2":{"9":1,"21":1,"52":1,"61":1,"64":1,"74":1,"81":1,"82":1,"100":1,"120":1,"121":1,"122":1,"156":1}}],["work",{"2":{"6":1,"10":1,"23":1,"31":1,"36":1,"51":1,"59":1,"84":1,"97":1,"157":1}}],["would",{"0":{"7":1,"87":1},"2":{"2":1,"3":1,"6":2,"12":1,"23":3,"38":1,"57":1,"59":1,"61":2,"72":1,"73":2,"83":1,"84":2,"85":1,"102":1,"115":1,"116":1,"156":3,"160":3}}],["walkthrough",{"0":{"101":1,"102":1}}],["walk",{"2":{"93":1}}],["wave",{"2":{"66":1,"156":1}}],["wake",{"2":{"66":1,"156":1}}],["warning",{"2":{"156":2,"160":1}}],["warnings",{"2":{"51":1,"157":1}}],["wars",{"2":{"11":1,"34":1,"40":1,"156":5}}],["waiting",{"2":{"102":1}}],["wait",{"2":{"20":1,"50":1,"51":2,"156":2,"157":2}}],["way",{"2":{"20":1,"23":1,"28":1,"48":1,"51":1,"59":1,"75":1,"76":1,"83":1,"99":1,"116":1,"146":1,"153":1,"156":1,"157":1,"160":1}}],["ways",{"2":{"11":1,"40":1,"82":1,"156":1}}],["was",{"2":{"6":1,"7":1,"9":1,"23":2,"46":1,"51":2,"59":1,"63":1,"66":1,"72":1,"75":1,"84":1,"100":1,"102":1,"111":1,"113":1,"120":1,"149":2,"156":4,"157":4,"160":2}}],["wanted",{"2":{"51":1,"156":1,"157":1}}],["wants",{"2":{"14":1}}],["want",{"2":{"1":1,"2":1,"6":2,"9":3,"10":1,"18":1,"20":1,"23":1,"50":1,"51":2,"57":2,"59":1,"62":2,"63":2,"73":2,"80":1,"84":2,"85":3,"91":1,"94":1,"97":1,"99":1,"100":3,"101":1,"156":26,"157":2,"160":2}}],["whose",{"2":{"156":2}}],["who",{"2":{"26":1,"54":2,"138":1,"158":2}}],["whole",{"0":{"6":1},"2":{"9":1,"36":1,"83":2,"97":1,"100":1,"146":1,"153":1,"156":5,"157":1}}],["while",{"2":{"120":1,"146":1,"153":1,"156":1,"157":1}}],["whispered",{"2":{"66":4,"156":4}}],["white",{"2":{"20":1,"50":1,"51":1,"66":1,"156":6,"157":1}}],["whichever",{"2":{"45":1}}],["which",{"2":{"6":3,"9":3,"16":1,"20":1,"23":1,"27":1,"33":1,"36":2,"41":1,"48":4,"51":4,"56":2,"57":2,"59":1,"62":1,"63":5,"66":2,"73":1,"80":1,"85":1,"86":1,"87":1,"94":1,"95":1,"100":3,"101":1,"102":2,"111":2,"136":1,"146":1,"153":1,"156":33,"157":5,"160":17}}],["why",{"0":{"68":1},"1":{"69":1},"2":{"12":1,"18":1,"51":2,"102":1,"120":1,"121":1,"156":2,"157":1}}],["when",{"2":{"9":4,"10":1,"16":1,"20":2,"21":1,"23":4,"27":1,"48":5,"50":1,"51":2,"57":1,"59":3,"62":1,"72":1,"81":1,"82":1,"84":1,"85":1,"94":1,"98":2,"99":2,"100":4,"101":2,"102":3,"134":1,"136":2,"137":1,"141":1,"144":1,"145":3,"151":1,"152":1,"156":15,"157":8,"160":3}}],["whereas",{"2":{"6":1,"102":1}}],["where",{"2":{"6":1,"9":1,"11":1,"41":1,"48":1,"51":5,"57":1,"63":3,"65":1,"66":1,"72":1,"73":1,"76":1,"84":1,"89":1,"99":1,"100":1,"102":1,"115":1,"116":1,"137":2,"143":1,"156":17,"157":8,"160":8}}],["whether",{"2":{"6":4,"9":3,"16":1,"51":2,"54":3,"63":4,"100":3,"102":1,"127":2,"156":17,"157":8,"158":3,"160":9}}],["whatever",{"2":{"51":1,"80":1,"157":1}}],["what",{"0":{"7":1,"69":1},"2":{"1":3,"4":1,"5":1,"6":5,"10":1,"11":2,"12":3,"14":1,"18":1,"20":1,"23":1,"34":1,"39":1,"40":1,"50":1,"51":2,"57":4,"63":4,"66":1,"83":3,"84":1,"91":4,"92":1,"94":1,"101":3,"111":1,"120":4,"134":1,"156":21,"157":2,"160":8}}],["welcome",{"2":{"160":1}}],["well",{"2":{"1":1,"20":1,"28":1,"30":1,"50":1,"51":1,"66":1,"102":4,"112":1,"147":1,"148":1,"156":3,"157":2,"160":3}}],["weaving",{"2":{"149":1}}],["weather",{"2":{"18":3}}],["web",{"2":{"57":2}}],["websearch",{"2":{"53":1,"54":4,"156":1,"158":5}}],["website",{"2":{"10":1,"71":1}}],["were",{"2":{"19":1,"36":1,"61":1,"137":1,"156":1}}],["weighs",{"2":{"18":1,"156":3}}],["weight",{"2":{"18":2,"156":8}}],["went",{"2":{"9":1,"100":1}}],["we",{"0":{"7":1,"86":1},"2":{"1":1,"2":3,"3":1,"4":3,"5":4,"6":8,"7":1,"9":6,"10":2,"11":1,"12":1,"14":1,"16":1,"17":2,"18":1,"19":1,"20":13,"21":2,"22":5,"23":3,"25":3,"26":1,"29":1,"30":1,"31":2,"35":2,"36":1,"41":1,"44":1,"50":13,"51":24,"55":1,"57":5,"59":1,"61":2,"62":2,"63":4,"66":1,"70":2,"73":5,"83":2,"84":14,"85":2,"86":1,"90":1,"93":1,"96":2,"97":1,"99":4,"100":4,"101":9,"102":23,"156":46,"157":22,"160":9}}],["wiki",{"2":{"157":2}}],["wikipedia",{"2":{"157":2,"160":1}}],["wisp",{"2":{"66":1,"156":1}}],["width",{"2":{"65":1,"66":3,"156":11,"160":5}}],["wide",{"2":{"29":1,"30":1,"51":1,"157":1}}],["wins",{"2":{"157":9}}],["winning",{"2":{"51":2,"157":2}}],["winks",{"2":{"40":2}}],["win",{"2":{"10":1}}],["windows",{"2":{"57":1,"66":2,"76":1,"156":2}}],["window",{"2":{"1":1,"66":2,"76":1,"89":1,"156":2,"157":1,"160":5}}],["will",{"2":{"0":2,"1":2,"3":1,"9":1,"12":2,"17":2,"18":1,"20":2,"23":1,"27":1,"34":1,"40":2,"48":1,"50":1,"51":23,"57":1,"59":1,"61":1,"63":21,"66":5,"68":1,"73":1,"74":1,"75":2,"76":1,"83":2,"85":4,"89":2,"90":1,"91":1,"96":4,"100":1,"102":1,"107":1,"120":1,"126":1,"145":1,"146":1,"147":2,"148":2,"152":1,"153":1,"156":52,"157":35,"160":33}}],["without",{"2":{"40":1,"51":1,"59":1,"66":1,"72":1,"99":1,"111":1,"133":1,"137":1,"149":1,"156":3,"160":3}}],["within",{"2":{"9":1,"63":1,"66":2,"100":1,"111":1,"156":9,"157":2,"160":4}}],["with",{"0":{"0":1,"20":1,"31":1,"32":1,"36":1,"37":1,"42":1,"43":1,"91":1,"92":1},"1":{"1":1,"32":1,"33":2,"34":2,"35":2,"37":1,"38":2,"39":2,"40":2,"41":2,"42":1,"43":1,"44":2,"45":2,"46":2},"2":{"0":3,"1":1,"5":2,"6":4,"7":1,"9":12,"10":2,"11":2,"12":4,"13":1,"14":1,"17":1,"18":5,"19":2,"20":5,"21":2,"22":5,"23":11,"25":2,"26":4,"27":2,"28":1,"29":3,"30":4,"31":1,"33":2,"34":1,"35":1,"36":2,"38":2,"39":1,"40":1,"41":4,"44":1,"45":1,"48":5,"50":6,"51":27,"52":1,"55":3,"56":1,"57":10,"59":3,"60":4,"61":1,"62":2,"63":22,"64":2,"66":8,"68":1,"70":1,"71":2,"72":1,"73":2,"75":4,"76":1,"81":3,"82":2,"83":4,"84":5,"85":2,"87":3,"89":3,"91":2,"93":1,"94":2,"95":1,"98":3,"99":7,"100":11,"101":2,"102":9,"104":1,"106":1,"109":2,"112":2,"113":1,"115":1,"116":1,"117":2,"118":1,"120":3,"121":2,"122":1,"124":1,"126":3,"129":1,"131":1,"134":1,"136":2,"137":3,"138":2,"143":3,"144":1,"146":2,"147":1,"148":1,"149":4,"151":1,"153":2,"156":93,"157":50,"160":52}}],["europe",{"2":{"160":4}}],["eyes",{"2":{"156":1}}],["educational",{"2":{"101":1}}],["editing",{"2":{"10":1}}],["ethos",{"2":{"80":1}}],["ethereal",{"2":{"66":1,"156":1}}],["etc",{"2":{"1":1,"14":1,"20":1,"23":1,"50":1,"51":2,"63":1,"68":1,"98":1,"100":1,"106":2,"109":1,"142":1,"155":1,"156":9,"157":2,"160":11}}],["echoes",{"2":{"156":5}}],["echoing",{"2":{"66":1,"156":1}}],["ecosystem",{"2":{"115":1}}],["econometrics",{"2":{"57":1}}],["e2e",{"2":{"60":1}}],["equality",{"2":{"84":1}}],["equal",{"2":{"51":1,"54":1,"63":1,"66":2,"156":2,"157":1,"158":1,"160":3}}],["equivalent",{"2":{"1":2,"12":1,"57":1,"100":1,"156":1}}],["essence",{"2":{"137":1}}],["essential",{"2":{"112":1,"157":2}}],["estimated",{"2":{"92":1}}],["estimate",{"2":{"35":1,"156":1}}],["especially",{"2":{"21":1,"51":1,"65":1,"81":1,"82":1,"101":1,"138":1,"157":2}}],["elapsed",{"2":{"156":21}}],["elaboration",{"2":{"111":1}}],["elicit",{"2":{"134":1}}],["else`",{"2":{"147":1,"148":1}}],["elseif",{"2":{"147":1,"148":1}}],["else",{"2":{"17":2,"40":1,"51":2,"84":2,"109":2,"156":4,"157":2}}],["element",{"2":{"12":2,"15":1,"18":1,"21":1,"23":3,"46":1,"83":1,"85":2,"101":2,"156":12}}],["evolving",{"2":{"157":1}}],["evolved",{"2":{"137":1}}],["ever",{"2":{"156":2}}],["every",{"2":{"59":1,"73":1,"75":1,"76":1,"82":1,"97":1,"155":1,"157":1}}],["everything",{"2":{"17":2,"84":1,"156":4}}],["even",{"2":{"18":2,"28":1,"29":1,"50":1,"51":2,"54":1,"61":1,"66":1,"136":1,"156":5,"157":1,"158":1}}],["eval=false",{"2":{"51":2,"156":2}}],["evalutes",{"2":{"157":1}}],["evaluted",{"2":{"51":1,"157":1}}],["evaluator",{"2":{"124":2,"157":3}}],["evaluating",{"2":{"16":1,"51":2,"111":1,"127":1,"156":2,"160":1}}],["evaluation",{"0":{"110":1},"1":{"111":1,"112":1,"113":1},"2":{"3":1,"5":2,"7":1,"9":1,"48":2,"51":10,"56":1,"100":1,"112":1,"113":1,"156":10,"157":10,"160":9}}],["evaluations",{"0":{"2":1},"1":{"3":1,"4":1,"5":1,"6":1},"2":{"51":3,"63":2,"157":5,"160":4}}],["evaluated",{"2":{"51":8,"120":1,"121":1,"156":9,"157":3}}],["evaluates",{"2":{"20":1,"50":1,"51":3,"156":3,"157":2,"160":2}}],["evaluate",{"0":{"5":1,"6":1},"2":{"2":1,"5":2,"6":2,"20":1,"50":1,"51":9,"112":2,"156":4,"157":20}}],["eval",{"2":{"6":2,"23":1,"51":9,"156":13,"160":1}}],["evals",{"2":{"3":6,"4":1,"5":3,"6":7,"56":1,"63":4,"156":3,"160":18}}],["effective",{"2":{"116":1,"117":1,"134":1}}],["effectively",{"2":{"11":1,"61":2,"65":1,"97":1,"101":2,"156":5,"157":1}}],["efficiently",{"2":{"57":1,"157":1}}],["efficient",{"2":{"51":2,"57":1,"64":1,"143":2,"157":2,"160":1}}],["effort",{"2":{"12":1,"55":1}}],["emphasize",{"2":{"147":1,"148":1}}],["empty",{"0":{"72":2},"2":{"51":2,"54":2,"63":2,"66":6,"156":35,"157":1,"158":2,"160":2}}],["email",{"2":{"143":7}}],["emotions",{"2":{"66":2,"156":2}}],["emotional",{"2":{"11":1}}],["emb",{"2":{"57":1,"160":8}}],["embedder",{"2":{"63":17,"160":24}}],["embedded",{"2":{"45":1,"63":1,"160":4}}],["embedding",{"0":{"44":1,"45":1},"2":{"1":1,"7":2,"15":1,"29":1,"30":1,"45":2,"62":1,"63":8,"156":8,"160":24}}],["embeddings",{"0":{"15":1,"43":1},"1":{"44":1,"45":1,"46":1},"2":{"1":1,"9":2,"15":3,"46":1,"60":2,"62":4,"63":7,"98":1,"100":2,"156":18,"160":22}}],["embeds",{"2":{"1":1,"63":1,"160":2}}],["embed",{"2":{"1":3,"15":3,"21":3,"22":1,"25":1,"29":1,"30":2,"44":2,"45":4,"46":1,"156":4}}],["either",{"2":{"9":1,"18":1,"22":1,"25":1,"41":1,"54":1,"59":1,"73":1,"84":1,"100":1,"156":6,"157":1,"158":1}}],["e",{"2":{"9":1,"18":1,"51":4,"100":1,"102":2,"126":1,"156":9,"157":2}}],["errorexception",{"2":{"51":1,"157":1}}],["errors",{"2":{"18":1,"20":1,"21":1,"48":1,"50":1,"51":7,"63":1,"102":1,"120":2,"122":1,"156":3,"157":10,"160":3}}],["error",{"0":{"72":2,"73":1},"2":{"6":2,"48":2,"51":13,"72":2,"73":3,"84":1,"102":4,"120":2,"121":1,"156":19,"157":14}}],["enforces",{"2":{"156":2}}],["enforce",{"2":{"156":4}}],["encapsulates",{"2":{"157":1}}],["encapsulated",{"2":{"157":1}}],["encouraging",{"2":{"143":1}}],["encode",{"2":{"102":2,"156":7}}],["encoded",{"2":{"9":1,"98":1,"100":1,"156":3}}],["enclosed",{"2":{"121":1}}],["enclose",{"2":{"120":1}}],["enhance",{"2":{"117":1,"134":1}}],["enhancing",{"2":{"52":1,"137":1,"156":1}}],["enigmatic",{"2":{"66":2,"156":2}}],["enough",{"2":{"66":2,"156":2}}],["ensuring",{"2":{"66":1,"134":1,"155":1,"156":1}}],["ensures",{"2":{"66":1,"156":1}}],["ensure",{"2":{"51":2,"84":3,"102":1,"117":2,"120":2,"129":1,"131":1,"136":1,"137":1,"143":1,"156":4,"157":2,"160":2}}],["enabling",{"2":{"62":1,"157":1}}],["enable",{"2":{"102":1}}],["enables",{"2":{"9":1,"48":1,"63":2,"100":1,"160":7}}],["enabled",{"2":{"9":1,"63":1,"100":1,"160":2}}],["enjoy",{"2":{"40":1}}],["en",{"2":{"29":1,"157":2}}],["engaging",{"2":{"149":1}}],["engage",{"2":{"29":1}}],["english",{"2":{"63":1,"160":3}}],["engineer",{"2":{"134":1}}],["engineering",{"2":{"16":1,"134":1}}],["engine",{"2":{"53":1,"102":1,"106":2,"155":1}}],["environments",{"2":{"51":1,"157":1}}],["environment",{"0":{"76":1},"2":{"31":1,"53":1,"57":1,"72":1,"76":2,"89":2,"156":4}}],["env",{"2":{"22":1,"25":1,"28":2,"29":1,"30":1,"72":3,"76":1,"89":2,"156":6}}],["enumerates",{"2":{"160":4}}],["enumerated",{"2":{"156":2}}],["enumerate",{"2":{"63":1,"160":1}}],["enum",{"2":{"18":1,"84":2}}],["entire",{"2":{"156":12}}],["entities",{"2":{"11":1,"18":1,"66":1,"106":1,"156":1}}],["entry",{"2":{"59":1,"63":1,"133":1,"160":1}}],["entries",{"2":{"5":1,"102":2,"156":2}}],["enter",{"2":{"23":2}}],["ended",{"2":{"156":1,"160":1}}],["end|>",{"2":{"156":2}}],["end>",{"2":{"156":1}}],["ending",{"2":{"156":1}}],["end`",{"2":{"120":1,"146":2,"147":1,"148":1,"153":2}}],["endpoint",{"2":{"63":2,"156":9,"158":1,"160":6}}],["end",{"2":{"6":1,"18":3,"20":1,"30":1,"33":1,"45":1,"50":1,"51":11,"57":2,"59":1,"63":4,"66":1,"73":1,"84":6,"87":2,"102":5,"146":1,"153":1,"156":14,"157":15,"160":5}}],["earlier",{"2":{"72":1}}],["eating",{"2":{"30":1,"102":1}}],["easiest",{"2":{"80":1,"91":1}}],["easier",{"2":{"6":1,"28":1,"64":1,"66":1,"100":1,"136":1,"156":1}}],["easily",{"2":{"14":1,"56":1,"84":1,"87":1,"91":1,"156":2}}],["easy",{"2":{"5":1,"46":1,"76":1,"79":1,"81":1,"89":1,"102":1,"133":1,"137":1,"156":5}}],["each",{"2":{"1":2,"3":1,"6":4,"9":2,"11":1,"14":1,"20":2,"50":2,"51":3,"57":1,"59":2,"61":1,"62":2,"63":20,"66":12,"75":1,"100":1,"106":1,"107":1,"112":1,"120":1,"136":3,"137":6,"138":4,"143":1,"146":2,"149":2,"153":2,"156":24,"157":6,"160":46}}],["eg",{"2":{"1":1,"5":1,"6":2,"7":1,"9":9,"11":2,"12":1,"14":2,"20":6,"22":2,"23":1,"24":1,"26":2,"27":1,"36":1,"41":1,"50":5,"51":10,"56":1,"57":3,"59":6,"60":1,"61":1,"62":3,"63":15,"65":4,"66":3,"68":3,"73":3,"75":2,"76":2,"81":1,"82":2,"84":2,"85":1,"91":2,"92":1,"94":3,"95":3,"96":1,"97":1,"98":3,"99":2,"100":10,"102":3,"106":1,"143":2,"146":1,"153":1,"156":36,"157":15,"159":2,"160":31}}],["exit",{"2":{"23":1}}],["existing",{"2":{"23":1,"59":1,"63":1,"109":1,"156":3,"160":1}}],["existent",{"2":{"20":1,"50":1,"51":1,"157":1}}],["exists",{"2":{"6":2,"160":1}}],["exist",{"2":{"5":1,"6":6}}],["exclude",{"2":{"54":2,"137":1,"156":1,"158":2}}],["exciting",{"2":{"22":1,"26":1}}],["exceed",{"2":{"73":1,"156":2}}],["exceeds",{"2":{"66":1,"156":1}}],["exceeding",{"2":{"63":1,"66":1,"156":1,"160":3}}],["exceeded",{"0":{"73":1},"2":{"20":1,"50":1,"73":2}}],["excessive",{"2":{"57":1}}],["exception",{"2":{"51":4,"59":1,"102":1,"156":3,"157":2}}],["except",{"2":{"20":1,"156":1,"157":1}}],["extremely",{"2":{"112":2,"121":1}}],["extras",{"2":{"63":2,"160":4}}],["extra",{"2":{"27":1,"63":1,"66":2,"84":1,"91":1,"137":1,"156":1,"160":1}}],["extractdata",{"0":{"131":1}}],["extractdataxml",{"0":{"129":1}}],["extracted",{"2":{"106":3,"156":16}}],["extractor",{"2":{"102":1,"156":1}}],["extraction",{"0":{"18":1,"130":1},"1":{"131":1},"2":{"30":1,"63":4,"102":1,"106":2,"129":2,"131":2,"156":9,"160":6}}],["extracting",{"2":{"1":1,"18":3,"63":1,"156":1,"160":1}}],["extracts",{"2":{"1":1,"48":1,"62":2,"63":2,"106":1,"107":1,"156":3,"157":1,"160":4}}],["extract",{"2":{"1":2,"7":1,"9":2,"18":3,"30":2,"51":3,"100":2,"101":1,"102":10,"106":4,"129":1,"131":1,"156":40,"157":4,"160":6}}],["external",{"2":{"111":1,"156":1}}],["extension",{"2":{"156":2,"160":1}}],["extensions",{"2":{"48":1,"55":1}}],["extensive",{"2":{"147":1}}],["extensible",{"2":{"51":1,"56":1,"59":1,"157":1}}],["extended",{"2":{"51":1,"156":1,"157":1}}],["extend",{"2":{"14":1,"22":1,"55":1}}],["executor",{"2":{"51":1,"156":1}}],["execute",{"2":{"51":3,"156":2,"157":1}}],["executed",{"2":{"48":1,"51":4,"121":1,"156":4,"157":2}}],["executes",{"2":{"9":1,"48":2,"51":2,"100":1,"156":2,"157":4}}],["execution",{"0":{"13":1},"2":{"9":1,"48":2,"51":11,"100":1,"121":1,"122":1,"156":6,"157":6}}],["exact",{"2":{"63":2,"156":4,"160":9}}],["exactly",{"2":{"9":2,"20":1,"23":1,"48":1,"51":1,"63":1,"100":2,"157":5,"160":2}}],["example>",{"2":{"153":2}}],["example",{"0":{"101":1,"102":1},"2":{"1":1,"4":1,"6":1,"9":1,"12":1,"14":1,"16":1,"17":1,"20":1,"23":3,"30":1,"36":1,"41":1,"48":1,"51":6,"54":1,"57":1,"61":1,"63":5,"66":3,"75":2,"84":3,"86":1,"87":1,"93":1,"96":1,"99":1,"100":2,"102":4,"106":1,"111":1,"138":4,"146":1,"156":39,"157":10,"158":1,"160":8}}],["examples",{"0":{"8":1,"49":1,"57":1},"1":{"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":1,"21":1,"22":1,"50":1},"2":{"1":5,"3":2,"4":1,"5":1,"12":2,"20":1,"23":1,"31":1,"36":1,"51":2,"57":2,"63":9,"66":3,"69":1,"87":1,"92":2,"106":1,"138":2,"145":1,"152":1,"156":18,"157":1,"160":12}}],["expr",{"2":{"156":4}}],["expression",{"2":{"51":6,"156":8}}],["export",{"2":{"76":1,"89":1}}],["exported",{"2":{"23":1,"36":1,"51":1,"157":1}}],["expanded",{"2":{"156":1}}],["expand",{"2":{"101":1,"156":1,"157":8}}],["expands",{"2":{"16":1,"157":2}}],["expanse",{"2":{"66":1,"156":1}}],["exploitation",{"2":{"157":1}}],["exploits",{"2":{"156":2}}],["exploration",{"2":{"157":1}}],["explorer",{"2":{"72":1}}],["explore",{"0":{"4":1},"2":{"4":1,"7":1,"40":1,"51":2,"57":1,"84":1,"156":1,"157":2}}],["explanatory",{"2":{"156":1}}],["explanations",{"2":{"18":1}}],["explaining",{"2":{"146":1,"153":1}}],["explain",{"2":{"120":1,"121":1,"149":1,"160":1}}],["explains",{"2":{"93":1,"149":1}}],["explicit",{"2":{"63":1,"129":1,"131":1,"156":8,"160":1}}],["explicitly",{"2":{"1":1,"12":1,"20":1,"22":1,"25":1,"41":1,"51":3,"82":1,"85":1,"115":1,"143":1,"147":1,"148":1,"156":4,"157":3,"159":1}}],["expects",{"2":{"126":1,"146":1,"153":1,"156":1}}],["expected",{"2":{"51":1,"156":1,"157":1}}],["expert",{"2":{"116":1,"117":1,"129":1,"131":1,"143":1,"146":1,"153":1,"156":1}}],["experiencing",{"2":{"72":1}}],["experience",{"2":{"11":1}}],["experiences",{"2":{"11":1}}],["experiment",{"2":{"51":1,"87":1,"156":1,"157":1}}],["experimental",{"0":{"20":1,"159":1},"2":{"0":3,"9":4,"20":2,"47":3,"51":6,"52":2,"54":1,"55":3,"59":1,"62":1,"63":6,"65":1,"84":1,"100":2,"156":125,"157":70,"158":4,"159":5,"160":177}}],["expensive",{"2":{"2":1,"51":3,"157":6}}],["ggi",{"2":{"156":3}}],["gguf",{"2":{"27":1}}],["gnarled",{"2":{"66":1,"156":1}}],["glossy",{"2":{"156":1}}],["globally",{"2":{"48":1}}],["global",{"2":{"41":1,"51":1,"156":3}}],["glittering",{"2":{"66":1,"156":1}}],["glasses",{"2":{"38":1}}],["glad",{"2":{"30":1}}],["gpu=99",{"2":{"36":2}}],["gpu",{"2":{"27":1,"36":1}}],["gpt4v",{"2":{"19":2,"156":5}}],["gpt4",{"2":{"14":1,"91":1,"92":1,"156":3}}],["gpt4t",{"2":{"5":1,"6":2,"14":3,"16":1,"20":1,"50":1,"91":1,"156":2}}],["gpt3t",{"2":{"101":1}}],["gpt3",{"2":{"14":1,"101":1,"156":5}}],["gpt",{"2":{"5":1,"6":1,"14":5,"91":2,"92":1,"156":14,"160":1}}],["guidance",{"2":{"102":1}}],["guidelines>",{"2":{"153":2}}],["guidelines",{"2":{"137":1,"143":2,"146":2,"147":1,"148":1,"153":1}}],["guide",{"0":{"81":1},"2":{"21":1,"40":1,"51":2,"57":2,"68":1,"76":2,"89":1,"146":1,"153":1,"156":2,"157":3}}],["guarantees",{"2":{"84":1}}],["guardian",{"2":{"66":1,"156":1}}],["guardrails",{"2":{"51":1,"157":1}}],["guessed",{"2":{"51":1,"157":1}}],["guesser",{"2":{"51":3,"157":3}}],["guesses",{"2":{"20":1,"50":1,"51":2,"157":2}}],["guess",{"2":{"20":1,"50":2,"51":31,"157":31}}],["guessing",{"2":{"20":1,"50":2,"51":1,"157":1}}],["g",{"2":{"18":1,"51":2,"126":1,"156":4}}],["group",{"2":{"84":1,"160":4}}],["grow",{"2":{"11":1}}],["gracefully",{"2":{"156":4}}],["grasp",{"2":{"117":1}}],["grammar",{"2":{"102":1}}],["granularity",{"2":{"57":1}}],["grab",{"2":{"51":1,"157":1}}],["gratefully",{"2":{"11":1}}],["grins",{"2":{"40":2}}],["greater",{"2":{"156":1,"160":1}}],["greatingpirate",{"2":{"85":5,"156":7}}],["great",{"2":{"10":1,"23":2,"66":2,"91":1,"142":2,"156":2}}],["giraffe",{"2":{"156":3}}],["github",{"2":{"66":1,"72":1,"76":1,"89":1,"156":5,"160":1}}],["gitignore",{"2":{"10":3}}],["give",{"2":{"23":1,"51":1,"69":1,"84":2,"157":1,"160":1}}],["given",{"2":{"18":2,"56":1,"63":2,"66":4,"73":1,"109":2,"117":1,"126":2,"134":1,"138":1,"145":1,"156":28,"157":5,"160":6}}],["gives",{"2":{"4":1,"12":1,"94":1,"95":1,"109":1,"156":2}}],["germany",{"2":{"160":2}}],["genai",{"2":{"75":1,"159":1}}],["gensym",{"2":{"63":1,"160":1}}],["genie",{"2":{"57":1}}],["general",{"0":{"132":1},"1":{"133":1,"134":1},"2":{"9":1,"22":1,"25":1,"100":1,"102":1,"106":1,"111":1,"137":2,"146":2,"147":1,"148":1,"153":3}}],["generally",{"2":{"6":1,"102":1}}],["generator",{"2":{"60":1,"61":1,"63":24,"160":24}}],["generated",{"2":{"5":1,"7":1,"9":5,"17":1,"23":1,"46":1,"48":1,"51":1,"56":1,"57":1,"63":5,"65":1,"66":1,"70":1,"91":1,"100":2,"111":2,"156":22,"157":2,"160":9}}],["generate",{"0":{"3":1},"2":{"1":1,"2":1,"6":1,"7":1,"9":4,"48":3,"51":1,"55":1,"56":3,"57":5,"59":2,"60":3,"62":2,"63":10,"65":2,"84":2,"85":1,"87":1,"89":1,"100":2,"102":3,"111":1,"115":1,"134":1,"156":24,"157":3,"160":17}}],["generates",{"2":{"1":1,"9":1,"20":1,"62":3,"63":1,"100":1,"115":1,"116":1,"156":5,"160":4}}],["generating",{"2":{"9":1,"47":1,"63":11,"100":1,"156":15,"157":2,"160":18}}],["generativeai",{"2":{"52":1}}],["generative",{"2":{"0":1,"56":1,"64":1,"65":1}}],["generation",{"0":{"0":1,"32":1,"37":1},"1":{"1":1,"33":1,"34":1,"35":1,"38":1,"39":1,"40":1,"41":1},"2":{"0":1,"5":1,"50":1,"55":1,"59":2,"62":3,"63":3,"102":1,"111":1,"156":24,"157":2,"159":1,"160":5}}],["generic",{"2":{"3":1,"99":1,"111":1,"141":1}}],["gestures",{"2":{"40":1}}],["gemini",{"2":{"31":3,"32":2,"33":3,"34":1,"35":1,"156":11}}],["getindex",{"2":{"51":1,"157":1}}],["getting",{"0":{"72":2,"73":1,"88":1},"1":{"89":1,"90":1,"91":1,"92":1},"2":{"21":1,"136":1}}],["get",{"2":{"9":1,"20":1,"31":1,"45":1,"50":2,"51":3,"54":2,"55":1,"57":1,"60":5,"62":5,"63":10,"68":1,"71":2,"72":2,"76":1,"77":1,"81":1,"82":1,"83":1,"84":4,"89":1,"91":1,"100":1,"101":1,"102":2,"156":32,"157":4,"158":2,"160":36}}],["gt",{"2":{"6":14,"9":3,"14":1,"20":3,"36":1,"41":2,"50":2,"51":4,"60":16,"63":9,"66":1,"76":1,"77":1,"81":1,"82":2,"85":3,"89":1,"92":2,"94":10,"96":1,"99":3,"100":3,"101":2,"102":5,"156":16,"157":7,"160":15}}],["gamma",{"2":{"156":1,"157":4}}],["game",{"2":{"20":1,"50":1,"51":2,"149":1,"157":2}}],["gaps",{"2":{"143":1}}],["gaze",{"2":{"66":1,"156":1}}],["gauge",{"2":{"23":1}}],["gave",{"2":{"11":1}}],["gain",{"2":{"5":1}}],["garbage",{"2":{"1":2}}],["goes",{"2":{"156":1}}],["goal",{"2":{"117":1,"120":1,"156":2}}],["going",{"2":{"61":1,"102":1}}],["got",{"2":{"51":2,"156":1,"157":2}}],["gotchas",{"0":{"35":1},"2":{"51":1,"157":1}}],["googleschema",{"2":{"156":2}}],["googlegenaipromptingtoolsext",{"2":{"156":1}}],["googlegenai",{"2":{"31":2,"156":1}}],["google",{"0":{"31":1},"1":{"32":1,"33":1,"34":1,"35":1},"2":{"10":1,"31":3,"35":1,"69":1,"106":1,"156":9}}],["good",{"2":{"3":1,"7":1,"12":1,"23":2,"51":1,"74":1,"84":1,"134":1,"146":1,"153":1,"156":3,"157":1,"160":2}}],["golden",{"2":{"3":1,"7":1}}],["go",{"2":{"1":1,"11":1,"40":3,"63":1,"71":1,"74":1,"81":2,"89":1,"99":1,"146":1,"153":1,"160":1}}],["=1",{"2":{"160":1}}],["=template",{"2":{"156":2}}],["=pt",{"2":{"156":3}}],["=prompt",{"2":{"156":1}}],["=url",{"2":{"156":1}}],["=user",{"2":{"133":1}}],["=system",{"2":{"133":1,"156":1}}],["=main",{"2":{"51":1,"156":1}}],["=nothing",{"2":{"51":2,"157":4}}],["==true",{"2":{"156":1}}],["==",{"2":{"20":2,"50":3,"51":4,"84":1,"146":4,"153":4,"156":2,"157":13}}],["=wordcount",{"2":{"12":1,"156":2}}],["=>dic",{"2":{"156":1}}],["=>dict",{"2":{"102":6,"156":2}}],["=>",{"2":{"5":8,"6":1,"84":3,"101":4,"102":16,"147":1,"148":1,"156":16,"160":3}}],["=",{"2":{"0":2,"1":6,"3":4,"5":8,"6":14,"9":1,"11":6,"12":7,"13":2,"14":3,"15":4,"16":2,"17":4,"18":2,"19":3,"20":14,"21":5,"22":7,"23":8,"24":1,"25":3,"26":5,"27":1,"28":9,"29":1,"30":3,"31":1,"33":2,"34":2,"36":2,"38":2,"39":5,"40":2,"41":8,"42":1,"44":2,"45":5,"46":3,"47":1,"50":14,"51":60,"54":4,"55":1,"57":8,"59":2,"61":28,"63":127,"66":28,"76":2,"77":1,"79":1,"82":2,"83":2,"84":11,"86":5,"89":2,"91":1,"92":1,"96":1,"99":1,"100":3,"101":10,"102":20,"145":1,"146":1,"147":2,"148":1,"152":1,"153":1,"156":416,"157":126,"158":7,"160":241}}],["ml",{"2":{"149":1}}],["mm",{"2":{"136":2,"137":3}}],["mdoel",{"2":{"101":1}}],["mdash",{"2":{"51":9,"54":1,"63":6,"66":5,"156":120,"157":37,"158":2,"159":1,"160":88}}],["m1",{"2":{"36":1}}],["mcts",{"2":{"20":1,"48":1,"51":1,"157":1}}],["m",{"2":{"20":2,"22":1,"25":1,"27":3,"29":1,"30":2,"33":1,"36":1,"41":1,"50":2,"51":3,"83":3,"102":1,"156":4,"157":3}}],["mytemplates",{"2":{"157":1}}],["mytype",{"2":{"84":1}}],["myfunction",{"2":{"156":2}}],["myaijudgemodel",{"2":{"160":1}}],["myadd",{"2":{"146":6,"153":6}}],["myabstractresponse",{"2":{"84":5}}],["mybool",{"2":{"84":2}}],["myreranker",{"2":{"59":4,"63":2,"160":2}}],["mymeasurementwrapper",{"2":{"156":2}}],["mymeasurement",{"2":{"18":5,"156":18}}],["my",{"2":{"11":1,"12":1,"22":3,"25":2,"30":1,"33":1,"34":1,"40":1,"73":1,"81":1,"83":3,"102":1,"156":10}}],["mix",{"2":{"156":1}}],["mixed",{"2":{"102":1}}],["mixtral",{"2":{"27":1,"29":1,"30":2,"36":2,"102":2,"156":1}}],["million",{"2":{"92":2}}],["mickey",{"2":{"66":1,"156":1}}],["middle",{"2":{"40":1,"157":1,"160":1}}],["mimics",{"2":{"66":1,"156":1}}],["mimic",{"2":{"26":1,"84":1,"95":1,"157":2}}],["mistakes",{"2":{"120":2}}],["mistrall",{"2":{"156":2}}],["mistralopenaischema",{"2":{"22":2,"25":2,"156":4}}],["mistralai",{"0":{"22":1,"25":1},"2":{"22":3,"24":1,"25":1,"26":1,"69":1,"76":2,"102":1,"156":5}}],["mistral",{"2":{"21":3,"22":7,"25":7,"36":1,"39":1,"46":1,"81":2,"156":22}}],["missing",{"2":{"6":1,"51":1,"143":1,"156":6}}],["mind",{"2":{"102":1}}],["minute",{"2":{"73":5}}],["minutes",{"2":{"10":2,"75":2,"156":3}}],["min",{"2":{"63":8,"66":1,"156":1,"160":11}}],["minichunks",{"2":{"66":1,"156":1}}],["minimal",{"2":{"55":1}}],["minimum",{"2":{"1":1,"63":2,"66":2,"156":2,"160":4}}],["mini",{"2":{"51":2,"157":2}}],["might",{"2":{"6":1,"9":1,"22":2,"25":1,"51":2,"66":1,"74":1,"76":1,"100":1,"156":2,"157":2}}],["music",{"2":{"136":1,"137":1}}],["must",{"2":{"11":2,"20":2,"36":1,"40":2,"50":2,"51":8,"63":1,"85":3,"99":1,"102":3,"107":1,"112":1,"116":1,"120":3,"121":1,"126":2,"136":1,"137":1,"138":2,"140":1,"146":2,"149":1,"153":2,"156":15,"157":12,"159":1,"160":6}}],["murmured",{"2":{"66":1,"156":1}}],["mutates",{"2":{"63":1,"160":1}}],["mutated",{"2":{"63":1,"160":3}}],["mutating",{"2":{"51":1,"62":1,"63":1,"157":2,"160":2}}],["mutable",{"2":{"51":1,"156":1}}],["multihits",{"2":{"160":1}}],["multihop",{"2":{"160":1}}],["multiindex",{"2":{"156":1,"160":2}}],["multiplication",{"2":{"46":1}}],["multiple",{"0":{"45":1},"2":{"5":1,"7":1,"13":1,"20":1,"45":1,"51":3,"59":1,"63":2,"66":3,"84":1,"92":1,"96":1,"99":1,"101":1,"102":1,"146":1,"147":2,"148":2,"153":1,"156":19,"157":6,"160":6}}],["multi",{"0":{"83":1},"2":{"34":1,"41":1,"92":1,"156":6,"157":2}}],["much",{"0":{"75":1},"2":{"6":2,"7":1,"12":1,"23":1,"40":1,"51":1,"63":2,"100":2,"102":1,"156":3,"157":3,"160":2}}],["msg1",{"2":{"156":2}}],["msgs",{"2":{"156":1}}],["msg=aigenerate",{"2":{"156":7}}],["msg",{"2":{"6":1,"11":2,"15":5,"18":4,"19":2,"21":4,"22":2,"25":1,"26":1,"28":1,"30":2,"34":1,"39":2,"40":1,"44":1,"45":1,"46":2,"51":9,"57":1,"63":2,"84":4,"101":3,"102":2,"156":92,"157":6,"160":3}}],["madrid",{"2":{"91":1,"92":3}}],["made",{"2":{"48":1,"51":5,"111":1,"137":1,"156":2,"157":6}}],["mapped",{"2":{"156":1}}],["mapping",{"2":{"84":1,"156":1}}],["mapreduce",{"2":{"45":1}}],["mask",{"2":{"65":1}}],["mastering",{"2":{"57":1}}],["master",{"2":{"11":2,"34":1,"40":1,"156":5}}],["making",{"2":{"94":1,"99":1}}],["makie",{"2":{"63":1,"160":1}}],["makes",{"2":{"36":1,"100":1,"136":1}}],["make",{"2":{"3":1,"5":1,"6":1,"7":2,"10":1,"28":1,"36":2,"51":2,"63":3,"64":1,"73":1,"76":2,"85":1,"89":2,"102":2,"104":1,"109":1,"136":3,"137":1,"156":8,"157":2,"160":4}}],["magenta",{"2":{"57":1,"160":3}}],["maintaining",{"2":{"155":1}}],["maintain",{"2":{"137":1,"156":2}}],["main",{"2":{"48":2,"56":1,"57":1,"59":3,"60":1,"63":1,"65":1,"85":1,"98":1,"102":1,"156":7,"159":1,"160":3}}],["machine",{"2":{"57":1}}],["machines",{"2":{"34":1}}],["mac",{"2":{"36":1,"76":1}}],["macros",{"2":{"51":1,"156":5}}],["macro",{"2":{"14":1,"33":1,"83":3,"91":1,"120":1,"156":8}}],["may",{"2":{"23":1,"33":1,"41":1,"85":2,"136":1,"156":6,"157":1,"159":1,"160":1}}],["maybeextract",{"2":{"18":1,"156":11}}],["marks",{"2":{"156":1}}],["markup",{"2":{"156":1}}],["marked",{"2":{"111":1,"156":2}}],["markdown",{"2":{"19":3,"136":1,"137":1,"138":1,"143":1,"156":20}}],["marsaglia",{"2":{"157":1}}],["mars",{"2":{"16":1,"156":2}}],["margin=",{"2":{"160":1}}],["margin",{"2":{"1":1,"160":4}}],["manner",{"2":{"136":1,"160":1}}],["manage",{"2":{"157":1}}],["managed",{"2":{"156":4,"157":1}}],["management",{"2":{"156":1}}],["manages",{"2":{"102":1,"156":1,"157":1}}],["manageable",{"2":{"62":1,"66":1,"156":1,"157":1}}],["managing",{"2":{"59":1}}],["manually",{"2":{"26":1,"27":1,"72":1}}],["manymeasurements",{"2":{"18":1}}],["many",{"2":{"18":1,"22":1,"26":1,"48":2,"65":1,"68":1,"69":1,"81":1,"84":1,"85":1,"96":1,"102":1,"115":1,"116":1,"156":4}}],["mandarin",{"2":{"13":1}}],["manipulations",{"2":{"142":1}}],["manipulation",{"2":{"1":1,"64":1}}],["matlab",{"2":{"57":1}}],["matter",{"2":{"35":1}}],["matrix",{"2":{"21":1,"45":2,"46":1,"62":1,"156":4,"160":9}}],["materialize",{"2":{"44":1,"156":1}}],["material",{"2":{"11":1}}],["matches",{"2":{"156":5,"160":2}}],["matched",{"2":{"115":1,"116":1,"118":1,"160":2}}],["match",{"2":{"6":2,"10":1,"23":3,"57":5,"62":1,"63":9,"66":7,"156":9,"160":16}}],["matching",{"2":{"6":5,"57":1,"63":3,"66":1,"156":1,"160":8}}],["maxes",{"2":{"73":1}}],["maximum",{"2":{"17":1,"20":2,"51":4,"54":1,"66":4,"73":1,"106":1,"156":13,"157":6,"158":1,"160":2}}],["max",{"2":{"7":1,"19":1,"20":4,"50":2,"51":18,"54":1,"63":3,"65":1,"66":21,"84":1,"86":1,"102":1,"156":45,"157":26,"158":1,"160":7}}],["merely",{"2":{"156":1}}],["mentioning",{"2":{"149":1}}],["mention",{"2":{"136":1}}],["mentioned",{"2":{"112":1,"149":1}}],["melody",{"2":{"66":1,"156":1}}],["memory`",{"2":{"102":1}}],["memory",{"2":{"57":1,"120":1,"142":1,"160":1}}],["memories",{"2":{"11":1,"66":1,"156":1}}],["meetings",{"2":{"136":2,"137":2}}],["meet",{"2":{"38":1,"39":1,"41":1}}],["messaging",{"2":{"156":3}}],["message",{"0":{"33":1,"38":1},"2":{"20":1,"48":2,"50":1,"51":17,"57":1,"63":1,"81":2,"83":2,"85":2,"91":2,"96":3,"101":2,"102":3,"120":1,"121":2,"124":1,"133":1,"156":77,"157":22,"160":1}}],["message`",{"2":{"20":1,"50":1,"51":1,"157":1}}],["messagese",{"2":{"156":1}}],["messages",{"0":{"98":1},"2":{"11":2,"22":1,"23":1,"26":1,"35":1,"51":2,"83":1,"84":1,"85":1,"87":1,"94":1,"97":1,"98":2,"101":6,"102":1,"156":41,"157":5}}],["mesages",{"2":{"23":1}}],["mechanisms",{"2":{"100":1}}],["mechanism",{"2":{"22":1,"25":1,"66":1,"102":1,"156":1}}],["medium",{"2":{"22":1,"25":1,"160":4}}],["measuring",{"2":{"160":1}}],["measures",{"2":{"66":2,"156":2}}],["measurements",{"2":{"18":2,"156":9}}],["meantime",{"2":{"156":1}}],["meant",{"2":{"120":1,"121":1,"156":2}}],["meaningful",{"2":{"136":1}}],["meaning",{"2":{"117":1,"160":1}}],["means",{"2":{"17":1,"31":1,"36":1,"57":1,"63":2,"65":2,"73":1,"156":2,"160":3}}],["mean",{"2":{"0":1,"6":2,"160":2}}],["me",{"2":{"15":2,"21":3,"22":1,"25":1,"29":2,"30":2,"33":1,"38":1,"41":1,"44":2,"45":4,"46":2,"66":1,"79":1,"84":2,"85":4,"149":1,"156":10}}],["meticulously",{"2":{"129":1,"131":1,"137":1}}],["metaprogramming",{"2":{"57":1}}],["metadatamessage",{"2":{"156":2}}],["metadata=true",{"2":{"1":1,"7":1}}],["metadata",{"0":{"105":1},"1":{"106":1,"107":1},"2":{"1":4,"7":1,"62":1,"63":1,"106":2,"107":1,"156":25,"160":13}}],["met",{"2":{"51":12,"84":2,"102":1,"157":16}}],["methoderror",{"2":{"102":1}}],["methods",{"2":{"51":3,"62":1,"63":1,"94":1,"102":1,"156":3,"157":3,"160":2}}],["method",{"2":{"9":1,"48":2,"51":1,"59":2,"63":13,"66":1,"94":1,"95":1,"100":1,"156":66,"157":39,"158":2,"160":99}}],["metrics",{"2":{"5":1}}],["mock",{"2":{"157":1}}],["month",{"2":{"74":1}}],["monte",{"2":{"20":1,"48":1,"51":1,"157":3}}],["money",{"2":{"74":1,"156":1}}],["moonlight",{"2":{"66":2,"156":2}}],["mouse",{"2":{"66":1,"156":1}}],["modified",{"2":{"160":1}}],["modify",{"2":{"23":1}}],["modal",{"2":{"156":2}}],["modality",{"2":{"64":1}}],["modular",{"2":{"56":1,"157":1}}],["modules",{"2":{"65":1}}],["module",{"0":{"159":1},"2":{"0":1,"9":1,"20":1,"23":1,"47":2,"48":1,"51":4,"52":2,"53":1,"55":2,"57":1,"65":1,"100":1,"156":4,"157":3,"159":4,"160":2}}],["modes",{"2":{"160":1}}],["modern",{"2":{"111":2}}],["moderation",{"2":{"16":1}}],["mode",{"2":{"23":2,"63":1,"102":2,"156":1,"160":1}}],["model3",{"2":{"156":1}}],["model2",{"2":{"156":1}}],["model1",{"2":{"156":3}}],["modeling",{"2":{"57":1}}],["model>",{"2":{"26":1}}],["model=pt",{"2":{"63":1,"160":1}}],["model=",{"2":{"19":2,"20":1,"21":3,"22":3,"25":2,"26":1,"29":2,"30":3,"34":1,"36":1,"42":1,"50":1,"51":3,"61":1,"100":2,"101":1,"156":29,"157":3}}],["modelspec",{"2":{"156":3}}],["models",{"0":{"21":1,"28":1,"36":1,"41":1},"1":{"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1},"2":{"9":1,"14":1,"16":1,"21":1,"22":4,"25":3,"27":2,"28":2,"31":1,"35":1,"36":4,"38":1,"41":1,"51":1,"52":1,"63":2,"66":2,"68":2,"69":1,"70":5,"81":3,"82":1,"92":1,"94":1,"95":1,"97":1,"100":1,"102":1,"129":1,"147":1,"148":1,"151":1,"152":1,"153":1,"156":29,"157":2,"160":2}}],["model",{"0":{"14":1,"82":1,"87":1,"95":1},"2":{"5":2,"6":2,"9":8,"14":8,"15":1,"16":1,"17":1,"20":3,"21":1,"22":2,"25":1,"26":2,"27":4,"28":5,"29":4,"30":6,"31":2,"32":1,"33":2,"34":1,"36":3,"38":2,"39":2,"40":1,"41":6,"44":2,"45":3,"46":1,"48":8,"50":3,"51":16,"55":1,"56":3,"57":2,"59":2,"61":4,"63":43,"75":1,"80":1,"81":1,"82":6,"83":2,"84":1,"87":1,"91":3,"94":3,"95":1,"96":1,"97":1,"98":3,"100":8,"101":4,"102":17,"109":2,"134":1,"146":1,"149":5,"153":1,"156":227,"157":19,"160":88}}],["moreover",{"2":{"23":1}}],["more",{"2":{"1":1,"4":3,"5":4,"6":4,"7":1,"9":1,"11":2,"12":3,"14":2,"15":1,"16":3,"18":2,"19":1,"20":2,"22":1,"23":3,"25":1,"27":1,"28":1,"36":1,"42":1,"51":4,"54":1,"57":4,"62":1,"63":6,"64":1,"65":1,"66":1,"73":1,"74":1,"77":1,"78":1,"80":1,"81":1,"82":1,"84":1,"86":1,"89":1,"92":2,"99":1,"100":2,"101":1,"102":5,"109":1,"117":1,"120":1,"156":51,"157":11,"158":1,"160":20}}],["moved",{"2":{"0":1,"157":1,"159":1,"160":1}}],["moment",{"2":{"0":1,"22":1,"26":1,"31":1,"42":1,"156":1}}],["mostly",{"2":{"87":1}}],["most",{"2":{"0":1,"7":1,"22":1,"25":1,"51":2,"57":1,"59":1,"62":2,"63":3,"80":1,"106":2,"107":2,"116":1,"126":1,"134":1,"138":1,"149":1,"156":10,"157":4,"160":7}}],["uct",{"2":{"156":1,"157":12}}],["ultimately",{"2":{"84":1}}],["u>",{"2":{"66":1,"156":5,"160":1}}],["u>promptingtools",{"2":{"66":1,"156":5,"160":1}}],["uint16",{"2":{"157":1}}],["uint64",{"2":{"44":2}}],["uint8",{"2":{"44":1}}],["utils",{"2":{"66":1,"156":1}}],["utilized",{"2":{"106":1}}],["utilizing",{"2":{"57":1}}],["utility",{"2":{"48":2,"65":1,"86":1,"101":1,"137":1,"156":4}}],["utilities",{"0":{"64":1},"1":{"65":1,"66":1},"2":{"20":1,"47":1,"48":2,"50":1,"51":1,"55":1,"64":1,"65":1,"66":2,"157":2}}],["ut",{"2":{"18":1}}],["unclear",{"2":{"160":1}}],["unhealthy",{"2":{"156":1}}],["unwraps",{"2":{"157":1}}],["unwrapping",{"2":{"156":2}}],["unwrap",{"2":{"156":1,"157":2}}],["unfortunately",{"2":{"102":2}}],["unlike",{"2":{"160":1}}],["unlock",{"2":{"100":1}}],["unless",{"2":{"66":1,"143":1,"147":2,"148":2,"156":2}}],["unnecessary",{"2":{"86":1,"111":1,"118":1}}],["unexpected",{"2":{"74":1}}],["unexported",{"2":{"47":1,"55":1,"79":1}}],["unanswered",{"2":{"66":1,"156":1}}],["unspecified",{"2":{"156":6}}],["unspoken",{"2":{"66":1,"156":1}}],["unsuccessfully",{"2":{"51":1,"156":1}}],["unsafe",{"2":{"51":6,"156":7,"157":1}}],["unusable",{"2":{"36":1}}],["un",{"2":{"23":1,"36":1}}],["until",{"2":{"20":3,"50":2,"51":2,"73":1,"156":1,"157":1}}],["unique",{"2":{"63":1,"106":1,"156":4,"157":1,"160":4}}],["universal",{"2":{"51":1,"157":1}}],["union",{"2":{"18":3,"30":1,"51":11,"63":3,"66":1,"102":1,"156":63,"157":17,"160":20}}],["units",{"2":{"160":1}}],["unitrange",{"2":{"44":1}}],["unit",{"2":{"18":2,"75":1,"94":1,"98":1,"120":1,"146":2,"153":2}}],["unknown",{"2":{"16":3,"127":2,"156":5}}],["underscores",{"2":{"160":1}}],["understandable",{"2":{"112":1,"156":1}}],["understand",{"2":{"48":1,"51":1,"63":1,"93":1,"136":1,"137":1,"156":1,"160":1}}],["understanding",{"0":{"78":1},"2":{"40":1,"93":1,"111":1}}],["underlying",{"2":{"9":1,"23":1,"48":1,"51":1,"55":1,"100":1,"102":1,"156":1,"157":2,"160":3}}],["under",{"2":{"1":1,"17":1,"22":1,"25":1,"27":1,"48":1,"57":1,"66":1,"85":2,"93":1,"101":2,"102":1,"156":7}}],["urls",{"2":{"98":1,"156":2}}],["url=env",{"2":{"28":1}}],["url=provider",{"2":{"26":1}}],["url=",{"2":{"22":1,"27":1,"156":2}}],["url",{"2":{"9":1,"19":2,"22":1,"26":4,"42":1,"61":3,"63":4,"100":1,"156":43,"158":1,"160":7}}],["usable",{"2":{"94":1,"99":1}}],["usage",{"2":{"20":1,"35":1,"66":1,"70":1,"156":2,"157":1}}],["usd",{"2":{"73":1}}],["usually",{"2":{"51":1,"72":1,"99":1,"156":4,"157":2}}],["usual",{"2":{"28":1,"29":2,"30":2}}],["us",{"2":{"9":1,"20":2,"48":2,"50":2,"51":1,"57":1,"70":1,"100":1,"157":1}}],["uses",{"2":{"9":2,"10":1,"12":1,"27":1,"48":1,"63":1,"86":1,"100":2,"121":1,"156":9,"160":22}}],["useful",{"2":{"9":1,"14":1,"16":1,"20":1,"50":1,"51":4,"63":4,"66":3,"81":1,"100":2,"109":2,"129":1,"147":1,"148":1,"151":1,"152":1,"153":1,"156":10,"157":2,"160":9}}],["users",{"2":{"156":1}}],["user=",{"2":{"85":1,"99":1}}],["usermessagewithimages",{"2":{"98":1,"156":5}}],["usermessage",{"2":{"9":1,"11":4,"23":5,"34":1,"40":1,"48":1,"51":2,"83":2,"85":2,"94":2,"98":2,"99":2,"100":1,"101":2,"156":24,"157":14}}],["user",{"2":{"9":3,"11":2,"12":1,"14":1,"16":1,"23":1,"34":1,"35":1,"40":1,"48":1,"51":10,"55":1,"62":1,"85":3,"94":1,"96":1,"98":3,"99":1,"100":3,"101":1,"102":5,"104":1,"106":2,"107":1,"109":1,"111":2,"112":4,"113":2,"115":2,"116":2,"117":1,"118":3,"120":6,"121":2,"122":1,"124":2,"126":3,"127":1,"129":4,"131":4,"133":5,"134":3,"136":2,"137":2,"140":2,"141":1,"142":1,"143":5,"144":1,"145":1,"146":5,"147":2,"148":2,"149":1,"151":1,"152":1,"153":9,"155":1,"156":45,"157":13}}],["used",{"2":{"5":2,"6":1,"14":4,"15":1,"16":2,"17":1,"51":1,"63":6,"66":4,"70":2,"83":1,"94":1,"120":1,"121":1,"149":1,"156":73,"157":10,"160":18}}],["use",{"2":{"1":5,"6":2,"7":3,"9":3,"11":2,"12":4,"14":2,"15":1,"16":1,"17":3,"18":2,"19":1,"20":4,"21":2,"22":3,"23":8,"24":1,"25":2,"26":1,"27":1,"28":4,"29":4,"30":4,"31":1,"32":1,"33":1,"36":1,"38":1,"41":1,"42":1,"45":1,"50":4,"51":9,"53":1,"54":1,"55":1,"56":1,"57":1,"59":1,"60":1,"61":1,"62":1,"63":44,"66":7,"68":2,"70":4,"73":1,"75":2,"76":1,"77":1,"79":1,"80":1,"82":2,"83":3,"84":4,"85":6,"87":2,"91":3,"92":4,"93":1,"96":1,"100":2,"101":3,"102":6,"120":4,"122":1,"136":9,"137":2,"143":2,"145":1,"147":1,"148":1,"149":1,"156":96,"157":19,"158":1,"160":69}}],["using",{"0":{"21":1,"22":1,"23":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"46":1,"92":1},"2":{"0":4,"4":1,"6":2,"7":2,"9":1,"22":1,"23":4,"26":1,"36":1,"41":1,"46":1,"55":2,"56":1,"57":4,"62":3,"63":6,"66":6,"79":1,"82":1,"85":1,"86":1,"89":1,"90":2,"100":1,"102":2,"106":1,"111":1,"120":1,"137":4,"146":2,"149":1,"152":1,"153":2,"156":31,"157":8,"160":25}}],["upto",{"2":{"156":2}}],["upfront",{"2":{"61":1,"63":1,"160":1}}],["uppercased",{"2":{"106":1}}],["upper",{"2":{"51":2,"157":4}}],["updates",{"2":{"57":1,"156":1,"157":2,"160":1}}],["updated",{"2":{"51":1,"156":1,"157":4}}],["update",{"2":{"19":1,"51":1,"72":1,"156":2,"157":1,"160":1}}],["uploads",{"2":{"19":1,"156":2}}],["upon",{"2":{"11":1,"51":3,"69":1,"137":1,"156":2,"157":2}}],["up",{"2":{"0":1,"7":1,"10":1,"14":1,"51":2,"53":1,"57":1,"62":1,"71":1,"83":1,"84":1,"99":1,"101":1,"102":3,"104":1,"109":1,"143":1,"156":2,"157":3,"160":3}}],["b64",{"2":{"156":4}}],["b",{"2":{"146":2,"153":2,"157":1}}],["b>",{"2":{"66":2,"156":10,"160":2}}],["br",{"2":{"160":1}}],["broader",{"2":{"156":1}}],["browser",{"2":{"81":1}}],["br>",{"2":{"66":1,"156":5,"160":1}}],["brand",{"2":{"102":4}}],["branching",{"2":{"51":1,"157":1}}],["branch",{"2":{"51":1,"157":1}}],["branches",{"2":{"51":4,"66":1,"156":1,"157":4}}],["brackets",{"2":{"57":1,"63":2,"137":1,"160":3}}],["breath",{"2":{"121":1,"152":1}}],["breaks",{"2":{"66":1,"156":1}}],["break",{"2":{"57":1,"121":1,"122":1}}],["bread",{"2":{"30":2,"102":2}}],["bright",{"2":{"156":2}}],["bring",{"2":{"90":1}}],["brings",{"2":{"11":1}}],["briefly",{"2":{"137":1}}],["brief",{"2":{"23":3,"65":1,"98":1,"99":1,"101":2,"104":1,"109":1,"115":1,"120":1,"136":3,"141":1,"142":1,"143":7,"144":1,"145":1,"146":1,"149":1,"151":1,"152":1,"153":1}}],["bge",{"2":{"29":1}}],["billing",{"2":{"74":1}}],["bigger",{"2":{"102":1}}],["big",{"2":{"57":1,"61":1,"63":1,"66":1,"156":1,"160":1}}],["bit",{"2":{"27":1,"38":1,"62":1,"102":1}}],["biases",{"2":{"156":1}}],["bias",{"2":{"9":1,"17":1,"100":1,"156":12}}],["blank",{"2":{"133":1}}],["blanksystemuser",{"0":{"133":1},"2":{"157":1}}],["black",{"2":{"20":1,"50":1,"51":1,"66":1,"102":1,"156":5,"157":1,"160":1}}],["blob",{"2":{"66":1,"156":6,"160":1}}],["block",{"2":{"51":12,"63":3,"120":1,"156":27,"160":7}}],["blocks",{"2":{"51":6,"63":2,"146":2,"153":2,"156":25,"157":3,"160":2}}],["blocking",{"2":{"10":1,"92":1}}],["blue",{"2":{"20":1,"50":1,"51":3,"57":1,"156":2,"157":3,"160":3}}],["bang",{"2":{"91":1}}],["bandit",{"2":{"51":1,"157":2}}],["barplot",{"2":{"63":1,"160":1}}],["bad",{"2":{"51":1,"66":1,"156":1,"157":1}}],["bakllava",{"2":{"41":1,"42":1,"156":3}}],["balance",{"2":{"34":1,"40":2}}],["baai",{"2":{"29":1}}],["backpropagate",{"2":{"156":1,"157":5}}],["backticks",{"2":{"120":1,"121":1,"138":1,"156":1}}],["back",{"2":{"51":4,"83":1,"84":1,"101":1,"156":5,"157":5}}],["backspace",{"2":{"23":1}}],["background",{"2":{"10":1,"21":1,"81":1}}],["batched",{"2":{"160":1}}],["batchembedder",{"2":{"63":1,"156":1,"160":9}}],["batch",{"2":{"13":1,"63":3,"160":9}}],["bash",{"2":{"27":1}}],["basename",{"2":{"85":1,"156":1}}],["base",{"2":{"22":2,"25":1,"26":1,"51":1,"55":1,"137":1,"156":11,"160":1}}],["base64decode",{"2":{"156":1}}],["base64",{"2":{"9":1,"98":1,"100":1,"156":2}}],["based",{"2":{"6":2,"51":3,"57":2,"59":1,"62":6,"63":5,"104":2,"109":1,"111":1,"112":1,"113":1,"120":2,"121":2,"122":1,"127":1,"147":1,"148":1,"156":16,"157":10,"160":12}}],["basic",{"0":{"103":1},"1":{"104":1},"2":{"9":1,"54":3,"94":1,"98":1,"100":1,"158":3}}],["bold",{"2":{"160":4}}],["body",{"2":{"143":1,"156":1}}],["bodies",{"2":{"66":1,"156":1}}],["border",{"2":{"66":4,"156":20,"160":4}}],["boundary",{"2":{"160":1}}],["boundaries",{"2":{"156":1,"160":3}}],["bound",{"2":{"51":7,"157":9}}],["bounds",{"2":{"51":1,"111":1,"157":1}}],["bool=isnothing",{"2":{"156":1}}],["bool=true",{"2":{"51":4,"63":1,"156":5,"157":1,"160":1}}],["bool=false",{"2":{"51":12,"156":29,"157":6}}],["boolean",{"2":{"51":2,"84":1,"156":16,"157":4,"160":6}}],["bool",{"2":{"9":2,"20":1,"50":1,"51":14,"54":3,"63":15,"84":3,"100":2,"102":2,"156":62,"157":16,"158":3,"160":41}}],["both",{"2":{"6":4,"20":2,"40":1,"50":2,"51":2,"121":1,"156":2,"157":2,"160":5}}],["bm25",{"2":{"7":1}}],["by",{"2":{"5":2,"9":2,"12":1,"20":2,"22":1,"23":6,"26":1,"48":3,"51":5,"55":1,"56":2,"57":4,"59":2,"61":1,"63":6,"65":1,"66":5,"70":1,"71":1,"72":1,"86":2,"100":1,"101":2,"102":2,"106":1,"111":1,"112":1,"115":1,"116":1,"118":1,"120":4,"121":3,"122":1,"136":1,"137":1,"139":1,"140":2,"145":1,"149":1,"152":1,"156":47,"157":12,"160":17}}],["business",{"2":{"143":1}}],["bullets",{"2":{"160":1}}],["bullet",{"2":{"120":5,"136":7,"137":5,"143":3}}],["bundle",{"2":{"87":1}}],["buy",{"2":{"75":1}}],["bug",{"2":{"72":2}}],["but",{"2":{"4":1,"5":2,"6":2,"9":3,"10":2,"11":2,"12":1,"17":1,"18":2,"22":1,"23":2,"27":2,"28":1,"29":1,"30":1,"40":2,"54":1,"57":1,"63":2,"66":3,"72":1,"73":1,"76":1,"83":1,"92":1,"93":1,"98":1,"100":3,"102":2,"137":1,"151":1,"156":18,"157":1,"158":1,"160":7}}],["built",{"2":{"3":1,"56":1,"59":1,"106":1,"156":1,"157":1,"160":1}}],["builds",{"2":{"156":6,"160":2}}],["build",{"2":{"0":1,"1":6,"3":1,"7":2,"9":2,"11":1,"48":1,"55":1,"56":4,"57":4,"59":3,"60":3,"62":4,"63":12,"102":1,"156":7,"160":30}}],["building",{"0":{"0":1},"1":{"1":1},"2":{"20":1,"47":1,"51":1,"55":1,"56":1,"57":1,"63":2,"157":2,"159":1,"160":6}}],["berlin",{"2":{"160":1}}],["bearer",{"2":{"156":3}}],["belong",{"2":{"137":1,"138":1,"156":1}}],["below",{"2":{"21":1,"51":2,"68":1,"72":1,"73":1,"80":1,"102":1,"107":1,"109":1,"116":1,"145":1,"152":1,"156":4,"157":2}}],["believe",{"2":{"120":1,"121":1,"122":1}}],["beneath",{"2":{"66":2,"156":2}}],["benefits",{"2":{"102":1}}],["benefit",{"2":{"51":1,"157":1}}],["behave",{"2":{"98":1}}],["behavior",{"2":{"48":1,"59":2,"66":2,"102":1,"156":2}}],["behavioural",{"2":{"138":1}}],["behaviours",{"2":{"51":1,"157":1}}],["behaviour",{"2":{"20":1,"50":1,"51":1,"102":1,"157":1}}],["behind",{"2":{"66":1,"149":1,"156":1}}],["begin",{"2":{"120":1,"146":3,"153":3,"156":1}}],["beginning",{"2":{"83":1,"111":1,"156":3}}],["beginners",{"2":{"57":1}}],["begins",{"2":{"62":1,"149":1}}],["before",{"2":{"51":3,"65":1,"66":1,"73":1,"76":1,"83":1,"89":2,"120":1,"136":1,"137":1,"156":6,"160":1}}],["bespoke",{"2":{"51":1,"156":2}}],["best",{"2":{"4":1,"20":1,"22":2,"25":2,"29":1,"30":1,"33":1,"48":1,"50":1,"51":3,"57":7,"63":1,"85":2,"126":2,"138":1,"139":1,"140":1,"143":1,"156":4,"157":12,"160":1}}],["been",{"2":{"22":1,"25":1,"41":1,"51":3,"99":1,"121":1,"156":3,"157":4}}],["becoming",{"2":{"11":1}}],["become",{"2":{"11":3,"23":1,"34":1,"40":2,"156":7}}],["because",{"2":{"6":1,"10":1,"20":1,"22":1,"25":1,"27":2,"35":1,"50":1,"51":1,"63":1,"100":1,"102":3,"156":4,"157":3,"160":1}}],["beta",{"2":{"156":1,"157":3}}],["betwee",{"2":{"51":1,"157":1}}],["between",{"2":{"5":1,"6":2,"9":1,"15":2,"20":1,"50":1,"51":5,"57":2,"65":4,"66":5,"94":1,"98":1,"112":1,"156":12,"157":6,"160":3}}],["better",{"2":{"7":1,"48":1,"54":1,"63":1,"66":2,"76":1,"102":3,"109":3,"156":4,"158":1,"160":5}}],["being",{"2":{"4":1,"6":1,"40":1,"51":1,"136":1,"156":3,"157":1}}],["be",{"0":{"72":2},"2":{"0":1,"1":1,"6":2,"9":5,"11":1,"14":4,"15":1,"16":1,"17":1,"20":3,"21":1,"22":1,"23":1,"25":1,"27":1,"34":1,"40":1,"48":2,"50":3,"51":22,"54":1,"55":1,"56":1,"57":3,"59":2,"61":3,"62":1,"63":38,"65":1,"66":7,"68":1,"73":1,"74":1,"79":1,"81":1,"82":1,"84":2,"85":3,"86":1,"90":1,"91":2,"93":1,"94":1,"95":1,"96":1,"97":1,"100":4,"102":2,"104":1,"107":1,"109":1,"111":1,"112":1,"115":1,"116":1,"120":6,"121":4,"126":1,"134":1,"136":5,"137":4,"138":8,"143":2,"146":4,"147":2,"148":2,"149":3,"153":4,"156":142,"157":32,"158":1,"159":2,"160":73}}],["f1",{"2":{"160":2}}],["fn",{"2":{"156":2}}],["ffs",{"2":{"120":1,"121":1}}],["f2",{"2":{"51":2,"157":2,"160":2}}],["fmixtral",{"2":{"30":2}}],["f",{"2":{"20":2,"30":1,"50":2,"51":11,"102":1,"147":1,"148":1,"156":6,"157":16}}],["fences",{"2":{"156":2}}],["fence",{"2":{"120":1,"121":1,"156":2}}],["fear",{"2":{"40":1}}],["features",{"2":{"75":1,"149":4}}],["feature",{"2":{"16":1,"20":1,"149":7,"156":1}}],["february",{"2":{"156":1}}],["feb",{"2":{"30":1,"35":1}}],["feedbackfromevaluator",{"0":{"124":1},"2":{"157":3}}],["feedback",{"0":{"123":1},"1":{"124":1},"2":{"20":6,"48":6,"50":8,"51":46,"84":2,"102":5,"120":9,"121":4,"122":4,"124":5,"156":4,"157":116}}],["feel",{"2":{"22":1,"25":1,"30":1,"33":1,"41":1,"56":1,"65":1}}],["feels",{"2":{"11":1}}],["feelings",{"2":{"11":1,"34":2,"40":2,"156":8}}],["few",{"2":{"1":2,"5":1,"10":1,"120":1,"136":1,"156":3,"157":2}}],["flexibility",{"2":{"157":1}}],["flexible",{"2":{"51":2,"55":1,"157":2}}],["fleming",{"2":{"111":3}}],["flowed",{"2":{"66":1,"156":1}}],["flow",{"2":{"60":4,"63":2,"84":1,"160":3}}],["flows",{"2":{"34":1}}],["float",{"2":{"156":1}}],["float32",{"2":{"57":4,"160":2}}],["float64",{"2":{"15":1,"18":1,"21":2,"44":2,"45":2,"46":2,"63":12,"156":19,"157":1,"160":27}}],["float64int64float64dict",{"2":{"6":1}}],["flavor",{"2":{"156":2,"160":1}}],["flavors",{"2":{"9":1,"48":1,"100":1}}],["flag",{"2":{"84":1,"156":3,"157":2,"160":6}}],["flags",{"2":{"14":1,"91":1}}],["fruit",{"2":{"156":2}}],["friendly",{"2":{"29":1,"143":2}}],["francisco",{"2":{"18":1}}],["france",{"2":{"14":1,"63":2,"91":3,"101":4,"160":7}}],["frameworks",{"2":{"57":1}}],["frame",{"2":{"6":8}}],["frames",{"2":{"5":1,"6":7}}],["frequently",{"0":{"67":1},"1":{"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1,"85":1,"86":1,"87":1},"2":{"36":1,"156":1}}],["free",{"2":{"22":1,"25":1,"30":2,"33":1,"35":1,"41":1,"56":1,"65":1,"69":1,"73":2,"75":1,"156":3,"160":1}}],["freedom",{"2":{"11":1}}],["french",{"2":{"13":1}}],["from",{"0":{"73":1,"79":1},"2":{"1":5,"2":1,"5":2,"6":13,"7":1,"9":6,"10":2,"11":2,"18":2,"19":1,"22":1,"24":1,"25":1,"30":1,"31":1,"34":1,"36":2,"40":1,"48":1,"50":1,"51":13,"54":2,"55":1,"56":4,"57":2,"59":2,"60":1,"62":2,"63":16,"65":2,"66":2,"71":1,"76":1,"79":2,"83":1,"84":1,"89":1,"91":1,"98":3,"100":4,"101":1,"102":6,"106":1,"107":2,"109":1,"111":2,"112":3,"113":1,"115":1,"116":1,"117":1,"118":2,"120":2,"124":2,"126":1,"134":1,"136":1,"137":1,"138":3,"143":1,"147":1,"148":1,"149":2,"155":1,"156":66,"157":29,"158":2,"160":35}}],["fur",{"2":{"156":1}}],["furthermore",{"2":{"102":1}}],["further",{"2":{"66":2,"84":2,"156":2,"157":1}}],["fulfilling",{"2":{"120":1}}],["fully",{"2":{"57":1,"63":1,"84":1,"160":2}}],["full",{"2":{"6":1,"63":3,"84":1,"156":8,"160":7}}],["fuzzy",{"2":{"66":1,"156":1}}],["functor",{"2":{"51":1,"157":2}}],["functionality",{"2":{"47":1,"55":1,"146":1,"153":1,"157":1,"159":3,"160":2}}],["function",{"0":{"46":1},"2":{"5":1,"6":2,"7":1,"9":4,"11":1,"15":1,"16":1,"19":1,"20":5,"21":2,"23":1,"30":1,"44":1,"46":1,"48":2,"50":6,"51":37,"53":2,"54":1,"57":2,"59":2,"60":1,"62":1,"63":20,"66":12,"72":2,"83":1,"84":2,"85":1,"87":1,"96":1,"100":4,"102":10,"106":1,"120":3,"129":3,"131":3,"146":3,"153":3,"156":75,"157":58,"160":32}}],["functions",{"0":{"9":1,"100":1},"2":{"1":1,"6":1,"9":5,"14":1,"20":2,"22":1,"23":1,"26":1,"36":1,"48":4,"50":1,"51":7,"56":1,"57":2,"59":5,"60":1,"62":1,"63":1,"65":2,"72":1,"79":1,"82":1,"83":1,"85":2,"99":1,"100":4,"145":1,"146":1,"147":1,"148":1,"152":1,"153":1,"156":10,"157":8,"160":9}}],["func",{"2":{"51":2,"157":4,"160":1}}],["future",{"2":{"0":1,"3":2,"22":1,"57":1,"85":1,"102":1,"156":2,"157":1,"159":1,"160":1}}],["five",{"2":{"156":1}}],["fits",{"2":{"93":1,"126":1}}],["fit",{"2":{"65":1,"66":2,"149":1,"156":2,"160":1}}],["fixes",{"2":{"157":1}}],["fixed",{"2":{"84":1,"157":1}}],["fix",{"2":{"48":2,"51":1,"72":1,"120":1,"122":1,"156":1,"157":4}}],["fixing",{"0":{"50":1,"119":1},"1":{"120":1,"121":1,"122":1},"2":{"9":1,"47":1,"48":1,"51":4,"100":1,"102":1,"157":21}}],["fired",{"2":{"146":1,"153":1}}],["firefunction",{"2":{"30":2}}],["fireworksopenaischema",{"2":{"30":2,"156":2}}],["fireworks",{"0":{"30":1},"2":{"22":1,"26":1,"30":4,"69":1,"102":1,"156":2}}],["first",{"2":{"1":1,"5":1,"6":7,"9":2,"12":1,"14":1,"15":1,"22":1,"23":2,"25":1,"27":1,"36":1,"38":1,"40":1,"59":2,"62":1,"63":2,"66":3,"72":2,"73":1,"84":1,"85":3,"100":2,"101":1,"145":1,"147":1,"148":1,"152":1,"156":18,"157":2,"160":4}}],["fieldnames",{"2":{"102":1}}],["fields",{"2":{"23":1,"51":3,"59":1,"84":1,"101":1,"102":2,"156":14,"157":6,"160":12}}],["field",{"2":{"9":5,"48":1,"51":2,"63":1,"91":1,"100":6,"102":8,"156":1,"157":3,"160":2}}],["finished",{"2":{"156":2}}],["finish",{"2":{"156":5}}],["finance",{"2":{"57":1}}],["finalizes",{"2":{"156":1}}],["finalize",{"2":{"156":2}}],["finally",{"2":{"62":1,"156":1}}],["final",{"2":{"5":1,"51":1,"57":2,"62":1,"63":5,"101":1,"112":1,"157":1,"160":17}}],["finetuning",{"2":{"87":5,"156":2}}],["fine",{"0":{"87":1},"2":{"30":1,"87":1,"156":1}}],["finder",{"2":{"63":7,"160":13}}],["findings",{"2":{"106":1}}],["finding",{"2":{"40":1,"63":2,"160":2}}],["find",{"2":{"1":1,"7":1,"10":1,"11":1,"12":1,"22":1,"25":1,"28":1,"34":1,"40":2,"48":1,"51":1,"57":5,"60":2,"62":3,"63":7,"65":2,"66":3,"156":17,"157":3,"160":32}}],["finds",{"2":{"1":1,"63":1,"157":1,"160":6}}],["filled",{"2":{"156":2}}],["fill",{"2":{"70":1,"102":1,"143":1,"156":13,"160":1}}],["fills",{"2":{"6":1}}],["filechunker",{"2":{"156":1,"160":4}}],["filenames",{"2":{"63":1,"160":1}}],["filename",{"2":{"23":2,"87":1,"156":2}}],["file",{"2":{"3":1,"10":6,"23":3,"27":1,"31":1,"36":1,"63":4,"72":1,"76":1,"87":2,"136":1,"137":1,"138":1,"156":15,"160":10}}],["files`",{"2":{"63":1,"160":1}}],["files",{"2":{"1":2,"23":1,"60":1,"63":4,"71":1,"89":1,"160":12}}],["filtered",{"2":{"57":1,"63":2,"160":4}}],["filtering",{"2":{"7":1,"16":1,"62":2,"63":2,"160":5}}],["filter",{"2":{"1":2,"6":1,"63":7,"106":2,"156":1,"160":12}}],["filters",{"2":{"1":2,"7":1,"62":1}}],["favors",{"2":{"157":1}}],["favorite",{"2":{"81":1}}],["far",{"2":{"157":2}}],["fairly",{"2":{"156":1}}],["fail",{"2":{"51":1,"156":2,"157":3}}],["failure",{"2":{"20":1,"48":3,"50":1,"51":1,"157":3}}],["failures",{"2":{"20":1,"50":1,"51":2,"156":3,"157":1}}],["fails",{"2":{"20":3,"48":1,"50":3,"51":2,"156":6,"157":3}}],["failedresponse",{"2":{"84":3}}],["failed",{"2":{"6":2,"18":1,"51":1,"84":2,"156":2,"157":1,"160":1}}],["faq",{"2":{"70":1,"89":1,"99":1}}],["fallback",{"2":{"156":7}}],["falls",{"2":{"51":1,"157":1}}],["fall",{"2":{"51":3,"157":3}}],["false`",{"2":{"51":1,"157":1}}],["false",{"2":{"1":1,"6":2,"16":2,"20":1,"50":1,"51":13,"54":3,"63":2,"127":2,"156":49,"157":14,"158":3,"160":14}}],["fahrenheit",{"2":{"18":1}}],["faster",{"2":{"20":1,"27":1,"45":1,"50":1,"51":1,"63":1,"157":1,"160":1}}],["fast",{"2":{"17":1,"27":1,"156":1}}],["face",{"2":{"41":1}}],["facing",{"2":{"14":1}}],["facilitate",{"2":{"9":1,"48":1,"51":1,"62":1,"100":1,"156":2,"157":1}}],["facts",{"2":{"148":1}}],["fact",{"2":{"9":1,"16":1,"48":1,"100":1}}],["familiar",{"2":{"0":1}}],["focus",{"2":{"143":1,"149":1,"156":2,"157":1}}],["focused",{"2":{"120":1,"137":1}}],["focusing",{"2":{"57":1}}],["four",{"2":{"16":1,"84":2,"156":2}}],["foundation",{"0":{"28":1},"2":{"28":1,"156":3}}],["found",{"2":{"6":1,"51":1,"63":2,"79":1,"156":19,"157":1,"160":5}}],["food",{"2":{"9":1,"30":5,"100":1,"102":24}}],["footers",{"2":{"1":1}}],["follow",{"2":{"83":1,"120":1,"121":1,"136":2,"138":1,"140":1,"143":3,"145":2,"149":1,"152":1,"156":1}}],["followed",{"2":{"23":1,"63":1,"160":1}}],["follows",{"2":{"6":1,"47":1,"52":1,"55":1,"109":1,"120":1,"156":1}}],["following",{"2":{"4":1,"6":2,"10":1,"16":1,"23":1,"51":1,"57":2,"65":1,"79":1,"86":2,"90":1,"94":1,"109":1,"121":1,"143":1,"156":6,"157":2,"160":1}}],["folder",{"2":{"1":1,"10":3,"12":1,"85":4,"156":4}}],["forget",{"2":{"156":1}}],["forward",{"2":{"156":1}}],["forwarded",{"2":{"63":12,"160":16}}],["forbidden",{"2":{"146":1,"153":1}}],["forum",{"2":{"74":1}}],["forefront",{"2":{"68":1}}],["forever",{"2":{"66":1,"156":1}}],["formulate",{"2":{"111":1}}],["form",{"2":{"70":1,"160":2}}],["former",{"2":{"66":1,"156":1}}],["forms",{"2":{"51":1,"157":1}}],["format=",{"2":{"156":2}}],["format=dict",{"2":{"102":2}}],["formatted",{"0":{"128":1,"150":1},"1":{"129":1,"151":1,"152":1,"153":1},"2":{"87":1,"96":1,"101":1,"102":1,"129":1,"138":1,"151":1,"152":1,"153":1,"157":1,"160":1}}],["formatting",{"2":{"50":1,"62":1,"129":1,"131":1,"136":1,"137":1,"138":1,"155":1,"160":2}}],["format",{"2":{"9":1,"20":1,"50":1,"51":2,"62":1,"84":1,"87":2,"96":1,"98":1,"100":1,"101":2,"102":3,"107":1,"120":1,"129":1,"131":1,"134":1,"136":1,"137":1,"138":2,"143":2,"156":10,"157":2,"160":1}}],["forth",{"2":{"40":1}}],["fortunately",{"2":{"5":1,"102":1}}],["force=true",{"2":{"72":1}}],["force",{"2":{"11":1,"17":1,"23":1,"34":2,"40":1,"72":2,"156":3}}],["for",{"0":{"44":1,"75":1,"76":1,"81":1,"101":1,"102":1,"157":1,"158":1,"159":1,"160":1},"2":{"1":6,"2":1,"3":2,"4":1,"5":2,"6":14,"7":1,"9":9,"11":2,"12":2,"13":2,"14":2,"15":1,"16":3,"17":7,"18":2,"20":7,"21":1,"22":5,"23":14,"25":2,"26":1,"29":5,"30":8,"31":2,"32":1,"34":2,"35":3,"36":3,"38":1,"40":2,"41":2,"46":1,"47":1,"48":6,"50":5,"51":29,"52":1,"53":1,"54":3,"55":1,"56":3,"57":28,"59":6,"61":3,"62":5,"63":63,"65":2,"66":12,"68":2,"70":2,"71":1,"73":1,"75":4,"76":3,"78":1,"79":1,"80":1,"81":1,"82":1,"84":5,"85":4,"86":3,"87":2,"89":2,"91":3,"92":3,"93":1,"95":2,"96":2,"99":2,"100":9,"101":7,"102":13,"104":1,"106":2,"107":1,"109":1,"111":4,"112":2,"113":1,"115":2,"116":1,"117":2,"118":3,"120":3,"121":3,"126":2,"129":4,"131":3,"133":2,"134":1,"136":2,"137":6,"138":3,"141":1,"142":1,"143":5,"144":1,"145":1,"146":7,"147":4,"148":4,"149":1,"151":2,"152":2,"153":8,"156":268,"157":65,"158":3,"159":4,"160":175}}],["l99",{"2":{"156":1}}],["l96",{"2":{"156":1}}],["l610",{"2":{"156":1}}],["l612",{"2":{"156":1}}],["l116",{"2":{"156":1}}],["l164",{"2":{"156":1}}],["l123",{"2":{"41":1}}],["l240",{"2":{"160":1}}],["l249",{"2":{"66":1,"156":1}}],["l211",{"2":{"160":1}}],["l285",{"2":{"66":1,"156":1}}],["llava",{"2":{"41":1}}],["llamaindex",{"2":{"109":1,"116":1,"117":1}}],["llama123",{"2":{"41":3}}],["llama2",{"2":{"36":1,"38":1,"41":2,"81":1,"82":1,"156":2}}],["llama",{"0":{"27":1},"2":{"24":1,"27":3,"28":6,"69":2,"94":1,"95":1,"156":6}}],["ll",{"2":{"20":1,"22":1,"25":1,"31":1,"33":1,"50":1,"51":1,"57":1,"74":1,"81":1,"93":1,"102":3,"120":1,"156":2,"157":1}}],["llmtextanalysis",{"2":{"85":1}}],["llm",{"2":{"9":3,"10":1,"12":1,"18":1,"21":1,"23":1,"48":2,"51":5,"61":1,"63":2,"75":1,"94":1,"95":1,"100":4,"127":1,"156":4,"157":7,"160":2}}],["llms",{"2":{"9":1,"12":1,"18":1,"63":1,"80":1,"81":1,"97":2,"100":1,"102":1,"156":2,"160":1}}],["lt",{"2":{"9":3,"36":1,"51":1,"54":2,"60":1,"63":4,"66":1,"76":1,"81":1,"85":3,"89":1,"96":2,"97":1,"98":1,"99":3,"100":3,"102":1,"156":19,"157":2,"158":2,"160":20}}],["laptop",{"2":{"102":1}}],["launch",{"2":{"76":1,"81":1}}],["launching",{"2":{"76":1,"89":1}}],["launched",{"2":{"36":1}}],["latter",{"2":{"69":1}}],["latency",{"2":{"160":1}}],["latest",{"2":{"12":1,"14":1,"23":4,"27":1,"57":2,"70":1,"72":1,"81":1,"104":1,"109":1,"120":1,"144":1,"151":1,"156":8,"157":1}}],["later",{"2":{"1":1,"3":1,"6":1,"12":1,"46":1,"74":1,"102":1,"156":2,"160":2}}],["lament",{"2":{"66":1,"156":1}}],["langchain",{"0":{"86":1},"2":{"66":3,"86":1,"118":1,"156":3}}],["languages",{"2":{"31":1,"57":1,"156":2}}],["language",{"2":{"9":1,"12":3,"13":3,"20":1,"21":1,"23":6,"48":1,"51":2,"57":6,"94":1,"95":1,"97":1,"100":1,"115":2,"118":1,"144":2,"145":2,"146":1,"147":2,"148":2,"151":2,"152":2,"153":1,"156":5,"157":2}}],["lazily",{"2":{"51":1,"157":1}}],["lazy",{"2":{"9":4,"20":2,"48":5,"50":2,"51":5,"84":1,"100":6,"157":20,"159":1}}],["layers",{"2":{"27":1}}],["largeint",{"2":{"84":3}}],["large",{"2":{"21":1,"29":1,"51":1,"57":1,"63":1,"65":1,"66":1,"94":1,"95":1,"97":1,"146":1,"153":1,"156":2,"157":2,"160":3}}],["larger",{"2":{"6":5,"63":1,"65":1,"66":1,"92":2,"156":3,"160":1}}],["last",{"2":{"20":3,"48":3,"50":4,"51":30,"83":4,"84":4,"101":1,"102":2,"120":1,"156":27,"157":37}}],["lastly",{"2":{"9":1,"51":1,"157":1,"160":1}}],["lake",{"2":{"18":2}}],["labeled",{"2":{"138":1}}],["labels",{"2":{"17":1}}],["label",{"2":{"17":1,"126":3,"149":3}}],["lawyer",{"2":{"6":4}}],["lossless",{"2":{"160":1}}],["losses",{"2":{"157":1}}],["losing",{"2":{"40":1}}],["lot",{"2":{"64":1}}],["lower",{"2":{"51":3,"63":1,"66":1,"156":1,"157":3,"160":1}}],["lowercased",{"2":{"160":1}}],["lowercase",{"2":{"19":1,"51":2,"156":2,"157":2}}],["low",{"2":{"51":1,"73":1,"156":8,"157":1,"160":5}}],["love",{"2":{"34":1}}],["localserver",{"2":{"156":1}}],["localserveropenaischema",{"2":{"156":5}}],["localpreferences",{"2":{"77":1,"156":2}}],["locally",{"2":{"24":1,"42":1,"63":3,"69":1,"94":1,"95":1,"102":1,"156":2,"160":3}}],["local",{"0":{"36":1},"1":{"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1},"2":{"22":1,"42":1,"51":1,"68":1,"72":1,"75":1,"82":1,"156":20,"157":1}}],["localhost",{"2":{"22":1,"27":1,"61":3,"63":3,"156":8,"160":3}}],["location",{"2":{"18":3}}],["loads",{"2":{"85":1,"156":4}}],["loading",{"2":{"63":1,"156":1,"160":1}}],["loaded",{"2":{"23":1,"76":1,"156":2,"160":2}}],["load",{"2":{"22":1,"23":6,"25":1,"55":1,"85":10,"99":1,"102":1,"156":31,"160":3}}],["longer",{"2":{"106":1,"156":1,"160":1}}],["longest",{"2":{"65":6,"66":17,"156":17}}],["long",{"2":{"22":2,"23":1,"25":1,"26":1,"28":1,"51":3,"73":1,"156":1,"157":4}}],["logprobs",{"2":{"156":1}}],["logged",{"2":{"63":1,"160":1}}],["logging",{"2":{"51":2,"63":5,"157":6,"160":14}}],["logic",{"2":{"63":5,"66":1,"96":1,"101":1,"156":1,"157":1,"160":9}}],["logit",{"2":{"9":1,"17":1,"100":1,"156":12}}],["logo",{"2":{"19":1,"156":2}}],["logs",{"2":{"9":1,"63":2,"100":1,"160":3}}],["log",{"2":{"6":1,"10":1,"63":1,"156":8,"160":1}}],["loose",{"2":{"160":1}}],["loosely",{"2":{"97":1,"99":1,"120":1}}],["look",{"2":{"63":1,"73":1,"86":1,"101":1,"156":1,"160":2}}],["looking",{"2":{"22":1,"25":1,"57":3,"157":1,"160":1}}],["looks",{"2":{"14":1,"62":1,"99":1,"156":4,"160":1}}],["lookups",{"2":{"160":1}}],["lookup",{"2":{"1":2,"7":1,"156":2,"160":2}}],["loop",{"2":{"6":1,"23":1,"73":1,"157":2}}],["led",{"2":{"111":1}}],["leetcode",{"2":{"66":1,"156":1}}],["legend",{"2":{"57":1}}],["legacy",{"2":{"19":1,"156":2}}],["less",{"2":{"51":1,"143":1,"157":1}}],["leveraging",{"2":{"156":1}}],["leverages",{"2":{"22":1,"25":1,"53":1,"102":1,"157":1}}],["leverage",{"2":{"12":1,"13":1,"20":1,"51":1,"57":1,"96":1,"102":1,"156":2,"157":1}}],["level",{"2":{"51":2,"59":3,"61":1,"63":2,"66":1,"156":7,"157":4,"160":4}}],["leaves",{"2":{"157":2}}],["leaving",{"2":{"66":2,"156":2}}],["leadership",{"2":{"138":1}}],["leads",{"2":{"34":1,"156":2}}],["leaf",{"2":{"63":2,"160":2}}],["least",{"2":{"62":1,"63":1,"138":1,"156":2,"160":1}}],["learned",{"2":{"11":1,"101":1}}],["learning",{"2":{"9":1,"48":1,"51":1,"57":1,"100":1,"157":1}}],["length=20",{"2":{"66":1,"156":1}}],["length=13",{"2":{"66":1,"156":1}}],["length=10000",{"2":{"66":2,"156":2}}],["length=10",{"2":{"63":1,"66":2,"86":1,"156":2,"160":1}}],["length",{"2":{"6":1,"7":1,"20":2,"23":1,"27":1,"50":2,"51":29,"57":1,"63":4,"65":7,"66":35,"86":1,"156":47,"157":46,"160":16}}],["left",{"2":{"5":1,"6":12,"51":1,"156":1,"157":1}}],["letter",{"2":{"120":1,"121":1}}],["let",{"2":{"0":1,"1":2,"2":1,"4":1,"5":2,"6":2,"11":1,"18":1,"19":1,"21":1,"22":1,"23":3,"25":1,"29":1,"31":1,"36":1,"40":2,"50":1,"51":8,"57":3,"61":1,"63":1,"66":1,"73":1,"83":1,"84":2,"85":1,"101":2,"102":7,"120":1,"121":1,"156":6,"157":9,"160":2}}],["lifting",{"2":{"102":1}}],["lifecycle",{"2":{"157":1}}],["life",{"2":{"28":1}}],["lightweight",{"2":{"160":1}}],["light",{"2":{"91":1,"102":1,"156":3}}],["libraries",{"2":{"57":1,"147":1,"148":1}}],["library",{"2":{"36":1,"81":1}}],["living",{"2":{"34":1,"156":1}}],["links",{"2":{"156":4}}],["link",{"2":{"23":1,"36":1,"136":1,"156":1}}],["line",{"2":{"10":1,"23":1,"27":2,"76":1,"136":1,"137":1,"156":3}}],["lines",{"0":{"1":1},"2":{"1":1,"51":3,"66":1,"156":9,"160":2}}],["linearalgebra",{"2":{"0":1,"15":2,"46":2,"55":1,"156":5,"160":5}}],["limitations",{"2":{"93":1,"156":2}}],["limits",{"0":{"74":1},"2":{"71":1,"73":4,"74":1,"156":2,"157":1}}],["limited",{"2":{"66":1,"149":1,"156":1}}],["limit",{"0":{"73":1},"2":{"13":1,"73":7,"74":4,"102":1,"156":5,"160":2}}],["listened",{"2":{"66":1,"156":1}}],["listed",{"2":{"6":2}}],["list",{"2":{"9":1,"23":1,"36":2,"54":4,"56":1,"57":1,"66":1,"81":1,"100":1,"102":1,"120":3,"126":1,"137":2,"156":20,"158":4,"160":5}}],["literate",{"2":{"7":1,"23":1,"46":1}}],["likely",{"2":{"9":2,"73":1,"75":1}}],["like",{"0":{"86":1},"2":{"1":2,"5":1,"6":2,"11":1,"12":1,"18":1,"20":1,"23":2,"24":1,"29":1,"30":1,"31":1,"34":1,"38":1,"41":2,"48":2,"50":1,"51":4,"59":1,"62":1,"63":3,"66":1,"83":1,"84":1,"85":5,"92":1,"102":1,"106":1,"142":1,"146":1,"147":1,"148":1,"153":1,"156":17,"157":5,"160":6}}],["jxnl",{"2":{"136":1,"137":1}}],["jarvislabs",{"2":{"87":1}}],["jack",{"2":{"18":1,"85":4,"156":7}}],["james",{"2":{"18":1,"156":4}}],["jane",{"2":{"6":4,"106":2}}],["jedi",{"2":{"11":3,"34":1,"156":1}}],["joy",{"2":{"40":1}}],["job",{"2":{"6":4,"75":1}}],["job=",{"2":{"6":1}}],["jobs",{"2":{"6":7}}],["john",{"2":{"6":3,"39":2,"83":6}}],["join",{"2":{"5":3,"6":23,"102":2,"156":1}}],["joining",{"2":{"4":2,"5":2,"6":2,"69":1}}],["joins",{"2":{"1":1,"4":3,"5":1,"6":15,"160":1}}],["joinpath",{"2":{"1":2,"23":1,"156":1}}],["jsonl",{"2":{"87":2,"156":1}}],["json",{"2":{"3":2,"23":2,"85":1,"87":1,"101":1,"102":14,"156":9}}],["json3",{"2":{"0":1,"3":2,"21":1,"44":1,"102":7,"156":3}}],["jump",{"2":{"156":1,"157":1}}],["judgment",{"2":{"143":1}}],["judging",{"2":{"112":1,"160":1}}],["judge=",{"2":{"160":1}}],["judgerating",{"2":{"156":1,"160":2}}],["judgeisittrue",{"0":{"127":1},"2":{"12":1,"16":2,"156":7}}],["judgeallscores",{"2":{"5":1,"156":1,"160":2}}],["judged",{"2":{"5":2}}],["judge",{"2":{"1":1,"5":3,"6":2,"9":1,"16":1,"51":1,"100":1,"112":4,"113":3,"127":1,"156":3,"157":1,"160":8}}],["juicy",{"2":{"30":2,"102":8}}],["just",{"2":{"1":1,"9":1,"12":1,"22":1,"23":4,"25":1,"30":1,"51":1,"54":1,"72":1,"79":1,"81":2,"83":1,"84":1,"99":1,"100":1,"102":4,"104":1,"109":1,"156":7,"157":1,"158":1}}],["juliaqa",{"2":{"160":1}}],["juliaquestion",{"2":{"57":1}}],["julianotagger",{"2":{"160":1}}],["julianotagfilter",{"2":{"160":1}}],["julianoreranker",{"2":{"160":1}}],["julianorephraser",{"2":{"160":1}}],["julianorefiner",{"2":{"160":1}}],["julianopostprocessor",{"2":{"160":1}}],["julianew",{"2":{"11":1,"51":1,"156":1,"157":1}}],["juliahyderephraser",{"2":{"160":1}}],["juliahtmlstyler",{"2":{"160":1}}],["juliaget",{"2":{"160":5}}],["juliagenerate",{"2":{"63":1,"160":1}}],["juliagamma",{"2":{"157":1}}],["juliabatchembedder",{"2":{"160":1}}],["juliabeta",{"2":{"157":1}}],["juliabuild",{"2":{"63":2,"156":1,"160":4}}],["juliauct",{"2":{"157":1}}],["juliausing",{"2":{"0":1,"9":1,"11":1,"12":1,"15":1,"19":1,"20":1,"23":1,"24":1,"31":2,"36":1,"46":1,"47":1,"52":1,"65":1,"84":1,"86":1,"90":1,"100":1,"101":2,"102":1,"156":7}}],["juliaopentagger",{"2":{"160":1}}],["juliaopenai",{"2":{"156":3}}],["juliaollama",{"2":{"156":1}}],["juliaobj",{"2":{"102":1}}],["juliaoutput",{"2":{"50":1}}],["juliaout",{"2":{"20":1,"51":2,"157":2}}],["juliaload",{"2":{"156":2,"160":1}}],["julialocalserveropenaischema",{"2":{"156":1}}],["juliallmleaderboard",{"2":{"87":1}}],["julialength",{"2":{"66":1,"156":1}}],["julialanguage",{"2":{"106":1}}],["julialang",{"2":{"56":1,"65":1,"66":1,"156":1}}],["juliawrap",{"2":{"66":2,"156":2}}],["juliaalign",{"2":{"160":1}}],["juliaalternative",{"2":{"156":1}}],["juliaadvancedretriever",{"2":{"160":1}}],["juliaadvancedgenerator",{"2":{"160":1}}],["juliaadd",{"2":{"157":1,"160":1}}],["juliaabstractretriever",{"2":{"160":1}}],["juliaabstractmultiindex",{"2":{"160":1}}],["juliaabstractindexbuilder",{"2":{"160":1}}],["juliaabstractgenerator",{"2":{"160":1}}],["juliaabstractchunkindex",{"2":{"160":1}}],["juliaabstractcandidatechunks",{"2":{"160":1}}],["juliaagenttools",{"2":{"157":1}}],["juliaa=1",{"2":{"156":1}}],["juliaaai",{"2":{"156":1}}],["juliaauth",{"2":{"156":1}}],["juliaa",{"2":{"156":2}}],["juliaapi",{"2":{"156":2}}],["juliaanswer",{"2":{"160":1}}],["juliaanytagfilter",{"2":{"160":1}}],["juliaanthropic",{"2":{"156":1}}],["juliaanthropicschema",{"2":{"156":1}}],["juliaannotatednode",{"2":{"160":1}}],["juliaannotater",{"2":{"63":1,"160":1}}],["juliaannotate",{"2":{"63":2,"160":2}}],["juliaassume",{"2":{"63":1,"160":1}}],["juliaaiscan",{"2":{"156":2,"157":1}}],["juliaaiimage",{"2":{"156":1}}],["juliaaiextract",{"2":{"156":1,"157":1}}],["juliaaiembed",{"2":{"29":1,"30":1,"156":2,"157":1}}],["juliaaitemplate",{"2":{"156":1}}],["juliaaitemplates",{"2":{"85":1,"156":2}}],["juliaaimessage",{"2":{"156":1}}],["juliaairag",{"2":{"63":1,"160":1}}],["juliaairetry",{"2":{"51":1,"157":1}}],["juliaaicodefixer",{"2":{"51":1,"157":2}}],["juliaaicode",{"2":{"51":1,"156":1}}],["juliaaicall",{"2":{"51":3,"157":6}}],["juliaaiclassify",{"2":{"16":2,"156":4,"157":1}}],["juliaaigenerate",{"2":{"51":1,"85":2,"156":7,"157":1}}],["juliaai",{"2":{"33":1,"83":1,"91":2,"156":1}}],["juliaindex",{"2":{"57":1,"63":1,"160":3}}],["juliaragresult",{"2":{"160":1}}],["juliaragconfig",{"2":{"160":1}}],["juliaragtools",{"2":{"160":1}}],["juliarun",{"2":{"157":2,"160":2}}],["juliarerank",{"2":{"160":1}}],["juliarefine",{"2":{"160":2}}],["juliaretryconfig",{"2":{"157":1}}],["juliaretrieve",{"2":{"63":1,"160":1}}],["juliaretriever",{"2":{"61":1,"63":2,"160":2}}],["juliarender",{"2":{"156":6}}],["juliarendered",{"2":{"101":1}}],["juliaremove",{"2":{"156":1}}],["juliaregister",{"2":{"156":2}}],["juliarecaptask",{"0":{"148":1}}],["juliarecapcottask",{"0":{"147":1}}],["juliarecursive",{"2":{"66":2,"156":2}}],["juliareplace",{"2":{"66":1,"156":1}}],["juliarephrase",{"2":{"61":1,"160":3}}],["juliaresponse",{"2":{"156":1}}],["juliaresize",{"2":{"156":2}}],["juliares",{"2":{"63":1,"160":1}}],["juliaresult",{"2":{"57":1,"100":1,"102":1,"156":4}}],["juliaresults",{"2":{"6":1}}],["juliar",{"2":{"54":2,"158":2}}],["juliafunction",{"2":{"156":1}}],["juliafind",{"2":{"156":2,"160":4}}],["juliafinalize",{"2":{"156":1}}],["juliafireworksopenaischema",{"2":{"156":1}}],["juliafilechunker",{"2":{"160":1}}],["juliafilename",{"2":{"23":1}}],["juliafiles",{"2":{"1":1}}],["juliafeedback",{"2":{"102":1}}],["juliafor",{"2":{"51":1,"157":1}}],["juliasplit",{"2":{"160":1}}],["juliaspec",{"2":{"156":1}}],["juliastyler",{"2":{"160":1}}],["juliasimpleretriever",{"2":{"160":1}}],["juliasimplerephraser",{"2":{"160":1}}],["juliasimplerefiner",{"2":{"160":1}}],["juliasimpleindexer",{"2":{"160":1}}],["juliasimplegenerator",{"2":{"160":1}}],["juliasimpleanswerer",{"2":{"160":1}}],["juliasig",{"2":{"102":3}}],["juliasample",{"2":{"157":1}}],["juliasamplenode",{"2":{"157":1}}],["juliasave",{"2":{"156":3}}],["juliasharegptschema",{"2":{"156":1}}],["juliaselect",{"2":{"157":1}}],["juliaset",{"2":{"156":1,"160":1}}],["juliasentences",{"2":{"57":1}}],["juliaserialize",{"2":{"1":1}}],["juliaschema",{"2":{"41":1}}],["juliamessages",{"2":{"156":1}}],["juliamarkdown",{"2":{"156":1}}],["juliamistralopenaischema",{"2":{"156":1}}],["juliamodelspec",{"2":{"156":1}}],["juliamodel",{"2":{"39":1,"102":2,"156":2}}],["juliamsgs",{"2":{"23":1}}],["juliamsg",{"2":{"11":1,"12":2,"19":1,"21":1,"22":1,"23":1,"25":1,"27":1,"28":1,"29":1,"30":1,"33":1,"38":1,"42":1,"44":2,"45":1,"46":1,"51":1,"63":1,"92":1,"156":10,"160":2}}],["juliadistance",{"2":{"156":1}}],["juliadetect",{"2":{"156":1}}],["juliadecode",{"2":{"156":1}}],["juliadatabricksopenaischema",{"2":{"156":1}}],["juliadatamessage",{"2":{"156":1}}],["juliadataexpertask",{"2":{"23":2}}],["juliadoc",{"2":{"63":1,"160":1}}],["juliadocs",{"2":{"45":1}}],["juliadf",{"2":{"6":1}}],["juliapassthroughtagger",{"2":{"160":1}}],["juliapackage",{"2":{"106":1}}],["juliapush",{"2":{"156":1}}],["juliapprint",{"2":{"57":1,"156":2}}],["juliapreferences",{"2":{"156":1}}],["juliaprompt",{"2":{"102":1}}],["juliapromptingtools",{"2":{"59":1,"156":5,"160":1}}],["juliaprompts",{"2":{"13":1}}],["juliaprint",{"2":{"51":1,"157":1,"160":1}}],["juliapt",{"2":{"23":2,"41":1,"63":1,"85":2,"156":4,"160":2}}],["juliachunkindex",{"2":{"160":1}}],["juliachoices",{"2":{"17":1,"84":1,"156":4}}],["juliacandidatechunks",{"2":{"160":1}}],["juliacall",{"2":{"156":1}}],["juliacustomopenaischema",{"2":{"156":1}}],["juliacfg",{"2":{"61":1,"63":1,"160":1}}],["juliacreate",{"2":{"54":1,"156":1,"158":1}}],["juliacohere",{"2":{"160":1}}],["juliacoherereranker",{"2":{"160":1}}],["juliacosinesimilarity",{"2":{"160":1}}],["juliacountry",{"2":{"91":1}}],["juliacommands",{"2":{"66":1,"156":1}}],["juliacode",{"2":{"51":2,"156":4}}],["juliaconv",{"2":{"156":1}}],["juliaconversation",{"2":{"34":1,"40":1,"83":1}}],["juliacontextenumerator",{"2":{"160":1}}],["juliacontext",{"2":{"66":1,"156":1}}],["juliaconst",{"2":{"14":1,"21":1,"22":1,"25":1,"156":15}}],["juliatokenize",{"2":{"160":1}}],["juliatoken",{"2":{"160":1}}],["juliatogetheropenaischema",{"2":{"156":1}}],["juliatags",{"2":{"160":1}}],["juliatavily",{"2":{"158":1}}],["juliatrigrams",{"2":{"160":1}}],["juliatrigram",{"2":{"160":1}}],["juliatrigramannotater",{"2":{"160":1}}],["juliatruncate",{"2":{"157":1}}],["juliatryparse",{"2":{"156":1}}],["juliathompsonsampling",{"2":{"157":1}}],["juliatypeof",{"2":{"156":5}}],["juliatemplate",{"2":{"101":1}}],["juliatextchunker",{"2":{"160":1}}],["juliatext1",{"2":{"66":1,"156":1}}],["juliatext",{"2":{"15":1,"66":7,"156":7}}],["juliatpl",{"2":{"23":1}}],["juliatmps",{"2":{"12":2,"23":1,"156":4}}],["juliajulia>",{"2":{"12":1,"51":1,"156":1,"157":1}}],["juliaexperimental",{"2":{"159":1}}],["juliaexperttask",{"2":{"156":1}}],["juliaexperttestcodexml",{"0":{"153":1}}],["juliaexperttestcode",{"0":{"146":1}}],["juliaexpertcottaskxml",{"0":{"152":1}}],["juliaexpertcottask",{"0":{"145":1}}],["juliaexpertaskxml",{"0":{"151":1}}],["juliaexpertask",{"0":{"144":1},"2":{"12":4,"23":6,"51":1,"100":2,"156":6,"157":1}}],["juliaextract",{"2":{"156":5}}],["juliaevaluate",{"2":{"157":1}}],["juliaeval",{"2":{"156":1}}],["juliaevals",{"2":{"3":1,"4":1}}],["juliaencode",{"2":{"156":1}}],["juliaenv",{"2":{"76":1}}],["juliaerror",{"2":{"51":1,"157":1}}],["juliax",{"2":{"5":1,"156":1}}],["julia>",{"2":{"4":1}}],["julia",{"2":{"1":3,"3":1,"5":1,"6":1,"9":1,"12":6,"18":2,"19":3,"20":2,"22":1,"23":14,"26":1,"28":1,"30":1,"42":1,"48":1,"50":2,"51":9,"55":1,"56":1,"57":26,"63":3,"66":1,"72":6,"73":1,"76":3,"79":1,"84":2,"85":1,"89":3,"98":1,"99":1,"100":1,"101":2,"102":2,"106":2,"115":4,"120":4,"121":2,"122":1,"144":2,"145":2,"146":3,"147":7,"148":8,"151":2,"152":2,"153":3,"156":66,"157":12,"160":7}}],["jls",{"2":{"1":2}}],["jl",{"0":{"77":1},"2":{"0":1,"1":3,"7":2,"9":1,"22":3,"23":1,"25":2,"26":1,"28":1,"29":1,"30":1,"46":1,"51":1,"57":3,"62":1,"63":1,"64":1,"66":3,"69":1,"76":2,"79":2,"80":1,"87":1,"89":2,"90":1,"93":1,"96":1,"100":1,"101":1,"102":1,"106":4,"156":17,"157":1,"160":3}}],["nfeedback",{"2":{"157":6}}],["n```",{"2":{"120":1,"121":1,"156":1}}],["nwhat",{"2":{"101":1}}],["nwe",{"2":{"6":1}}],["nparagraph",{"2":{"66":6,"86":2,"156":6}}],["n=5",{"2":{"63":1,"160":1}}],["n=2",{"2":{"20":1,"50":1,"51":1,"157":1}}],["nsfw",{"2":{"51":1,"157":1}}],["nsemijoin",{"2":{"6":1}}],["nbsp",{"2":{"51":9,"54":1,"63":6,"66":5,"156":120,"157":37,"158":2,"159":1,"160":88}}],["ngl",{"2":{"27":2}}],["nli",{"2":{"16":1}}],["nthreads",{"2":{"160":2}}],["nthe",{"2":{"6":1}}],["ntasks=2",{"2":{"73":2}}],["ntasks=1",{"2":{"63":1,"73":1,"160":2}}],["ntasks=10",{"2":{"13":1}}],["ntasks",{"2":{"63":1,"73":1,"160":3}}],["neighboring",{"2":{"160":1}}],["network",{"2":{"160":1}}],["never",{"2":{"156":1}}],["negative",{"2":{"146":1,"149":1,"153":1,"160":1}}],["nesting",{"2":{"146":1,"153":1}}],["nested",{"2":{"61":2,"63":2,"146":1,"153":1,"156":2,"160":2}}],["neuroplasticity",{"2":{"106":2}}],["nexample",{"2":{"102":1}}],["next",{"0":{"7":1},"2":{"20":1,"50":1,"51":1,"83":1,"101":2,"137":14,"143":1,"157":2,"160":1}}],["nearest",{"2":{"85":2,"156":2}}],["near",{"2":{"55":1}}],["news",{"2":{"156":1}}],["newline",{"2":{"66":3,"156":4,"160":1}}],["newlines",{"2":{"65":1,"66":2,"86":1,"156":3}}],["new",{"2":{"11":2,"23":2,"30":1,"51":5,"59":3,"63":1,"71":1,"72":1,"73":1,"81":1,"83":1,"89":1,"91":1,"99":1,"101":1,"109":2,"120":1,"121":1,"136":1,"137":1,"147":1,"148":1,"156":18,"157":5,"160":3}}],["necessary>",{"2":{"143":1}}],["necessary",{"2":{"9":1,"22":1,"26":1,"40":1,"48":1,"51":2,"55":1,"62":1,"93":1,"100":1,"136":1,"156":3,"157":1}}],["needing",{"2":{"111":1}}],["needs",{"2":{"51":1,"96":1,"102":1,"120":1,"156":1,"157":1}}],["needed>",{"2":{"143":1}}],["needed",{"2":{"9":1,"18":1,"20":1,"48":1,"50":1,"63":1,"84":1,"100":1,"109":2,"149":1,"156":5,"157":2,"160":1}}],["need",{"2":{"2":1,"3":2,"4":1,"6":1,"9":2,"10":2,"27":1,"31":1,"36":1,"38":1,"41":1,"50":1,"51":1,"53":1,"56":1,"57":3,"61":1,"65":1,"66":1,"73":1,"75":1,"76":1,"82":1,"84":2,"85":1,"89":2,"97":2,"100":2,"102":4,"120":1,"121":1,"122":1,"136":1,"137":1,"138":1,"145":1,"146":1,"148":1,"149":1,"152":1,"153":1,"156":22,"157":1}}],["n7",{"2":{"6":1}}],["n6",{"2":{"6":1}}],["n5",{"2":{"6":1}}],["numerical",{"2":{"57":1}}],["num",{"2":{"36":2,"157":6}}],["number",{"2":{"13":1,"27":1,"51":14,"54":1,"57":1,"63":8,"65":1,"66":2,"73":4,"84":10,"102":1,"146":1,"153":1,"156":19,"157":26,"158":1,"160":11}}],["numbers",{"2":{"1":1,"51":4,"146":2,"153":2,"156":3,"157":1}}],["null",{"2":{"6":1,"156":2}}],["n4",{"2":{"6":1}}],["n3",{"2":{"6":1}}],["n2",{"2":{"6":1}}],["n2×3",{"2":{"6":1}}],["n2×2",{"2":{"6":2}}],["n1",{"2":{"6":1}}],["njob",{"2":{"6":2}}],["njulia",{"2":{"6":1}}],["nice",{"2":{"12":1,"23":1,"38":1,"39":1,"41":1,"156":4}}],["nid",{"2":{"6":2}}],["nintroduction",{"2":{"6":1}}],["n─────┼───────────────",{"2":{"6":1}}],["n─────┼─────────────────────────",{"2":{"6":1}}],["n─────┼─────────────────",{"2":{"6":1}}],["naming",{"2":{"156":1}}],["name`",{"2":{"156":1}}],["named",{"2":{"146":1,"153":1,"156":12}}],["namedtuple=namedtuple",{"2":{"160":1}}],["namedtuples",{"2":{"147":1,"148":1}}],["namedtuple",{"2":{"9":2,"51":4,"63":51,"100":2,"156":62,"157":6,"158":2,"160":63}}],["namespace",{"2":{"90":1}}],["names",{"2":{"6":3,"26":1,"27":1,"57":1,"59":1,"61":1,"66":1,"102":2,"106":5,"147":1,"148":1,"156":14,"160":1}}],["name",{"2":{"6":6,"10":1,"11":2,"12":2,"14":1,"23":6,"27":1,"28":1,"29":1,"30":2,"36":1,"39":3,"41":1,"81":1,"82":1,"83":5,"85":7,"87":1,"99":2,"102":9,"106":1,"120":2,"143":1,"146":1,"153":1,"156":64,"157":1}}],["name=",{"2":{"6":1,"11":1,"82":1,"85":2,"147":1,"148":1,"156":3}}],["narrative",{"2":{"149":2}}],["nature",{"2":{"137":1,"156":1}}],["natural",{"2":{"57":1,"118":1,"156":1}}],["native",{"2":{"102":1}}],["navigate",{"2":{"0":1}}],["n",{"2":{"6":17,"12":2,"20":2,"23":8,"50":2,"51":7,"61":2,"63":8,"66":24,"86":10,"99":2,"101":4,"102":4,"120":1,"121":1,"156":30,"157":18,"160":16}}],["noschema",{"2":{"101":1,"156":3}}],["nopostprocessor",{"2":{"63":2,"156":1,"160":6}}],["noreranker",{"2":{"156":1,"160":3}}],["norephraser",{"2":{"156":1,"160":4}}],["norefiner",{"2":{"63":3,"156":1,"160":7}}],["normal",{"2":{"156":1}}],["normalization",{"2":{"46":1}}],["normalized",{"2":{"15":1,"65":1,"66":1,"156":3}}],["normalize",{"2":{"15":2,"46":2,"66":2,"156":8,"160":3}}],["norm",{"2":{"66":2,"156":2}}],["nodes",{"2":{"51":1,"63":5,"157":4,"160":15}}],["node",{"2":{"51":5,"63":3,"156":4,"157":31,"160":38}}],["nods",{"2":{"40":2}}],["nomic",{"2":{"30":2}}],["now",{"2":{"22":1,"23":1,"25":1,"31":1,"40":1,"51":2,"57":1,"75":1,"79":1,"84":1,"85":1,"87":1,"102":1,"156":6,"157":3,"160":1}}],["non",{"2":{"10":1,"20":1,"50":1,"51":1,"82":1,"156":10,"157":1,"160":1}}],["none",{"2":{"3":1,"20":1,"50":1,"63":2,"106":1,"107":1,"111":1,"136":1,"137":1,"138":1,"146":1,"148":1,"149":1,"152":1,"153":1,"160":3}}],["no",{"2":{"9":1,"14":1,"19":1,"20":1,"50":1,"51":5,"57":3,"63":3,"100":1,"101":2,"120":1,"122":1,"137":1,"143":1,"156":13,"157":6,"160":17}}],["noise",{"2":{"1":1}}],["notfound",{"2":{"157":1}}],["notagfilter",{"2":{"156":1,"160":5}}],["notagger",{"2":{"63":2,"156":1,"160":8}}],["notation",{"2":{"51":1,"157":1}}],["notification",{"2":{"74":1}}],["notion",{"2":{"48":1}}],["notice",{"2":{"20":3,"21":1,"33":1,"50":2,"51":2,"57":1,"59":1,"61":2,"83":2,"84":1,"87":1,"91":1,"99":1,"101":1,"102":1,"156":5,"157":2}}],["nothing",{"2":{"5":1,"6":1,"11":1,"18":4,"30":1,"51":18,"63":7,"72":1,"84":1,"102":4,"109":2,"136":1,"156":82,"157":24,"160":29}}],["notes",{"2":{"51":2,"63":4,"66":2,"136":4,"137":1,"156":7,"157":5,"160":6}}],["notexist",{"2":{"20":1,"50":1,"51":2,"157":2}}],["noteworthy",{"2":{"9":1,"63":1,"160":1}}],["note",{"2":{"0":1,"5":1,"6":4,"18":1,"20":1,"22":1,"23":1,"26":1,"29":1,"30":1,"36":1,"41":2,"50":1,"51":2,"62":1,"63":1,"68":1,"85":1,"99":1,"101":1,"102":1,"143":1,"156":22,"157":4,"159":1,"160":2}}],["not",{"2":{"0":1,"4":1,"6":3,"9":3,"10":1,"11":2,"18":1,"21":1,"22":1,"23":2,"26":1,"31":1,"34":2,"35":2,"40":1,"41":2,"42":1,"48":1,"51":20,"54":1,"59":1,"63":1,"66":5,"68":1,"70":2,"71":2,"72":2,"74":1,"76":1,"81":1,"83":1,"84":4,"86":1,"89":3,"93":1,"100":3,"102":9,"111":1,"112":3,"118":1,"120":2,"136":5,"137":1,"138":1,"145":1,"146":1,"147":2,"148":2,"149":2,"153":1,"156":69,"157":23,"158":1,"159":2,"160":7}}],["photos",{"2":{"155":1}}],["phase",{"2":{"62":3}}],["phases",{"2":{"59":1}}],["pct",{"2":{"149":3}}],["python",{"2":{"42":1,"57":1,"147":1,"148":1,"156":2}}],["pkgdir",{"2":{"23":1}}],["pkg",{"2":{"23":2,"31":2,"51":1,"72":1,"90":2,"156":2}}],["png",{"2":{"19":2,"42":2,"156":10}}],["p",{"2":{"17":2,"84":4,"156":4,"157":2}}],["plots",{"2":{"106":2}}],["plural",{"2":{"87":1}}],["plus",{"2":{"16":2,"84":2,"156":4}}],["please",{"2":{"23":1,"72":1,"112":1,"157":1}}],["platform",{"2":{"156":1}}],["plausible",{"2":{"149":2}}],["plain",{"2":{"143":2,"160":1}}],["plaintextexplain",{"2":{"149":1}}],["plaintextextract",{"2":{"107":1}}],["plaintextuser",{"2":{"126":1}}],["plaintextusing",{"2":{"23":1}}],["plaintextignore",{"2":{"120":1}}],["plaintexthere",{"2":{"118":1}}],["plaintextoriginal",{"2":{"117":1}}],["plaintextquery",{"2":{"116":1}}],["plaintextwrite",{"2":{"115":1}}],["plaintextwe",{"2":{"109":1}}],["plaintextyou",{"2":{"106":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"126":1,"127":1,"129":1,"131":1,"134":1,"141":1,"142":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"151":1,"152":1,"153":1,"155":1}}],["plaintextact",{"2":{"104":1,"109":1,"136":1,"137":1,"143":1}}],["plaintextaimessage",{"2":{"11":1}}],["plaintext2",{"2":{"101":2}}],["plaintext>",{"2":{"11":1}}],["plaintext",{"2":{"10":1,"57":1,"91":2,"92":1,"104":1,"106":1,"107":1,"111":1,"112":1,"113":1,"120":1,"121":2,"122":2,"124":2,"127":1,"129":1,"131":1,"133":2,"134":1,"136":1,"137":1,"138":1,"140":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"151":1,"152":1,"153":1,"155":1}}],["placing",{"2":{"120":1,"121":1}}],["places",{"2":{"102":1,"106":1}}],["placeholder",{"2":{"23":1,"63":1,"85":1,"99":1,"101":1,"127":1,"129":1,"131":1,"134":1,"156":4,"160":1}}],["placeholders",{"0":{"92":1},"2":{"12":2,"23":4,"94":1,"99":1,"101":4,"102":2,"104":2,"106":2,"107":2,"109":2,"111":2,"112":2,"113":2,"115":2,"116":2,"117":2,"118":2,"120":2,"121":2,"122":2,"124":2,"126":2,"127":1,"129":1,"131":1,"133":2,"134":1,"136":2,"137":2,"138":2,"141":2,"142":2,"143":2,"144":2,"145":2,"146":2,"147":2,"148":2,"149":3,"151":2,"152":2,"153":2,"155":2,"156":8}}],["place",{"2":{"20":1,"51":1,"59":1,"96":1,"137":1,"156":1,"157":2}}],["plant",{"2":{"17":2,"84":1,"156":7}}],["plan",{"2":{"15":1,"22":1,"137":1,"156":2}}],["plays",{"2":{"137":1}}],["playing",{"2":{"136":1}}],["play",{"2":{"7":1,"20":1,"50":1,"51":2,"157":2}}],["pprint",{"2":{"9":1,"56":2,"57":1,"63":3,"156":9,"160":8}}],["penicillin",{"2":{"111":2}}],["perched",{"2":{"156":1}}],["permanently",{"2":{"156":1}}],["permanent",{"2":{"156":1}}],["persist",{"2":{"156":1}}],["persistently",{"2":{"82":1}}],["persists",{"2":{"77":1}}],["personality",{"2":{"156":1}}],["personal",{"2":{"81":1}}],["persona",{"0":{"135":1},"1":{"136":1,"137":1,"138":1},"2":{"23":1,"133":1,"134":1,"156":5}}],["personas",{"2":{"23":1}}],["person",{"2":{"6":2,"18":1,"156":2}}],["period",{"2":{"73":1}}],["perfectly",{"2":{"112":1}}],["perfect",{"2":{"66":1,"102":1,"115":1,"156":2}}],["performance",{"2":{"13":1,"57":2,"117":1,"147":1,"148":1}}],["perform",{"2":{"6":1}}],["per",{"2":{"63":1,"73":5,"75":3,"102":4,"106":1,"156":9,"157":1,"160":2}}],["perhaps",{"2":{"40":1}}],["perplexity",{"2":{"22":1,"26":1}}],["people",{"2":{"6":4,"92":1,"136":1,"156":1}}],["push",{"2":{"156":4}}],["published",{"2":{"57":1,"106":1}}],["pull",{"2":{"36":1,"81":2}}],["purposes",{"2":{"101":1}}],["purpose",{"2":{"4":2,"5":1,"6":6,"9":2,"99":1,"100":2,"102":1,"146":1,"153":1}}],["put",{"2":{"1":1,"51":1,"73":1,"102":1,"156":1,"157":1}}],["pipe",{"2":{"157":1}}],["pipelines",{"2":{"51":1,"157":2,"159":1}}],["pipeline",{"2":{"5":1,"55":1,"57":2,"59":2,"61":1,"63":8,"160":11}}],["pinpoint",{"2":{"137":1}}],["pinpointing",{"2":{"120":1}}],["pirate",{"2":{"85":4,"156":6}}],["piece",{"2":{"63":1,"160":3}}],["pieces",{"2":{"1":1,"62":1}}],["pick",{"2":{"7":1,"51":1,"96":1,"102":1,"126":1,"156":1,"157":1}}],["picture",{"2":{"4":2,"5":1,"6":2}}],["pop",{"2":{"157":1}}],["population=",{"2":{"92":1}}],["population",{"2":{"91":1,"92":4}}],["popular",{"2":{"80":1}}],["port",{"2":{"156":6}}],["porsche",{"2":{"84":2,"102":2}}],["points",{"2":{"112":1,"120":4,"136":9,"137":4,"143":1,"157":1}}],["point",{"2":{"59":1,"63":1,"85":2,"102":1,"111":1,"120":1,"136":2,"137":1,"143":2,"156":2,"160":1}}],["pose",{"2":{"137":1}}],["positive",{"2":{"75":1,"143":1,"146":1,"149":1,"153":1}}],["positions",{"2":{"156":8,"160":6}}],["position",{"2":{"66":1,"156":1,"160":8}}],["pos",{"2":{"66":4,"156":4}}],["post",{"2":{"156":3,"160":1}}],["postorderdfs",{"2":{"51":3,"157":8}}],["postprocessor",{"2":{"63":8,"160":11}}],["postprocessing",{"0":{"46":1},"2":{"46":1,"62":1,"63":1,"160":3}}],["postprocess",{"2":{"44":1,"60":1,"63":3,"156":6,"160":5}}],["possibly",{"2":{"160":1}}],["possible",{"2":{"6":1,"51":1,"66":4,"115":1,"116":2,"136":1,"146":1,"153":1,"155":1,"156":6,"157":1,"160":4}}],["possess",{"2":{"11":1}}],["possessions",{"2":{"11":1,"34":1}}],["powerful",{"2":{"14":1,"20":1,"51":1,"55":1,"87":1,"102":2,"134":1,"156":1,"157":1,"160":1}}],["powered",{"2":{"13":1,"92":1}}],["power",{"2":{"11":1,"106":1}}],["poor",{"2":{"3":1}}],["potentially",{"2":{"62":1,"63":2,"160":2}}],["potential",{"2":{"1":2,"51":1,"62":1,"117":1,"149":1,"157":2}}],["practically",{"2":{"157":1}}],["practical",{"2":{"92":1,"102":1,"156":1}}],["practices",{"2":{"57":6,"63":1,"160":1}}],["practice",{"2":{"3":1,"6":1,"101":1}}],["pristine",{"2":{"156":1}}],["pricing",{"2":{"75":1}}],["price",{"2":{"75":1}}],["privacy",{"0":{"70":1},"2":{"68":1}}],["principles",{"2":{"59":1}}],["printed",{"2":{"63":1,"160":4}}],["printstyled",{"2":{"160":1}}],["prints",{"2":{"51":1,"63":1,"157":1,"160":3}}],["println",{"2":{"51":5,"156":7,"157":2}}],["print",{"2":{"23":1,"48":3,"51":6,"57":1,"63":3,"66":1,"156":17,"157":8,"160":18}}],["printing",{"2":{"9":1,"56":1,"57":2,"156":1,"160":1}}],["priority",{"2":{"156":1}}],["prioritizing",{"2":{"129":1,"131":1}}],["prioritize",{"2":{"106":1,"111":1,"137":1,"146":1,"147":1,"148":1,"153":1,"157":2}}],["prior",{"2":{"36":1}}],["pr",{"2":{"23":1}}],["predicts",{"2":{"149":1}}],["prediction",{"2":{"149":6}}],["predictions",{"2":{"149":2}}],["pre",{"2":{"99":1,"156":1}}],["prerequisites",{"0":{"89":1}}],["preset",{"2":{"156":3}}],["present",{"2":{"137":1,"138":1,"156":3}}],["preserve",{"2":{"66":1,"156":1}}],["preserving",{"2":{"66":2,"156":2}}],["press",{"2":{"23":2}}],["prepend",{"2":{"83":1}}],["prepended",{"2":{"51":1,"156":2}}],["preparing",{"2":{"111":1}}],["prepare",{"2":{"60":1,"61":1,"63":1,"160":2}}],["prepared",{"2":{"51":1,"157":1}}],["preparation",{"2":{"59":1,"62":1,"160":5}}],["prefences",{"2":{"156":1}}],["preference",{"2":{"156":5}}],["preferencesfor",{"2":{"156":1}}],["preferences",{"0":{"77":1},"2":{"22":2,"25":2,"41":1,"72":1,"77":3,"82":2,"156":34}}],["prefer",{"2":{"45":1,"66":2,"147":1,"148":1,"156":2}}],["prefix",{"2":{"51":3,"156":5}}],["preorderdfs",{"2":{"51":1,"157":3}}],["precedence",{"2":{"136":1,"140":1,"156":1}}],["preceding",{"2":{"1":1,"160":1}}],["precision",{"2":{"136":1,"155":1}}],["precisely",{"2":{"136":2,"140":1,"143":1,"145":1}}],["precise",{"2":{"23":3,"99":1,"101":2,"134":1,"141":1,"144":1,"151":1}}],["precompile",{"2":{"72":1}}],["precompiled",{"2":{"72":1}}],["precompilation",{"2":{"72":3}}],["prev",{"2":{"160":1}}],["previously",{"2":{"66":1,"85":1,"86":1,"99":1,"120":1,"156":1,"160":2}}],["previous",{"2":{"23":1,"51":4,"83":1,"91":1,"97":2,"109":1,"120":2,"136":1,"140":1,"156":5,"157":5}}],["previews",{"2":{"156":2}}],["preview",{"2":{"12":2,"14":2,"23":3,"85":2,"156":10,"157":3}}],["prevent",{"2":{"10":1,"74":1,"156":1}}],["pretty",{"2":{"9":1,"51":1,"56":1,"57":2,"63":2,"156":3,"157":1,"160":6}}],["proxy",{"2":{"157":1}}],["proposed",{"2":{"157":1}}],["propertynames",{"2":{"156":5}}],["property",{"2":{"156":5}}],["properties",{"2":{"51":2,"102":3,"156":2,"157":1,"160":1}}],["proper",{"2":{"51":1,"156":1,"157":1,"160":3}}],["properly",{"2":{"7":1}}],["professional",{"2":{"143":1}}],["proficient",{"2":{"23":2}}],["project",{"2":{"85":2,"87":1,"156":1}}],["projects",{"2":{"77":1}}],["prototyping",{"2":{"85":1}}],["programming",{"2":{"57":2}}],["programmatically",{"2":{"51":1,"156":1}}],["programmer",{"2":{"12":1,"23":4,"98":1,"144":1,"145":1,"146":1,"147":1,"148":1,"151":1,"152":1,"153":1,"156":2}}],["program",{"2":{"51":2,"84":2,"157":2}}],["promising",{"2":{"51":1,"157":1}}],["prompting",{"2":{"156":1}}],["promptingtools",{"2":{"0":4,"9":2,"11":1,"12":2,"14":2,"18":1,"20":1,"21":1,"22":4,"23":11,"24":3,"25":3,"26":1,"27":1,"28":1,"29":1,"30":1,"31":4,"36":3,"44":2,"45":1,"46":1,"47":2,"51":13,"52":1,"54":1,"55":2,"57":1,"63":6,"64":1,"65":1,"66":7,"72":5,"76":1,"77":3,"79":3,"80":1,"82":1,"83":4,"84":1,"85":5,"86":1,"87":1,"89":1,"90":4,"93":1,"100":1,"101":6,"102":2,"156":413,"157":72,"158":4,"159":3,"160":177}}],["promptengineerfortask",{"0":{"134":1}}],["prompts",{"0":{"11":1,"12":1,"34":1,"40":1,"97":1},"2":{"11":1,"12":2,"13":1,"16":2,"23":5,"73":1,"94":2,"97":1,"99":2,"156":8}}],["prompt",{"0":{"85":1,"99":1},"2":{"9":2,"16":3,"22":2,"23":2,"26":2,"30":2,"35":1,"41":3,"51":3,"59":1,"73":2,"82":2,"84":2,"85":3,"91":1,"92":1,"94":1,"97":1,"99":2,"100":2,"102":8,"104":2,"106":2,"107":2,"109":2,"111":2,"112":2,"113":3,"115":2,"116":2,"117":2,"118":2,"120":2,"121":2,"122":2,"124":2,"126":2,"127":2,"129":3,"131":2,"133":5,"134":9,"136":3,"137":3,"138":1,"140":1,"141":2,"142":2,"143":2,"144":2,"145":2,"146":2,"147":2,"148":2,"149":2,"151":3,"152":3,"153":3,"155":2,"156":214,"157":3,"160":3}}],["prob",{"2":{"156":4}}],["probabilities",{"2":{"156":1}}],["probability",{"2":{"149":4,"156":2}}],["probably",{"2":{"0":1,"10":2}}],["problems",{"2":{"66":2,"120":1,"145":1,"147":1,"148":1,"152":1,"156":2}}],["problem",{"2":{"40":1,"145":1,"147":1,"148":1}}],["produce",{"2":{"35":1}}],["production",{"2":{"159":1}}],["product",{"2":{"6":1,"15":1,"66":3,"156":5}}],["providing",{"0":{"42":1},"2":{"26":1,"27":1,"30":1,"42":1,"48":2,"63":2,"137":1,"157":2,"160":2}}],["provider",{"2":{"26":2,"95":1,"96":1,"101":1,"102":2}}],["providers",{"0":{"95":1},"2":{"22":1,"26":1,"63":1,"94":2,"95":2,"102":1,"156":2,"160":2}}],["provide",{"2":{"1":2,"3":1,"4":2,"5":1,"6":2,"9":4,"11":1,"17":1,"18":2,"20":1,"22":2,"25":2,"29":1,"34":1,"36":1,"42":1,"50":2,"51":10,"53":1,"56":1,"57":3,"59":1,"61":1,"63":4,"68":1,"72":1,"83":1,"85":1,"92":1,"96":1,"97":1,"99":2,"100":3,"102":6,"106":1,"107":2,"109":2,"111":1,"113":1,"120":3,"133":1,"136":2,"143":1,"156":33,"157":15,"160":11}}],["provides",{"2":{"1":1,"41":1,"47":1,"55":1,"64":1,"66":1,"106":1,"111":1,"137":1,"146":1,"147":1,"148":1,"153":1,"156":2,"157":3,"160":2}}],["provided",{"2":{"1":1,"6":1,"9":3,"14":1,"16":2,"19":1,"20":2,"23":2,"30":1,"48":1,"50":2,"51":7,"55":1,"57":2,"59":1,"62":3,"63":9,"65":1,"66":2,"85":1,"100":3,"101":1,"102":8,"104":2,"106":2,"107":2,"109":1,"111":4,"112":9,"113":2,"115":2,"116":1,"117":1,"120":2,"122":1,"126":1,"127":3,"129":1,"131":1,"136":1,"138":2,"140":1,"143":1,"145":2,"146":1,"149":2,"152":1,"153":2,"155":1,"156":71,"157":14,"160":35}}],["processed",{"2":{"66":1,"156":2}}],["processes",{"2":{"63":1,"66":1,"156":1,"157":1,"160":1}}],["process",{"2":{"20":1,"50":1,"63":2,"64":1,"73":2,"84":2,"87":2,"118":1,"157":4,"160":6}}],["processing",{"2":{"7":1,"57":2,"63":1,"66":1,"84":1,"91":1,"101":1,"156":3,"157":1,"160":1}}],["pro",{"2":{"13":1,"31":1,"32":1,"71":1,"91":1,"92":2,"156":2}}],["pt",{"2":{"0":1,"14":2,"21":2,"22":3,"23":6,"24":1,"25":2,"26":1,"27":1,"28":4,"29":2,"30":2,"31":1,"34":2,"36":5,"38":1,"40":2,"41":6,"46":1,"51":6,"63":9,"79":1,"82":5,"85":2,"87":2,"101":12,"102":3,"156":54,"157":19,"160":20}}],["palm",{"2":{"156":1}}],["payout",{"2":{"149":1}}],["payload",{"2":{"102":1}}],["paying",{"0":{"75":1},"2":{"75":1}}],["pay",{"2":{"73":1,"75":3}}],["painting",{"2":{"66":1,"156":1}}],["pair",{"0":{"4":1,"5":1},"2":{"156":2,"160":2}}],["pairs",{"0":{"3":1},"2":{"2":1,"5":1,"56":1,"63":3,"156":1,"160":3}}],["padding",{"2":{"66":1,"156":5,"160":1}}],["padawan",{"2":{"11":1,"34":1,"156":1}}],["pauses",{"2":{"40":1}}],["paper",{"2":{"20":2,"51":2,"116":1,"120":1,"157":2,"160":1}}],["page",{"2":{"7":1,"23":1,"31":1,"46":1,"70":1,"71":1,"89":1,"156":2}}],["pages",{"2":{"1":3,"10":1,"155":1}}],["paris",{"2":{"91":1,"160":3}}],["parents",{"2":{"157":1}}],["parent",{"2":{"63":1,"157":4,"160":15}}],["param2",{"2":{"160":1}}],["param1",{"2":{"160":1}}],["parameter",{"2":{"63":1,"101":1,"102":3,"156":5,"157":1,"160":3}}],["parameters",{"2":{"1":1,"5":2,"6":3,"9":2,"51":1,"63":16,"100":2,"102":5,"156":7,"157":3,"160":29}}],["paragraphs",{"2":{"66":2,"86":1,"156":2}}],["paragraph",{"2":{"66":3,"86":1,"156":3}}],["parallelism",{"2":{"57":1}}],["parallel",{"2":{"51":1,"57":7,"63":1,"157":1,"160":1}}],["paralellize",{"2":{"45":1}}],["parts",{"2":{"56":1,"137":1,"157":1}}],["particular",{"2":{"63":1,"64":1,"149":1,"156":1,"160":1}}],["particularly",{"2":{"51":1,"66":2,"129":1,"131":1,"156":2,"157":2}}],["partially",{"2":{"156":2}}],["partial",{"2":{"23":2,"57":1,"156":2,"160":1}}],["part",{"2":{"11":1,"51":1,"62":2,"63":2,"136":2,"157":1,"160":2}}],["parseable",{"2":{"84":2}}],["parses",{"2":{"51":1,"156":1}}],["parser",{"2":{"51":1,"156":1}}],["parse",{"2":{"19":1,"51":2,"75":1,"156":4,"157":1}}],["parsed",{"2":{"7":1,"51":6,"102":1,"156":8}}],["parsing",{"2":{"6":1,"18":1,"51":6,"156":7,"157":1}}],["past",{"2":{"83":2,"120":5,"156":1,"157":2}}],["paste",{"2":{"1":1}}],["passage",{"2":{"115":3,"116":3,"160":2}}],["passtroughtagger",{"2":{"63":1,"160":1}}],["passthroughtagger",{"2":{"63":3,"156":1,"160":6}}],["passthrough",{"2":{"62":1,"160":3}}],["pass",{"2":{"51":1,"59":1,"61":3,"63":3,"83":1,"156":8,"157":1,"160":8}}],["passing",{"0":{"61":1},"2":{"51":1,"157":1}}],["passes",{"2":{"20":1,"50":1,"160":4}}],["passed",{"2":{"5":1,"6":4,"51":2,"57":1,"59":2,"63":8,"84":1,"156":6,"157":8,"160":8}}],["patience",{"2":{"40":1}}],["pathways",{"2":{"146":1,"153":1}}],["path=",{"2":{"19":1,"42":1,"156":4}}],["path",{"2":{"11":1,"34":2,"40":1,"42":2,"156":29}}],["paths",{"2":{"3":1,"63":4,"156":2,"160":6}}],["patterns",{"2":{"156":3}}],["pattern",{"2":{"0":1,"10":1,"156":4}}],["packages",{"2":{"12":2,"23":2,"51":1,"57":1,"106":1,"147":1,"148":1,"156":4,"159":1,"160":2}}],["package",{"2":{"0":1,"1":1,"9":1,"23":10,"31":1,"36":1,"51":1,"55":1,"72":1,"90":1,"93":2,"96":1,"100":1,"106":1,"156":8,"157":1,"160":3}}],["cn",{"2":{"66":1,"156":1}}],["cfg",{"2":{"60":1,"61":1,"63":7,"160":7}}],["cb",{"2":{"51":12,"156":23}}],["cpp",{"0":{"27":1},"2":{"24":1,"27":3,"69":1,"94":1,"95":1,"156":1}}],["c",{"2":{"18":1,"27":2,"66":1,"73":2,"74":2,"156":1,"157":1}}],["city",{"2":{"18":3}}],["ceo",{"2":{"138":1}}],["cents",{"2":{"75":2}}],["cent",{"2":{"75":1}}],["celestial",{"2":{"66":2,"156":2}}],["celsius",{"2":{"18":2}}],["certainly",{"2":{"66":1,"156":1}}],["certain",{"2":{"14":1,"84":1}}],["curr",{"2":{"160":2}}],["currently",{"2":{"22":1,"25":1,"51":1,"53":1,"60":1,"81":1,"156":7,"157":2,"160":1}}],["currentweather",{"2":{"18":2}}],["current",{"2":{"18":2,"36":1,"51":1,"84":1,"90":1,"156":2,"157":3,"160":1}}],["curiosity",{"2":{"156":1}}],["customizing",{"2":{"157":1}}],["customized",{"2":{"136":1}}],["customize",{"2":{"51":1,"57":1,"59":4,"63":6,"82":1,"157":1,"160":8}}],["custom",{"0":{"24":1,"41":1},"1":{"25":1,"26":1,"27":1,"28":1,"29":1,"30":1},"2":{"51":1,"55":1,"59":5,"61":5,"63":13,"66":2,"84":2,"156":5,"157":1,"160":14}}],["customopenaischema",{"2":{"22":2,"26":2,"27":1,"156":7}}],["cut",{"2":{"14":1}}],["craft",{"2":{"143":1,"149":1}}],["criticism",{"2":{"120":1}}],["critique>",{"2":{"120":1}}],["critiques",{"2":{"120":1}}],["critique",{"2":{"63":1,"120":12,"160":1}}],["criterion",{"2":{"112":1}}],["criteria",{"2":{"112":2,"160":1}}],["credit",{"2":{"102":1,"156":1}}],["credits",{"2":{"69":1}}],["creativity",{"2":{"156":1}}],["creative",{"2":{"9":1,"100":1}}],["creation",{"2":{"111":1}}],["creating",{"0":{"71":1},"2":{"57":1,"63":1,"143":1,"160":2}}],["creature",{"2":{"17":2,"84":1,"156":4}}],["createqafromcontext",{"2":{"63":1,"160":1}}],["creates",{"2":{"51":1,"62":1,"85":1,"156":3,"157":5}}],["create",{"0":{"85":1},"2":{"1":2,"9":1,"15":1,"23":2,"48":1,"51":1,"53":1,"54":3,"59":1,"63":3,"71":2,"85":6,"87":1,"89":2,"94":1,"99":3,"100":1,"101":1,"102":1,"145":1,"152":1,"156":22,"157":3,"158":4,"160":4}}],["crunchy",{"2":{"30":2,"102":2}}],["cross",{"2":{"6":1}}],["cababcab",{"2":{"156":1}}],["cache",{"2":{"72":1}}],["caused",{"2":{"120":1}}],["causes",{"2":{"40":1}}],["caught",{"2":{"51":1,"156":1,"157":1}}],["captioning",{"2":{"156":2}}],["captain",{"2":{"85":2,"156":2}}],["capturing",{"2":{"51":3,"155":1,"156":3}}],["captured",{"2":{"51":3,"156":3}}],["capture",{"2":{"51":4,"102":1,"156":10}}],["capabilities",{"2":{"51":1,"68":1,"156":1}}],["capital",{"2":{"14":1,"63":2,"91":5,"92":2,"101":4,"160":5}}],["casual",{"2":{"143":1}}],["cases",{"2":{"51":1,"146":1,"153":1,"156":3,"157":1}}],["case",{"2":{"23":1,"57":1,"68":1,"84":2,"87":1,"101":1,"106":1,"143":1,"156":2,"160":1}}],["castle",{"2":{"17":1,"156":1}}],["carries",{"2":{"147":1,"148":1}}],["carrying",{"2":{"66":1,"156":1}}],["carefully",{"2":{"106":1,"113":1,"138":1}}],["care",{"2":{"96":1}}],["car",{"2":{"84":2,"102":1,"156":7}}],["carve",{"2":{"55":1}}],["carlo",{"2":{"20":1,"48":1,"51":1,"157":3}}],["cartesian",{"2":{"6":1}}],["cat",{"2":{"156":7}}],["categorize",{"2":{"138":1}}],["categories",{"0":{"17":1},"2":{"17":1,"84":2,"106":1,"107":1,"156":2}}],["category",{"2":{"84":2,"106":1,"107":1,"156":1,"160":1}}],["catch",{"2":{"18":1,"20":2,"48":1,"50":2,"51":4,"102":1,"157":9}}],["ca",{"2":{"18":1}}],["camelcase",{"2":{"20":1,"156":1}}],["came",{"2":{"9":1}}],["calculating",{"2":{"156":1}}],["calculation",{"2":{"156":1}}],["calculated",{"2":{"73":1,"149":1,"156":2}}],["calculate",{"2":{"6":1,"15":2,"156":10}}],["calling",{"2":{"23":1,"30":1,"36":1,"48":1,"102":2,"129":1,"131":1,"156":2,"157":1}}],["calls",{"0":{"50":1},"2":{"9":1,"20":4,"45":1,"48":2,"50":1,"51":6,"54":1,"61":1,"63":10,"70":1,"84":1,"100":1,"102":2,"129":1,"131":1,"156":6,"157":13,"158":1,"159":1,"160":17}}],["call",{"2":{"9":2,"10":1,"14":1,"20":2,"23":1,"28":1,"48":3,"50":2,"51":15,"62":1,"63":10,"72":1,"75":1,"83":1,"85":2,"94":1,"99":1,"100":2,"102":2,"129":1,"131":1,"156":42,"157":44,"160":14}}],["called",{"2":{"6":3,"9":1,"10":1,"20":1,"23":1,"48":1,"51":1,"62":4,"85":1,"91":1,"94":1,"100":1,"156":3,"157":4,"160":2}}],["cannot",{"0":{"69":1,"72":2},"2":{"16":1,"63":1,"66":1,"68":1,"102":2,"127":1,"156":6,"160":2}}],["candidatechunks",{"2":{"57":4,"62":1,"63":1,"156":1,"160":12}}],["candidate",{"2":{"1":1,"160":13}}],["candidates",{"2":{"1":1,"57":4,"59":1,"62":3,"63":2,"160":25}}],["can",{"2":{"1":1,"5":2,"6":7,"7":1,"9":4,"10":1,"11":4,"12":2,"13":2,"14":5,"15":2,"16":2,"17":2,"18":2,"19":4,"20":8,"21":3,"22":9,"23":11,"25":7,"26":1,"27":2,"28":3,"29":5,"30":4,"31":1,"32":1,"34":2,"38":1,"39":1,"41":2,"42":1,"45":2,"48":2,"50":7,"51":22,"54":2,"57":4,"59":1,"61":2,"62":2,"63":9,"65":1,"66":4,"68":1,"70":2,"71":1,"72":3,"73":2,"74":1,"75":2,"76":1,"77":1,"79":2,"81":2,"82":3,"83":3,"84":6,"85":8,"87":2,"89":1,"90":1,"91":1,"92":2,"93":1,"94":2,"95":2,"97":2,"99":3,"100":3,"101":1,"102":10,"106":1,"111":1,"120":1,"121":1,"122":1,"136":1,"146":1,"153":1,"156":105,"157":23,"158":2,"160":19}}],["ctx",{"2":{"5":6,"6":2,"160":4}}],["click",{"2":{"71":1,"89":1}}],["clipboard",{"2":{"51":2,"156":2}}],["clearly",{"2":{"59":1,"137":1,"138":1}}],["clear",{"2":{"51":1,"111":1,"112":3,"113":1,"120":1,"134":1,"136":1,"137":2,"138":1,"143":2,"157":1}}],["cleaning",{"2":{"142":1}}],["cleanup",{"2":{"23":1}}],["cleaner",{"2":{"23":1,"84":1}}],["cleaned",{"2":{"0":1}}],["clustering",{"2":{"15":1}}],["closely",{"2":{"66":2,"155":1,"156":2}}],["close",{"2":{"66":2,"143":1,"156":2}}],["closest",{"2":{"1":5,"60":1,"62":1,"63":4,"66":4,"156":6,"160":20}}],["cloud",{"2":{"11":1}}],["claudes",{"2":{"156":1}}],["claudeo",{"2":{"156":1}}],["claudeh",{"2":{"156":6}}],["claude",{"2":{"156":7}}],["classes=",{"2":{"160":3}}],["classes",{"2":{"160":5}}],["classifies",{"2":{"156":2}}],["classified",{"2":{"149":3,"156":1}}],["classification",{"0":{"16":1,"125":1},"1":{"17":1,"126":1,"127":1},"2":{"17":1,"126":2,"127":1,"156":3}}],["classify",{"2":{"9":1,"16":1,"100":1,"156":3}}],["class",{"2":{"12":1,"23":4,"98":1,"99":1,"101":2,"102":1,"104":1,"106":1,"109":1,"111":1,"115":1,"116":1,"117":1,"126":1,"129":1,"131":1,"134":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"151":1,"152":1,"153":1,"155":1,"156":2,"160":1}}],["clarity",{"2":{"5":1,"112":1,"137":3,"147":1,"148":1}}],["child2",{"2":{"157":2}}],["child11",{"2":{"157":2}}],["child1",{"2":{"157":3}}],["children",{"2":{"63":2,"157":2,"160":9}}],["chuckles",{"2":{"40":2}}],["chunked",{"2":{"63":1,"160":6}}],["chunkers",{"2":{"160":1}}],["chunker=filechunker",{"2":{"63":1,"160":1}}],["chunker",{"2":{"57":3,"59":2,"63":11,"160":21}}],["chunking",{"2":{"59":1,"62":1,"63":1,"160":2}}],["chunkindex",{"2":{"56":1,"63":3,"156":1,"160":8}}],["chunk",{"2":{"1":3,"7":1,"9":1,"57":2,"62":3,"63":6,"66":8,"86":1,"106":1,"111":4,"156":8,"160":16}}],["chunks",{"2":{"1":13,"2":1,"3":2,"7":1,"56":2,"57":2,"59":3,"60":3,"62":5,"63":35,"66":13,"86":1,"156":14,"160":93}}],["choose",{"2":{"138":1,"156":1}}],["choice=",{"2":{"156":1}}],["choice",{"2":{"102":1,"156":12,"157":1}}],["choices",{"2":{"9":3,"17":2,"84":2,"100":3,"101":1,"126":6,"156":40}}],["chosen",{"2":{"20":1,"50":1,"51":1,"157":1}}],["cheaper",{"2":{"20":1,"50":1,"51":1,"156":2,"157":1}}],["cheap",{"2":{"17":1,"156":1}}],["checkout",{"2":{"66":1,"156":1}}],["check",{"2":{"20":1,"21":1,"50":1,"51":7,"63":1,"70":1,"72":1,"76":1,"77":1,"81":1,"84":3,"87":1,"89":1,"102":2,"145":1,"147":1,"148":1,"152":1,"156":8,"157":5,"160":2}}],["checks",{"2":{"16":1,"50":1,"51":4,"75":1,"156":2,"157":6}}],["checking",{"2":{"16":1}}],["chain",{"2":{"145":1,"147":1,"152":1}}],["chapter",{"2":{"136":2}}],["chapters",{"2":{"136":4,"137":1}}],["chars",{"2":{"66":1,"156":1}}],["character",{"2":{"66":3,"155":1,"156":3,"160":1}}],["characters",{"2":{"35":1,"66":4,"120":1,"156":5,"157":4,"160":7}}],["charles",{"2":{"54":2,"158":2}}],["charge",{"2":{"35":2,"156":1}}],["chance",{"2":{"160":1}}],["chances",{"2":{"20":1,"50":1,"51":1,"157":1}}],["channel",{"2":{"56":1,"65":1}}],["changing",{"0":{"82":1},"2":{"51":1,"57":1,"59":1,"63":1,"156":1,"160":1}}],["changed",{"2":{"120":1}}],["changes",{"0":{"41":1},"2":{"36":1,"85":1,"156":1,"157":1,"160":3}}],["change",{"2":{"0":1,"20":1,"23":1,"41":1,"50":1,"51":1,"63":3,"102":1,"120":1,"156":7,"157":3,"159":1,"160":7}}],["chat1",{"2":{"156":1}}],["chatmlschema",{"2":{"156":5}}],["chat",{"2":{"28":3,"38":1,"41":1,"63":8,"82":2,"94":1,"101":2,"156":24,"160":20}}],["chatgpt",{"2":{"10":1}}],["chatbots",{"2":{"156":1}}],["chatbot",{"2":{"0":1,"1":1}}],["copies",{"2":{"160":1}}],["copy",{"2":{"1":1,"21":1,"44":2,"51":1,"156":3}}],["coding",{"2":{"157":1}}],["codeunits",{"2":{"156":3}}],["code>",{"2":{"120":1,"153":6}}],["codefixer",{"2":{"157":4}}],["codefixertiny",{"0":{"122":1}}],["codefixershort",{"0":{"121":1}}],["codefixerrci",{"0":{"120":1},"2":{"157":1}}],["codefailedtimeout",{"2":{"51":1,"157":1}}],["codefailedeval",{"2":{"51":1,"157":1}}],["codefailedparse",{"2":{"51":1,"157":1}}],["codellama",{"2":{"81":1}}],["codesuccess",{"2":{"51":1,"157":1}}],["codeempty",{"2":{"51":1,"157":1}}],["code",{"0":{"119":1},"1":{"120":1,"121":1,"122":1},"2":{"19":3,"22":1,"23":2,"25":1,"47":1,"48":1,"51":49,"55":1,"63":4,"65":2,"84":1,"98":1,"106":1,"120":13,"121":9,"122":3,"145":3,"146":12,"152":3,"153":10,"156":94,"157":36,"159":1,"160":13}}],["coalitional",{"2":{"149":1}}],["cot",{"2":{"145":1,"147":1,"152":1}}],["covering",{"2":{"146":1,"153":1}}],["cover",{"2":{"112":1}}],["coversation",{"2":{"51":1,"157":1}}],["core",{"2":{"117":1,"157":1}}],["corpus",{"2":{"56":1}}],["corresponds",{"2":{"156":2}}],["correspondence",{"2":{"143":1}}],["correspond",{"2":{"57":1,"63":1,"157":1,"160":2}}],["corresponding",{"2":{"11":1,"29":1,"30":1,"48":2,"59":2,"61":1,"62":2,"63":4,"69":1,"102":2,"136":1,"137":1,"138":1,"149":1,"156":15,"160":8}}],["correctiverag",{"2":{"160":1}}],["correctly",{"2":{"149":3,"156":5}}],["correct",{"2":{"51":4,"84":1,"102":3,"120":1,"145":1,"146":4,"147":1,"148":1,"152":1,"153":4,"157":4}}],["collects",{"2":{"157":1}}],["collect",{"2":{"156":1,"157":2}}],["collection",{"2":{"23":1,"63":1,"160":1}}],["collaboration",{"2":{"143":1}}],["colorful",{"2":{"156":1}}],["colors",{"2":{"63":1,"160":1}}],["color",{"2":{"20":2,"50":2,"51":2,"57":3,"66":1,"156":6,"157":2,"160":8}}],["column",{"2":{"6":2,"156":2,"160":2}}],["columns",{"2":{"5":1,"6":4}}],["cosmic",{"2":{"66":1,"156":1}}],["cosinesimilarity",{"2":{"63":1,"156":1,"160":9}}],["cosine",{"2":{"15":2,"46":2,"156":4,"160":2}}],["cost2",{"2":{"156":2}}],["cost1",{"2":{"156":3}}],["costing",{"2":{"156":2}}],["costs",{"2":{"9":1,"63":1,"74":1,"100":1,"156":9,"160":1}}],["cost",{"0":{"75":1},"2":{"3":1,"10":1,"12":1,"19":2,"22":1,"23":1,"25":1,"29":1,"30":1,"57":1,"63":15,"68":1,"75":1,"83":1,"91":2,"92":1,"156":72,"160":35}}],["counds",{"2":{"157":1}}],["counts",{"2":{"160":1}}],["counted",{"2":{"157":1}}],["counter",{"2":{"63":3,"157":2,"160":6}}],["counterpart",{"2":{"48":1,"66":1,"156":1}}],["counterparts",{"2":{"9":1,"20":1,"48":1,"100":1}}],["country=",{"2":{"92":1}}],["country",{"2":{"91":1,"92":1,"160":4}}],["count",{"2":{"27":1,"36":1,"63":1,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":1,"121":1,"122":1,"124":1,"126":1,"127":1,"129":1,"131":1,"133":1,"134":1,"136":1,"137":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"151":1,"152":1,"153":1,"155":1,"156":5,"160":1}}],["couldn",{"2":{"156":1}}],["could",{"2":{"6":1,"7":1,"20":2,"33":1,"50":2,"51":2,"63":2,"66":2,"73":1,"102":1,"134":1,"156":5,"157":2,"160":2}}],["combination",{"2":{"156":1,"160":2}}],["combining",{"2":{"55":1}}],["combines",{"2":{"63":2,"160":2}}],["combined",{"2":{"6":1}}],["combine",{"2":{"4":1,"5":2,"6":5}}],["com",{"2":{"19":1,"53":2,"66":1,"156":10,"158":1,"160":4}}],["comes",{"2":{"87":1,"160":1}}],["come",{"2":{"12":1,"156":2}}],["commit",{"2":{"76":1,"89":1}}],["comments",{"2":{"75":1,"120":1,"121":1,"156":1}}],["comment",{"2":{"75":1,"146":1,"153":1}}],["commercial",{"2":{"75":1}}],["commands",{"2":{"66":4,"90":1,"156":4}}],["command",{"2":{"23":1,"27":1,"66":1,"97":1,"156":1}}],["community",{"2":{"23":1,"57":1}}],["communications",{"2":{"143":2}}],["communication",{"2":{"23":3,"94":1,"96":1,"98":1,"99":1,"101":2,"141":1,"142":1,"143":3,"144":1,"145":1,"151":1,"152":1,"156":1,"157":1}}],["commun",{"2":{"12":1,"23":1,"156":2}}],["commonly",{"2":{"156":1}}],["common",{"2":{"0":1,"6":2,"65":7,"66":17,"84":1,"87":1,"156":20,"157":1}}],["compelling",{"2":{"149":1}}],["complicated",{"2":{"84":2,"102":1}}],["complicity",{"2":{"66":1,"156":1}}],["complement",{"2":{"159":1}}],["completions",{"2":{"156":5}}],["completions`",{"2":{"156":1}}],["completion",{"2":{"156":5}}],["completeling",{"2":{"156":1}}],["completely",{"2":{"63":1,"65":1,"160":2}}],["completeness",{"2":{"5":1,"112":1}}],["complete",{"2":{"4":2,"5":1,"6":2,"112":1}}],["complex",{"2":{"11":1,"15":1,"16":1,"18":1,"20":1,"57":1,"92":1,"157":2}}],["compact",{"2":{"146":1,"153":1}}],["compass",{"2":{"85":2,"156":2}}],["company",{"2":{"66":2,"156":2}}],["companion",{"2":{"65":1}}],["comparing",{"2":{"66":1,"156":1}}],["comparison",{"2":{"57":2}}],["compared",{"2":{"160":1}}],["compare",{"2":{"65":1,"66":2,"156":2,"157":1}}],["compatibility",{"2":{"41":1,"156":6}}],["compatible",{"0":{"22":1,"26":1},"2":{"22":2,"24":1,"26":1,"156":2}}],["computes",{"2":{"160":1}}],["compute",{"2":{"66":1,"156":1}}],["computer",{"2":{"21":1,"22":1}}],["computational",{"2":{"57":1}}],["computing",{"2":{"57":8,"63":1,"160":1}}],["comprehensively",{"2":{"111":1,"146":1,"153":1}}],["comprehensive",{"2":{"57":2,"134":1,"136":1}}],["comprehension",{"0":{"19":1}}],["composite",{"2":{"160":1}}],["compose",{"2":{"111":1}}],["composed",{"2":{"6":1}}],["components",{"2":{"61":2,"63":1,"160":3}}],["component",{"2":{"51":1,"61":1,"63":1,"157":2,"160":1}}],["compiled",{"2":{"63":1,"72":3,"160":1}}],["compile",{"2":{"23":1,"27":1,"72":1}}],["coherence",{"2":{"156":1}}],["coherereranker",{"2":{"63":1,"156":1,"160":5}}],["cohere",{"2":{"1":1,"7":1,"63":2,"156":6,"160":18}}],["conv",{"2":{"102":2,"156":18}}],["conventions",{"2":{"156":1}}],["convention",{"2":{"156":1}}],["convenient",{"2":{"156":2}}],["convenience",{"2":{"1":1,"66":2,"156":4}}],["conversion",{"2":{"102":1}}],["conversation=myconversation",{"2":{"157":1}}],["conversational",{"2":{"156":1}}],["conversation2",{"2":{"87":1}}],["conversation1",{"2":{"87":1}}],["conversations",{"0":{"11":1,"83":1},"2":{"11":1,"34":1,"41":2,"57":2,"63":2,"87":3,"92":1,"156":24,"157":1,"160":7}}],["conversation",{"2":{"9":2,"11":6,"20":1,"29":1,"34":1,"40":1,"50":1,"51":27,"63":1,"83":11,"84":4,"85":1,"87":3,"91":1,"97":2,"100":2,"102":3,"156":177,"157":73,"160":4}}],["converting",{"2":{"118":1}}],["convert",{"2":{"101":2,"102":1,"156":4}}],["convey",{"2":{"94":1,"97":1}}],["confusion",{"2":{"40":1}}],["confident",{"2":{"23":3,"99":1,"101":2,"141":1,"144":1,"151":1}}],["confidence",{"2":{"9":1,"66":1,"156":1,"157":2}}],["config=retryconfig",{"2":{"84":1,"102":1}}],["configuring",{"0":{"76":1}}],["configuration",{"2":{"63":1,"76":2,"157":1,"160":2}}],["configurable",{"2":{"57":1}}],["config",{"2":{"20":2,"48":1,"50":2,"51":9,"79":1,"102":2,"156":1,"157":14}}],["connection",{"2":{"34":1}}],["conducted",{"2":{"157":1}}],["cond",{"2":{"20":1,"50":1,"51":6,"157":15}}],["conditions",{"2":{"156":1}}],["condition",{"2":{"20":4,"50":5,"51":16,"84":3,"102":3,"156":1,"157":31}}],["conclude",{"2":{"149":1}}],["conclusions",{"2":{"136":1}}],["concentrate",{"2":{"149":1}}],["concepts",{"0":{"94":1},"1":{"95":1,"96":1,"97":1,"98":1,"99":1,"100":1},"2":{"93":1,"94":1}}],["concept",{"2":{"15":1}}],["concrete",{"2":{"102":1}}],["concatenates",{"2":{"156":1}}],["concatenate",{"2":{"35":1,"51":1,"156":1}}],["concise",{"2":{"23":3,"98":1,"99":1,"101":2,"104":1,"109":1,"111":1,"117":1,"120":1,"136":3,"137":2,"140":1,"141":1,"142":1,"143":2,"144":1,"145":1,"151":1,"152":1,"156":5}}],["concurrent",{"2":{"13":1,"73":2}}],["concurrently",{"2":{"13":1,"92":1}}],["contrast",{"2":{"156":1}}],["control",{"2":{"84":1,"91":1,"147":1,"148":1,"160":2}}],["controlling",{"2":{"51":1,"157":1}}],["controlled",{"2":{"20":1}}],["contribute",{"2":{"57":1}}],["contribution",{"2":{"48":1,"149":1}}],["continuous",{"2":{"70":1}}],["continued",{"2":{"156":1}}],["continues>",{"2":{"156":5}}],["continue",{"2":{"51":1,"91":1,"156":6,"157":1}}],["continue>",{"2":{"19":1,"156":2}}],["continuing",{"2":{"10":1,"75":1}}],["contained",{"2":{"160":2}}],["container",{"2":{"156":1}}],["containing",{"2":{"9":3,"63":4,"100":3,"156":7,"157":1,"160":11}}],["contain",{"2":{"6":1,"9":1,"51":1,"100":1,"156":2,"157":1,"160":1}}],["contains",{"2":{"5":1,"6":8,"23":1,"31":1,"36":1,"57":1,"59":1,"65":1,"98":1,"156":3,"157":1,"159":1,"160":1}}],["contemporary",{"2":{"14":1,"156":1}}],["contents",{"2":{"160":2}}],["content=",{"2":{"156":8}}],["content",{"2":{"5":1,"9":11,"15":3,"18":2,"19":2,"20":1,"21":2,"22":1,"26":1,"30":1,"44":1,"45":1,"46":2,"50":1,"51":5,"54":2,"63":1,"84":3,"91":2,"100":11,"101":5,"102":3,"136":1,"137":2,"156":77,"157":10,"158":2,"160":6}}],["context=true",{"2":{"160":1}}],["context=",{"2":{"160":4}}],["contexts",{"2":{"157":1}}],["contextual",{"2":{"63":1,"111":1,"160":2}}],["contextenumerator",{"2":{"63":3,"156":1,"160":11}}],["contexter",{"2":{"63":8,"160":14}}],["context",{"2":{"1":2,"4":1,"5":3,"7":3,"9":2,"10":2,"27":1,"51":1,"53":1,"57":6,"59":1,"60":1,"62":5,"63":32,"65":1,"66":9,"97":2,"104":7,"109":11,"111":11,"112":8,"113":6,"134":1,"137":1,"138":1,"149":1,"156":14,"157":4,"160":95}}],["consecutive",{"2":{"160":2}}],["conservative",{"2":{"35":1,"156":1}}],["consumer",{"2":{"70":1}}],["consuming",{"2":{"2":1}}],["considered",{"2":{"63":1,"66":1,"156":1,"160":2}}],["considering",{"2":{"57":1,"112":1,"156":2,"160":1}}],["consider",{"2":{"23":1,"63":2,"149":1,"156":1,"160":2}}],["consistent",{"2":{"112":2,"113":1,"160":1}}],["consistency",{"2":{"5":1,"35":1,"112":1,"156":2}}],["consisting",{"2":{"19":1,"156":2}}],["consists",{"2":{"6":1}}],["constant",{"2":{"156":9}}],["construct",{"2":{"156":1}}],["constructs",{"2":{"62":1}}],["constraints",{"2":{"66":1,"156":1}}],["const",{"2":{"0":2,"23":1,"24":1,"31":1,"36":1,"47":1,"55":1,"79":1,"101":1,"102":1,"156":2}}],["iobuffer",{"2":{"160":1}}],["io",{"2":{"156":22,"160":17}}],["illustrated",{"2":{"139":1,"140":1}}],["illustrate",{"2":{"138":1}}],["i>macro",{"2":{"156":1}}],["i>method",{"2":{"156":4,"160":1}}],["i>",{"2":{"66":1,"156":5,"160":1}}],["i>function",{"2":{"66":1}}],["iphone",{"2":{"11":4,"34":1,"40":2,"156":7}}],["ignored",{"2":{"156":1}}],["ignore",{"2":{"10":4,"149":1}}],["immediate",{"2":{"156":1}}],["immediately",{"2":{"85":1,"156":1}}],["im",{"2":{"156":2}}],["imagine",{"2":{"10":1,"75":1}}],["image",{"0":{"19":1},"2":{"9":2,"19":7,"42":6,"100":2,"155":1,"156":88}}],["images",{"0":{"42":1},"2":{"9":2,"19":1,"41":1,"42":1,"54":2,"98":2,"100":2,"156":17,"158":2}}],["imprints",{"2":{"66":1,"156":1}}],["improving",{"2":{"13":1,"51":1,"157":1}}],["improved",{"2":{"120":1,"121":1}}],["improvements",{"2":{"120":2,"121":2}}],["improvement",{"2":{"70":1,"120":4}}],["improves",{"2":{"18":1}}],["improve",{"2":{"7":1,"20":1,"50":1,"51":1,"62":2,"70":3,"115":1,"116":1,"117":1,"118":1,"120":6,"122":1,"156":1,"157":2,"160":2}}],["impact",{"2":{"57":1,"111":1}}],["impartial",{"2":{"16":1,"112":2,"113":1,"127":1}}],["implementing",{"2":{"156":1}}],["implemented",{"2":{"100":1,"160":4}}],["implementations",{"2":{"57":1,"60":1}}],["implementation",{"2":{"57":1,"120":1,"121":2,"156":5,"160":7}}],["implement",{"2":{"50":1,"51":1,"57":1,"157":1}}],["implements",{"2":{"48":1,"157":2}}],["imported",{"2":{"159":1}}],["imports",{"2":{"51":2,"156":7}}],["import",{"2":{"20":1,"23":2,"31":1,"36":1,"47":1,"52":1,"55":1,"65":1,"82":1,"156":2}}],["important",{"2":{"7":1,"63":2,"106":2,"136":2,"138":1,"156":2,"160":2}}],["impermanence",{"2":{"11":1}}],["ie",{"2":{"9":3,"23":1,"51":4,"55":1,"62":1,"63":2,"65":1,"66":2,"73":1,"75":1,"85":2,"86":1,"98":1,"100":3,"102":1,"156":15,"157":7,"160":12}}],["irrelevant",{"2":{"7":1}}],["idiomatic",{"2":{"120":1}}],["idempotent",{"2":{"156":9}}],["identity",{"2":{"156":4,"160":1}}],["identifies",{"2":{"156":2}}],["identified",{"2":{"138":1,"160":1}}],["identifiers",{"2":{"63":1,"106":1,"160":1}}],["identifier",{"2":{"63":1,"106":1,"157":1,"160":4}}],["identify",{"2":{"106":1,"120":1,"137":1,"138":1}}],["identical",{"2":{"65":1,"102":1}}],["ideas",{"2":{"7":1}}],["idea",{"2":{"4":1,"12":1,"156":3}}],["ideally",{"2":{"0":1,"1":1,"2":1,"156":2}}],["id`",{"2":{"51":1,"157":1}}],["ids",{"2":{"6":2,"126":1,"156":8}}],["id",{"2":{"6":11,"51":53,"57":1,"63":2,"126":1,"156":34,"157":69,"160":13}}],["id=",{"2":{"6":2,"66":1,"156":5,"160":1}}],["itr2",{"2":{"66":2,"156":2}}],["itr1",{"2":{"66":2,"156":2}}],["iters",{"2":{"156":1}}],["iterative",{"2":{"157":1}}],["iteratively",{"2":{"66":2,"122":1,"156":2,"157":2}}],["iterating",{"2":{"157":1}}],["iterations",{"2":{"121":1}}],["iteration",{"2":{"66":1,"156":1,"157":2}}],["iterates",{"2":{"156":1}}],["iterate",{"2":{"51":1,"157":1}}],["itemsextract",{"2":{"156":6}}],["items",{"2":{"6":2,"7":1,"59":1,"63":2,"102":1,"106":2,"156":5,"160":9}}],["item",{"2":{"3":1,"4":1,"5":1,"6":3,"51":1,"63":1,"157":1,"160":10}}],["itself",{"2":{"18":1,"50":1,"63":1,"66":1,"156":1,"160":3}}],["its",{"2":{"9":2,"11":1,"20":2,"23":1,"31":1,"48":3,"50":2,"51":2,"57":2,"61":1,"63":4,"66":4,"82":1,"85":1,"86":1,"95":1,"98":1,"100":2,"109":1,"111":1,"117":1,"137":3,"147":1,"148":1,"149":3,"156":12,"157":6,"160":11}}],["it",{"0":{"75":2,"93":1},"1":{"94":1,"95":1,"96":1,"97":1,"98":1,"99":1,"100":1,"101":1,"102":1},"2":{"1":4,"3":2,"4":1,"6":1,"7":2,"9":14,"10":4,"11":4,"12":4,"14":1,"16":1,"17":3,"18":2,"20":8,"21":3,"22":2,"23":11,"25":1,"27":5,"28":3,"29":3,"30":4,"31":3,"35":1,"36":1,"38":1,"39":1,"40":3,"41":4,"42":1,"48":5,"50":6,"51":32,"53":1,"55":2,"56":1,"57":2,"59":1,"63":17,"65":1,"66":14,"68":1,"71":2,"72":4,"73":3,"74":1,"75":2,"76":6,"77":1,"81":4,"82":2,"83":4,"84":8,"85":13,"86":1,"87":2,"89":6,"91":1,"93":3,"94":1,"95":1,"96":3,"97":2,"98":1,"99":4,"100":13,"101":8,"102":17,"109":2,"111":1,"112":1,"115":1,"116":2,"117":2,"118":2,"120":5,"121":5,"122":1,"126":1,"127":3,"134":1,"136":3,"138":2,"140":1,"143":2,"145":1,"146":4,"147":1,"148":1,"149":2,"152":1,"153":4,"156":172,"157":63,"159":2,"160":67}}],["i",{"0":{"69":1,"87":1},"2":{"1":1,"10":1,"11":5,"12":4,"20":2,"21":1,"22":5,"23":1,"25":5,"29":3,"30":5,"33":2,"34":2,"38":1,"39":1,"40":5,"41":1,"50":2,"51":3,"57":1,"66":2,"83":4,"84":2,"86":1,"101":1,"102":7,"107":1,"120":2,"121":2,"122":2,"156":41,"157":10,"160":5}}],["if",{"0":{"69":1},"2":{"0":1,"1":1,"9":5,"12":1,"15":1,"16":1,"17":1,"18":1,"19":1,"20":2,"21":1,"22":2,"23":4,"25":2,"26":1,"27":1,"31":1,"36":1,"38":1,"41":2,"48":2,"50":3,"51":39,"56":2,"57":2,"59":1,"61":2,"62":3,"63":20,"65":1,"66":8,"72":5,"73":5,"75":2,"81":1,"82":1,"83":1,"84":6,"85":5,"91":1,"92":1,"96":1,"100":4,"102":6,"104":1,"106":2,"107":2,"109":5,"111":2,"120":3,"121":1,"127":1,"136":4,"137":2,"138":1,"140":1,"143":1,"146":3,"147":1,"148":2,"149":2,"152":1,"153":3,"156":160,"157":47,"160":46}}],["inherit",{"2":{"160":1}}],["inactive",{"2":{"156":2}}],["inactived",{"2":{"156":1}}],["inanimate",{"2":{"34":1}}],["initialized",{"2":{"156":10}}],["initiate",{"2":{"51":1,"156":1,"157":1}}],["inline",{"2":{"120":3}}],["inject",{"2":{"91":1,"156":1}}],["investigate",{"2":{"57":1}}],["invalid",{"2":{"51":2,"63":2,"156":2,"160":2}}],["inferred",{"2":{"156":1}}],["inferfaces",{"2":{"156":1}}],["influential",{"2":{"149":1}}],["influence",{"2":{"51":1,"102":1,"157":1}}],["informal",{"2":{"143":1}}],["informative",{"2":{"136":1,"137":1}}],["information",{"2":{"5":1,"9":1,"16":1,"18":1,"22":3,"25":3,"28":1,"29":1,"48":1,"51":2,"54":1,"55":1,"59":1,"63":4,"70":1,"73":1,"78":1,"80":1,"81":1,"82":1,"86":1,"94":1,"97":1,"98":2,"100":1,"102":1,"104":2,"109":2,"111":4,"112":1,"113":1,"118":1,"129":1,"131":1,"136":1,"137":1,"143":1,"147":1,"148":1,"149":1,"156":41,"157":5,"158":1,"160":4}}],["informed",{"2":{"5":1}}],["info",{"2":{"3":1,"6":2,"9":1,"10":2,"19":2,"21":1,"22":1,"25":1,"29":1,"30":1,"51":10,"57":1,"66":2,"83":2,"84":2,"91":2,"92":1,"100":1,"102":1,"156":13,"157":10}}],["inplace",{"2":{"157":6}}],["inplace=true",{"2":{"51":1,"157":1}}],["input=",{"2":{"102":2}}],["input2",{"2":{"66":3,"156":3}}],["input1",{"2":{"66":3,"156":3}}],["inputclassifier",{"0":{"126":1},"2":{"17":1,"84":1,"156":3}}],["inputs",{"2":{"9":2,"20":1,"48":3,"50":1,"85":1,"94":1,"96":1,"99":1,"100":2,"102":1,"146":1,"153":1,"156":2,"160":2}}],["input",{"2":{"9":2,"17":4,"63":1,"84":2,"100":2,"102":3,"117":1,"126":7,"149":1,"156":19,"160":15}}],["incredible",{"2":{"136":1}}],["increase",{"2":{"48":1,"74":1,"156":3,"157":1}}],["inconsistent",{"2":{"112":1}}],["incomplete",{"2":{"112":1,"156":1}}],["including",{"2":{"9":1,"16":1,"24":1,"48":1,"81":1,"102":1,"149":1,"156":16,"157":2,"160":2}}],["includes",{"2":{"6":1,"63":2,"80":1,"157":2,"160":5}}],["included",{"2":{"6":1,"121":1,"137":1,"156":2,"159":1,"160":2}}],["include",{"2":{"1":1,"6":1,"9":1,"54":9,"57":2,"63":1,"100":1,"115":1,"116":1,"136":2,"137":1,"146":1,"153":1,"156":6,"158":9,"160":4}}],["independent",{"2":{"157":1}}],["indexing",{"2":{"63":1,"147":1,"148":1,"160":1}}],["indexes",{"2":{"160":1}}],["indexed",{"2":{"62":2,"63":1,"160":1}}],["indexer",{"2":{"60":1,"63":8,"160":8}}],["index",{"2":{"1":16,"2":1,"3":2,"5":1,"6":1,"7":3,"9":1,"56":5,"57":11,"59":5,"60":5,"61":2,"62":5,"63":40,"156":1,"160":91}}],["indifferent",{"2":{"66":1,"156":1}}],["individual",{"2":{"51":2,"112":1,"156":1,"157":2,"160":1}}],["indicate",{"2":{"157":2}}],["indicates",{"2":{"51":3,"149":1,"156":1,"157":4}}],["indication",{"2":{"138":1}}],["indicating",{"2":{"63":1,"156":14,"157":1,"160":9}}],["indices",{"2":{"7":1,"156":1,"160":6}}],["inserting",{"2":{"160":1}}],["inside",{"2":{"51":2,"156":3}}],["insights",{"2":{"5":1,"136":5}}],["inspired",{"2":{"20":1,"51":1,"115":1,"116":1,"149":1,"156":2,"157":1}}],["inspect",{"2":{"12":1,"51":2,"156":2,"157":1}}],["instructor",{"2":{"156":2}}],["instruct",{"2":{"27":1,"102":1}}],["instructions>",{"2":{"153":4}}],["instructions=",{"2":{"63":1,"106":1,"107":1,"111":1,"136":1,"137":1,"138":1,"146":1,"148":1,"149":1,"153":1,"160":2}}],["instructions",{"2":{"3":1,"35":1,"63":2,"97":1,"99":1,"104":1,"106":9,"107":5,"109":1,"111":9,"112":1,"120":3,"126":1,"129":1,"131":1,"136":7,"137":6,"138":3,"140":4,"143":1,"146":7,"147":4,"148":6,"149":9,"153":6,"156":1,"160":2}}],["installation",{"0":{"90":1},"2":{"81":1}}],["installated",{"2":{"36":1}}],["installing",{"2":{"51":1,"156":2}}],["installed",{"2":{"21":1,"81":1,"90":2}}],["instant",{"0":{"79":1}}],["instantiation",{"2":{"51":2,"156":2}}],["instantiated",{"2":{"9":1,"48":1,"100":1,"156":1}}],["instance",{"2":{"9":2,"18":1,"48":2,"51":2,"59":1,"100":2,"149":3,"156":2,"157":18}}],["instead",{"2":{"14":1,"23":1,"66":1,"86":1,"120":1,"121":1,"149":1,"156":7,"160":1}}],["innerjoin",{"2":{"6":2}}],["inner",{"2":{"5":1,"6":5}}],["int16",{"2":{"156":1}}],["intricate",{"2":{"156":5}}],["intro",{"2":{"66":2}}],["introduced",{"2":{"41":1}}],["introduction",{"0":{"47":1,"52":1,"55":1},"1":{"48":1,"49":1,"50":1,"51":1,"53":1,"54":1,"56":1,"57":1,"58":1,"59":1,"60":1,"61":1,"62":1,"63":1},"2":{"4":1,"121":1}}],["int=32000",{"2":{"157":1}}],["int=35000",{"2":{"66":4,"156":4}}],["int=1",{"2":{"157":1}}],["int=512",{"2":{"51":1,"157":1}}],["intent",{"2":{"117":1}}],["intention",{"2":{"59":1,"159":1}}],["intended",{"2":{"51":1,"62":2,"116":1,"117":1,"157":2,"160":1}}],["intends",{"2":{"35":1}}],["intelligence",{"2":{"15":1}}],["integrates",{"2":{"57":1,"157":1}}],["integration",{"0":{"10":1},"2":{"146":1,"153":1}}],["integer",{"2":{"54":1,"63":9,"84":1,"156":8,"158":1,"160":11}}],["integer=1",{"2":{"51":1,"157":2}}],["integers",{"2":{"6":1,"156":3}}],["intersection",{"2":{"160":1}}],["interpolate",{"2":{"156":1}}],["interpolated",{"2":{"156":1}}],["interpolation",{"0":{"39":1},"2":{"91":1,"147":1,"148":1,"156":6}}],["interprets",{"2":{"134":1}}],["interested",{"2":{"102":1,"156":1}}],["interesting",{"2":{"40":1}}],["internally",{"2":{"63":1,"160":1}}],["internal",{"2":{"59":1,"63":1,"143":3,"156":1,"160":1}}],["interface",{"0":{"58":1},"1":{"59":1,"60":1,"61":1,"62":1},"2":{"55":1,"62":1,"156":2}}],["intermediate",{"2":{"5":1,"57":2}}],["interaction",{"2":{"157":2}}],["interactions",{"2":{"149":1,"157":5}}],["interactive",{"2":{"51":1,"57":1,"157":2}}],["interact",{"2":{"0":1,"9":1,"19":1,"48":1,"51":2,"95":1,"100":1,"157":2}}],["int64",{"2":{"6":3,"12":1,"23":1,"44":1,"57":4,"85":1,"156":3}}],["int",{"2":{"6":1,"18":2,"51":15,"63":2,"66":2,"84":5,"156":37,"157":28,"160":16}}],["into",{"0":{"10":1},"2":{"1":3,"9":1,"16":1,"17":1,"34":1,"44":1,"51":1,"55":1,"57":1,"62":1,"63":6,"65":6,"66":6,"79":1,"85":1,"90":1,"100":1,"102":4,"118":1,"136":2,"137":2,"138":2,"143":1,"156":22,"157":2,"160":20}}],["in",{"0":{"1":1,"78":1},"2":{"0":2,"1":9,"2":1,"3":2,"5":3,"6":24,"7":4,"9":8,"10":6,"11":1,"12":3,"13":1,"14":3,"16":1,"17":1,"18":4,"19":4,"20":5,"21":2,"22":3,"23":15,"25":2,"27":3,"29":2,"30":2,"31":3,"34":1,"35":1,"36":1,"38":1,"40":1,"41":2,"48":1,"50":3,"51":53,"53":1,"54":4,"56":4,"57":15,"59":6,"61":1,"62":3,"63":22,"64":1,"65":4,"66":7,"68":1,"70":2,"72":6,"73":3,"74":1,"75":1,"76":4,"77":2,"79":1,"81":2,"83":4,"84":4,"85":7,"87":2,"89":4,"91":4,"92":1,"99":2,"100":8,"101":7,"102":16,"106":3,"111":4,"112":3,"115":1,"116":2,"117":1,"118":1,"120":7,"121":4,"122":1,"126":1,"129":3,"131":2,"136":2,"137":2,"138":3,"141":1,"143":5,"144":1,"145":2,"146":1,"151":1,"152":5,"153":3,"155":1,"156":155,"157":65,"158":4,"159":2,"160":71}}],["isolate",{"2":{"156":1}}],["isn",{"2":{"102":1,"109":2,"156":2}}],["isnothing",{"2":{"6":1,"51":1,"84":1,"156":1,"157":1,"160":1}}],["issues",{"2":{"120":2}}],["issue",{"2":{"56":1,"63":1,"65":1,"72":1,"120":2,"160":2}}],["islowercase",{"2":{"51":1,"157":1}}],["isvalid",{"2":{"20":1,"50":1,"51":4,"156":4,"157":1}}],["isa",{"2":{"9":4,"51":2,"84":1,"100":4,"102":1,"156":1,"157":2}}],["is",{"0":{"75":1},"2":{"0":2,"1":4,"2":1,"4":2,"5":5,"6":18,"7":1,"9":11,"10":3,"11":1,"12":4,"14":1,"16":4,"18":4,"20":9,"21":2,"23":5,"27":5,"28":1,"29":3,"30":5,"31":1,"34":1,"35":1,"36":2,"38":1,"39":2,"40":4,"41":2,"42":1,"46":1,"47":1,"48":6,"50":8,"51":45,"52":1,"53":1,"54":9,"55":2,"56":2,"57":2,"59":4,"61":2,"62":7,"63":52,"66":21,"70":2,"72":5,"73":3,"75":3,"76":1,"80":2,"81":2,"83":4,"84":5,"85":1,"87":1,"91":8,"92":5,"93":2,"95":3,"97":1,"98":2,"99":3,"100":12,"101":14,"102":16,"107":1,"109":3,"111":3,"112":5,"115":1,"116":1,"117":4,"118":2,"120":2,"121":2,"126":1,"127":3,"129":1,"134":1,"136":1,"137":3,"141":1,"142":1,"143":2,"144":1,"145":3,"146":2,"147":1,"148":1,"149":1,"151":2,"152":4,"153":3,"156":201,"157":67,"158":9,"159":5,"160":124}}],["dynamic",{"2":{"157":1}}],["duplication",{"2":{"63":1,"160":1}}],["due",{"2":{"51":1,"156":2}}],["during",{"2":{"6":1,"51":3,"156":3,"157":2,"160":1}}],["drawn",{"2":{"160":2}}],["draft",{"2":{"143":1}}],["drafts",{"2":{"143":1}}],["drafteremailbrief",{"0":{"143":1}}],["dry",{"2":{"156":20}}],["drives",{"2":{"157":1}}],["driven",{"2":{"156":1}}],["drive",{"2":{"102":1}}],["drops",{"2":{"51":1,"157":1}}],["dr",{"2":{"38":1,"106":2}}],["d",{"2":{"29":1,"30":1,"75":1,"86":1}}],["dllama",{"2":{"28":3}}],["dspy",{"2":{"20":1,"51":1,"157":1}}],["date",{"2":{"106":1}}],["dates",{"2":{"106":1}}],["datastructtype",{"2":{"156":1}}],["dataset",{"2":{"5":1,"87":2,"149":1}}],["data>",{"2":{"129":4,"152":4}}],["databricksopenaischema",{"2":{"28":2,"156":3}}],["databricks",{"0":{"28":1},"2":{"28":9,"63":1,"156":16,"160":1}}],["database",{"2":{"1":1,"4":2,"5":1,"6":11}}],["datamessage",{"2":{"9":2,"44":2,"45":1,"46":1,"98":1,"100":2,"156":10}}],["dataframerowsourcecontextquestionanswerretrieval",{"2":{"6":1}}],["dataframe",{"2":{"6":11,"12":1,"23":1,"156":2}}],["dataframeswhat",{"2":{"6":1}}],["dataframesmeta",{"2":{"0":1,"1":1}}],["dataframes",{"2":{"0":1,"1":3,"4":1,"12":1,"23":1,"106":2,"156":2,"160":1}}],["data",{"0":{"18":1,"70":1},"2":{"1":4,"4":5,"5":4,"6":34,"7":2,"9":1,"18":3,"21":1,"23":6,"44":1,"51":9,"57":4,"59":3,"62":1,"70":7,"81":1,"98":1,"100":1,"102":2,"106":1,"129":6,"131":6,"133":1,"142":5,"145":6,"147":1,"149":1,"152":4,"156":10,"157":18,"160":2}}],["damaging",{"2":{"86":1}}],["day",{"2":{"75":1}}],["dashboard",{"2":{"74":1,"160":1}}],["dashboards",{"2":{"57":1}}],["dance",{"2":{"66":1,"156":1}}],["danced",{"2":{"66":1,"156":1}}],["dangerous",{"2":{"34":1,"156":1}}],["darkness",{"2":{"34":1}}],["daphodil",{"2":{"17":1,"156":1}}],["dall",{"2":{"9":1,"100":1,"156":5}}],["diagnostics",{"2":{"160":1}}],["diagram",{"0":{"60":1},"1":{"61":1},"2":{"59":1}}],["dir",{"2":{"156":6}}],["direct",{"2":{"138":1}}],["directly",{"2":{"51":1,"61":2,"63":4,"66":1,"74":1,"85":2,"99":1,"111":2,"116":1,"117":1,"156":8,"160":8}}],["directory",{"2":{"23":3,"156":3}}],["diligent",{"2":{"142":1}}],["dilemma",{"2":{"11":1}}],["diverse",{"2":{"117":1}}],["division",{"2":{"66":1,"156":1}}],["divides",{"2":{"62":1}}],["div",{"2":{"66":1,"156":5,"160":1}}],["div>",{"2":{"66":1,"156":5,"160":1}}],["digits",{"2":{"51":2,"84":1,"157":2}}],["digits=1",{"2":{"6":1}}],["didn",{"2":{"157":1}}],["did",{"2":{"23":1,"102":1}}],["disables",{"2":{"156":1}}],["disable",{"2":{"156":1}}],["disabled",{"2":{"63":1,"160":1}}],["disney",{"2":{"66":2,"156":2}}],["disk",{"2":{"62":1,"85":2,"99":1,"160":1}}],["displayed",{"2":{"156":1}}],["displaysize",{"2":{"156":1,"160":2}}],["display",{"2":{"12":1,"23":1,"57":1,"63":1,"83":1,"156":2,"160":1}}],["dispatching",{"2":{"59":1,"66":1,"156":1,"160":23}}],["dispatches",{"2":{"51":1,"57":1,"63":1,"157":1,"160":1}}],["dispatched",{"2":{"51":1,"157":1}}],["dispatch",{"2":{"12":1,"23":1,"41":1,"59":2,"60":4,"61":2,"63":1,"66":2,"96":1,"99":1,"147":2,"148":2,"156":6,"157":1,"160":2}}],["distinct",{"2":{"137":1,"138":1}}],["distinguished",{"2":{"147":1,"148":1}}],["distinguish",{"2":{"66":1,"156":1}}],["dist",{"2":{"66":6,"156":6}}],["distributed",{"2":{"157":1}}],["distributing",{"2":{"149":1}}],["distribution",{"2":{"57":1,"157":2}}],["distract",{"2":{"40":1}}],["distraction",{"2":{"10":1}}],["distances",{"2":{"66":1,"156":1}}],["distance",{"2":{"7":1,"15":2,"65":3,"66":12,"156":14}}],["discussed",{"2":{"137":2}}],["discuss",{"2":{"29":1,"30":1}}],["discussions",{"2":{"137":1}}],["discussion",{"2":{"10":1,"156":1}}],["discovery",{"2":{"111":1}}],["discovered",{"2":{"111":2}}],["discover",{"2":{"9":1,"40":1,"57":3,"60":1,"63":2,"99":1,"100":1,"157":1,"160":2}}],["discrete",{"2":{"9":1,"100":1}}],["differs",{"2":{"102":1}}],["differ",{"2":{"9":1,"100":1}}],["differences",{"2":{"156":1}}],["difference",{"2":{"5":1,"6":2,"66":1,"100":1,"156":1}}],["differently",{"2":{"9":1,"48":1,"84":1,"100":1,"156":1}}],["different",{"2":{"5":1,"6":2,"21":1,"51":4,"63":1,"65":1,"85":1,"120":2,"143":1,"146":1,"147":1,"148":1,"153":1,"156":6,"157":5,"160":6}}],["dict=parameters",{"2":{"160":1}}],["dict=dict",{"2":{"160":1}}],["dicts",{"2":{"156":1}}],["dictates",{"2":{"63":1,"160":3}}],["dictionaries",{"2":{"41":1,"96":1}}],["dictionary",{"2":{"14":1,"57":1,"147":1,"148":1,"156":4}}],["dict",{"2":{"5":4,"6":2,"57":1,"84":1,"101":3,"102":9,"156":20,"160":13}}],["doing",{"2":{"156":1}}],["dollar",{"2":{"87":1}}],["dolphin",{"2":{"36":1}}],["domains",{"2":{"54":4,"158":4}}],["dominating",{"2":{"22":1,"25":1}}],["dot",{"2":{"15":1,"156":2}}],["double",{"2":{"10":1,"70":1,"77":1,"147":1,"148":1,"156":1}}],["doewhat",{"2":{"6":1}}],["doe",{"2":{"6":6}}],["does",{"0":{"75":1},"2":{"1":1,"6":1,"9":1,"35":1,"40":1,"48":1,"51":2,"63":1,"70":2,"84":1,"86":1,"100":1,"112":2,"120":1,"121":1,"156":11,"157":1,"160":3}}],["don",{"2":{"5":1,"7":1,"23":1,"36":1,"38":1,"97":1,"102":1,"104":3,"106":1,"107":1,"109":3,"111":1,"136":2,"137":1,"138":1,"146":1,"148":1,"149":1,"153":1,"156":6}}],["done",{"2":{"1":1,"6":1,"51":1,"57":1,"63":1,"82":1,"101":1,"102":1,"156":4,"157":2,"160":1}}],["downstream",{"2":{"84":1,"91":1}}],["downloads",{"2":{"42":1,"156":6}}],["downloaded",{"2":{"21":1}}],["download",{"2":{"9":1,"23":1,"36":1,"42":2,"81":2,"100":1,"156":10}}],["down",{"2":{"1":1,"59":1,"66":1,"121":1,"156":1}}],["do",{"0":{"7":1,"86":1},"2":{"1":1,"5":1,"6":4,"9":1,"10":2,"11":3,"12":3,"18":2,"19":1,"20":2,"22":1,"23":1,"25":1,"33":1,"34":1,"35":1,"40":1,"45":1,"50":2,"51":6,"59":2,"66":1,"71":2,"72":2,"73":1,"79":1,"83":1,"84":2,"85":1,"86":1,"89":2,"100":1,"102":6,"120":2,"121":2,"122":1,"136":3,"137":1,"146":1,"149":1,"153":1,"156":24,"157":6,"160":1}}],["doc9",{"2":{"57":1}}],["doc2",{"2":{"57":1}}],["doc5",{"2":{"57":1}}],["doc15",{"2":{"57":1}}],["doc8",{"2":{"57":1}}],["doc$i",{"2":{"57":1}}],["doc",{"2":{"45":2,"63":4,"156":4,"160":4}}],["doctor1",{"2":{"6":1}}],["doctorwhat",{"2":{"6":2}}],["doctor",{"2":{"6":2}}],["documented",{"2":{"59":1}}],["document",{"0":{"44":1},"2":{"1":1,"6":1,"9":1,"44":1,"60":1,"63":9,"106":1,"156":5,"160":17}}],["documents",{"0":{"45":1},"2":{"1":1,"6":1,"45":1,"56":2,"57":2,"63":2,"66":2,"156":6,"160":12}}],["documentation",{"2":{"0":1,"18":1,"31":1,"54":1,"57":1,"63":1,"71":1,"89":1,"96":1,"102":1,"106":1,"115":1,"156":7,"158":1,"160":1}}],["docstring",{"2":{"77":1,"86":1,"102":1,"156":5}}],["docstrings",{"2":{"18":1,"59":1,"131":1,"156":1}}],["docs",{"2":{"1":2,"20":1,"23":1,"45":1,"60":1,"63":5,"69":1,"102":6,"156":7,"160":24}}],["dplyr",{"2":{"1":3}}],["denote",{"2":{"136":1}}],["declaration",{"2":{"156":4}}],["decoded",{"2":{"156":1}}],["decodes",{"2":{"156":1}}],["decode",{"2":{"102":1,"156":4}}],["decision",{"2":{"137":9}}],["decisions",{"2":{"5":1,"137":5}}],["decides",{"2":{"160":1}}],["decide",{"2":{"16":1,"17":1,"127":1}}],["deduplicate",{"2":{"63":1,"160":1}}],["dedicated",{"2":{"0":1,"22":1,"25":1,"100":1,"137":1}}],["dev",{"2":{"66":1,"156":1}}],["developing",{"2":{"57":1}}],["development",{"2":{"57":1,"111":1}}],["device",{"2":{"40":1}}],["depot",{"2":{"72":1}}],["depth",{"2":{"54":3,"158":3}}],["depend",{"2":{"156":1}}],["dependencies",{"2":{"23":1,"55":1}}],["depends",{"2":{"12":1}}],["depending",{"2":{"9":1,"63":2,"100":1,"156":1,"160":2}}],["deemed",{"2":{"51":1,"156":1}}],["deepdive",{"0":{"62":1}}],["deeper",{"2":{"61":2}}],["deep",{"2":{"40":1,"121":1,"122":1,"152":1}}],["democards",{"2":{"66":1,"156":1}}],["demonstrate",{"2":{"51":1,"157":1}}],["demanding",{"2":{"27":1}}],["delim",{"2":{"156":2}}],["delicious",{"2":{"30":2,"102":8}}],["dels",{"2":{"62":1}}],["delay=2",{"2":{"51":1,"157":1}}],["delay",{"2":{"20":1,"50":1,"51":5,"156":1,"157":7}}],["delete",{"2":{"1":1,"3":1,"72":1}}],["defauls",{"2":{"156":1}}],["defaults",{"2":{"51":10,"55":1,"59":1,"62":1,"63":19,"66":4,"156":67,"157":17,"160":28}}],["default",{"0":{"82":1},"2":{"15":1,"36":1,"41":1,"51":2,"54":7,"57":3,"59":1,"63":40,"66":1,"82":2,"91":1,"100":1,"101":1,"102":1,"156":52,"157":1,"158":7,"160":82}}],["defining",{"2":{"59":3}}],["definition",{"2":{"51":1,"120":1,"149":7,"156":2}}],["defines",{"2":{"156":5,"160":2}}],["defined",{"0":{"17":1},"2":{"17":1,"51":1,"61":1,"62":1,"98":1,"99":2,"137":1,"156":15,"157":1,"160":2}}],["define",{"2":{"1":1,"9":1,"18":2,"23":1,"36":1,"41":1,"51":1,"63":2,"84":2,"100":1,"101":1,"102":2,"156":9,"157":1,"160":2}}],["deferring",{"2":{"51":1,"157":1}}],["deferred",{"2":{"9":1,"48":2,"51":2,"100":1,"157":3}}],["defer",{"2":{"16":1}}],["despite",{"0":{"72":2},"2":{"156":1}}],["desired",{"2":{"65":2,"157":1}}],["designed",{"2":{"9":4,"48":1,"51":2,"55":1,"59":2,"85":1,"100":4,"102":1,"156":2,"157":2}}],["descriptive",{"2":{"136":2,"137":1}}],["description=sig",{"2":{"102":2}}],["descriptions",{"2":{"17":1,"156":5}}],["description",{"0":{"139":1,"140":1},"1":{"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"150":1,"151":1,"152":1,"153":1},"2":{"12":1,"17":1,"23":4,"41":1,"85":2,"102":10,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":1,"121":1,"122":1,"124":1,"126":1,"127":1,"129":2,"131":1,"133":1,"134":1,"136":3,"137":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":5,"151":1,"152":1,"153":1,"155":1,"156":25}}],["describes",{"2":{"126":1}}],["describe",{"2":{"19":2,"42":1,"136":1,"137":2,"145":1,"147":1,"152":1,"156":6}}],["deserialize",{"2":{"1":1,"156":1}}],["determining",{"2":{"156":1}}],["determines",{"2":{"94":1,"156":1}}],["determine",{"2":{"3":1,"6":1}}],["detects",{"2":{"156":3}}],["detect",{"2":{"156":2}}],["detail=",{"2":{"156":1}}],["detailorientedtask",{"0":{"142":1}}],["detail",{"2":{"63":1,"77":1,"120":1,"121":1,"129":1,"131":1,"142":2,"156":14,"160":1}}],["detailed",{"2":{"51":2,"111":1,"157":4}}],["details",{"2":{"1":1,"9":2,"23":1,"36":1,"48":1,"51":2,"62":1,"63":8,"99":2,"100":2,"102":2,"115":1,"116":1,"118":1,"129":1,"131":1,"149":1,"156":5,"157":8,"160":21}}],["detachment",{"2":{"11":2}}],["debugging",{"2":{"93":1,"156":10,"160":1}}],["debug",{"2":{"1":1,"63":1,"160":1}}],["tsang",{"2":{"157":1}}],["tl",{"2":{"38":1}}],["tmixtral",{"2":{"29":2,"102":2}}],["tmp",{"2":{"23":1}}],["tmps",{"2":{"12":1,"156":2}}],["tpl=pt",{"2":{"85":1,"156":2}}],["tpl",{"2":{"23":1,"85":2,"156":2}}],["typically",{"2":{"156":3}}],["typing",{"2":{"19":1,"156":2}}],["typed",{"0":{"84":1},"2":{"84":2,"90":1}}],["type=fruit",{"2":{"156":1}}],["type=food",{"2":{"9":1,"30":1,"100":1,"102":1}}],["type=maybetags",{"2":{"160":1}}],["type=manymeasurements",{"2":{"18":1,"156":1}}],["type=mymeasurement",{"2":{"156":2}}],["type=currentweather",{"2":{"18":1}}],["types",{"2":{"11":1,"51":2,"59":2,"60":4,"62":1,"63":7,"79":1,"82":1,"84":4,"98":1,"100":1,"102":4,"147":2,"148":2,"156":2,"157":2,"160":7}}],["type",{"2":{"5":1,"9":1,"10":1,"14":1,"18":2,"51":3,"56":1,"57":1,"59":2,"61":2,"62":1,"63":3,"84":2,"91":1,"94":1,"100":1,"102":21,"120":1,"147":1,"148":1,"156":68,"157":8,"160":56}}],["tiktokenizer",{"2":{"156":1}}],["titles",{"2":{"136":2,"137":2}}],["title",{"2":{"136":2,"137":1}}],["tiniest",{"2":{"122":1}}],["tiny",{"2":{"22":3,"25":3,"156":2}}],["tier",{"2":{"73":3,"156":2}}],["timing",{"2":{"51":1,"157":1}}],["timed",{"2":{"51":1,"157":1}}],["timeout",{"2":{"51":3,"156":8}}],["timestamp",{"2":{"136":2,"137":3}}],["timestamps",{"2":{"136":3,"137":2}}],["times",{"2":{"20":1,"48":2,"50":1,"51":3,"85":1,"157":4}}],["time",{"2":{"2":1,"6":1,"9":1,"12":1,"20":1,"23":3,"30":1,"35":1,"50":1,"51":3,"66":1,"70":1,"75":2,"76":1,"82":1,"87":1,"94":1,"97":1,"99":1,"100":1,"156":21,"157":4,"159":1,"160":1}}],["tired",{"2":{"18":1}}],["tips",{"2":{"20":1,"66":1,"136":2,"137":2,"156":2}}],["tip",{"2":{"13":1,"66":1,"71":1,"91":1,"92":2,"107":1,"156":1}}],["tell",{"2":{"149":1}}],["tedious",{"2":{"82":1}}],["tens",{"2":{"75":1}}],["tends",{"2":{"86":1}}],["tend",{"2":{"9":1,"66":1,"69":1,"82":1,"100":1,"156":1}}],["terms",{"2":{"106":1}}],["term",{"2":{"55":1,"106":1}}],["terminal",{"2":{"27":1,"36":1,"57":1,"65":1,"76":4,"81":1,"89":2}}],["testing",{"2":{"156":5}}],["testechoopenaischema",{"2":{"156":2}}],["testechoollamaschema",{"2":{"156":2}}],["testechoollamamanagedschema",{"2":{"156":2}}],["testechogoogleschema",{"2":{"156":2}}],["testechoanthropicschema",{"2":{"156":2}}],["test`",{"2":{"120":1,"146":3,"153":3}}],["tests>",{"2":{"153":2}}],["testset`",{"2":{"146":1,"153":1}}],["testsets",{"2":{"146":1,"153":1}}],["testset",{"2":{"51":1,"120":1,"146":2,"153":2,"156":1}}],["tests",{"2":{"51":4,"120":1,"146":4,"153":4,"156":5}}],["test",{"2":{"41":1,"51":2,"63":5,"146":14,"149":1,"153":14,"156":9,"160":10}}],["teacher",{"2":{"111":1}}],["teach",{"2":{"40":1}}],["technical",{"2":{"106":2}}],["technically",{"2":{"27":1}}],["technique",{"2":{"87":1}}],["techniques",{"2":{"57":1}}],["technology",{"2":{"40":1}}],["tempdir",{"2":{"156":1}}],["temporary",{"2":{"156":1}}],["temperature",{"2":{"18":1,"156":2,"157":2}}],["temperatureunits",{"2":{"18":2}}],["temperature=0",{"2":{"9":1,"51":3,"100":3,"102":2,"157":3}}],["templating",{"2":{"12":1,"92":1,"156":1}}],["template",{"0":{"85":1,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":1,"121":1,"122":1,"124":1,"126":1,"127":1,"129":1,"131":1,"133":1,"134":1,"136":1,"137":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"151":1,"152":1,"153":1,"155":1},"2":{"12":5,"16":1,"19":1,"23":10,"51":1,"59":2,"61":5,"63":17,"85":13,"99":7,"101":4,"120":1,"121":1,"122":1,"129":1,"131":1,"133":1,"136":1,"137":1,"138":1,"142":1,"143":1,"147":1,"148":1,"156":115,"157":9,"160":41}}],["templated",{"0":{"12":1},"2":{"16":1,"23":1}}],["templates=true",{"2":{"156":1}}],["templates",{"0":{"99":1,"103":1,"105":1,"108":1,"110":1,"114":1,"119":1,"123":1,"125":1,"128":1,"130":1,"132":1,"135":1,"150":1,"154":1},"1":{"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":1,"121":1,"122":1,"124":1,"126":1,"127":1,"129":1,"131":1,"133":1,"134":1,"136":1,"137":1,"138":1,"151":1,"152":1,"153":1,"155":1},"2":{"9":1,"12":2,"16":1,"23":13,"85":12,"92":1,"94":1,"99":3,"100":1,"156":46,"157":4}}],["text=",{"2":{"160":1}}],["text1",{"2":{"66":1,"156":1}}],["text2",{"2":{"66":2,"156":2}}],["texts",{"2":{"63":3,"66":2,"156":2,"160":3}}],["textchunker",{"2":{"63":1,"156":1,"160":6}}],["textanalysis",{"2":{"7":1}}],["text",{"0":{"32":1,"37":1,"64":1},"1":{"33":1,"34":1,"35":1,"38":1,"39":1,"40":1,"41":1,"65":1,"66":1},"2":{"1":3,"7":1,"9":6,"15":1,"18":2,"19":1,"29":1,"30":2,"51":2,"57":1,"63":9,"64":2,"65":7,"66":41,"86":2,"98":1,"100":4,"102":1,"106":7,"107":6,"143":2,"155":1,"156":60,"157":1,"160":28}}],["trove",{"2":{"85":2,"156":2}}],["troubleshooting",{"2":{"36":1}}],["treated",{"2":{"156":2}}],["treasure",{"2":{"85":2,"156":2}}],["trees",{"2":{"51":1,"157":3}}],["tree",{"2":{"17":2,"20":2,"48":1,"51":7,"63":2,"66":1,"84":1,"156":6,"157":17,"160":4}}],["trained",{"2":{"136":1,"137":1}}],["train",{"2":{"70":1}}],["training",{"2":{"70":1,"156":1}}],["trail",{"2":{"66":1,"156":1}}],["tracked",{"2":{"156":1,"160":1}}],["tracker",{"2":{"63":8,"156":1,"160":20}}],["tracking",{"2":{"156":1,"157":1}}],["tracks",{"2":{"63":1,"156":1,"160":1}}],["track",{"2":{"63":5,"157":1,"160":12}}],["transcript",{"2":{"136":7,"137":6}}],["transcripts",{"2":{"136":2,"137":2}}],["transcribe",{"2":{"19":2,"155":2,"156":8}}],["transformations",{"0":{"114":1},"1":{"115":1,"116":1,"117":1,"118":1},"2":{"116":1}}],["transform",{"2":{"63":1,"156":1,"160":2}}],["translate",{"2":{"13":1}}],["truncates",{"2":{"157":1}}],["truncated",{"2":{"156":4}}],["truncate",{"2":{"156":1,"157":2}}],["truths",{"2":{"66":1,"156":1}}],["trusted",{"2":{"57":1}}],["true",{"2":{"3":1,"5":2,"6":1,"11":2,"16":3,"20":1,"34":1,"44":1,"50":1,"51":30,"54":1,"63":17,"84":4,"85":2,"127":2,"156":96,"157":23,"158":1,"160":42}}],["tryparse",{"2":{"51":4,"84":2,"156":1,"157":4}}],["try",{"2":{"17":1,"30":1,"40":1,"51":2,"63":1,"85":1,"102":4,"104":1,"109":1,"115":1,"116":1,"156":2,"157":1,"160":2}}],["trying",{"2":{"11":1,"34":1,"40":1,"48":1,"156":5,"157":1}}],["trial",{"2":{"160":1}}],["trims",{"2":{"156":1}}],["tries",{"2":{"156":1,"157":1,"160":1}}],["triple",{"2":{"120":1,"121":1,"138":1,"156":1}}],["trivially",{"2":{"84":1}}],["trigram",{"2":{"63":3,"156":1,"160":8}}],["trigrams",{"2":{"63":6,"65":4,"156":2,"160":23}}],["trigramannotater",{"2":{"63":4,"156":1,"160":10}}],["triggers",{"2":{"51":1,"102":1,"157":2}}],["triggered",{"2":{"48":1,"51":1,"157":1}}],["trigger",{"2":{"9":1,"48":1,"51":2,"100":1,"157":3}}],["trick",{"2":{"9":1,"17":1,"40":1,"100":1,"156":4}}],["tuning",{"2":{"87":1,"156":1}}],["tune",{"0":{"87":1}}],["tuned",{"2":{"30":1}}],["tuple",{"2":{"44":1,"156":30,"157":1,"160":5}}],["tuples",{"2":{"17":1,"147":1,"148":1,"156":5}}],["turn",{"0":{"83":1},"2":{"34":2,"41":1,"92":1,"156":2}}],["turbo",{"2":{"6":1,"14":3,"91":2,"101":1,"102":2,"156":9}}],["tutorials",{"2":{"57":2}}],["tutorial",{"2":{"7":1,"71":1,"89":1,"90":1}}],["t",{"2":{"5":1,"7":1,"23":1,"26":1,"27":1,"29":1,"36":1,"38":1,"51":2,"82":1,"97":1,"102":2,"104":3,"106":1,"107":1,"109":5,"111":1,"136":2,"137":1,"138":1,"146":1,"148":1,"149":1,"153":1,"156":22,"157":5,"160":3}}],["tweak",{"2":{"1":2,"6":1,"51":2,"157":2}}],["two",{"0":{"1":1},"2":{"1":1,"4":4,"5":1,"6":7,"9":1,"15":1,"16":3,"20":1,"50":2,"51":2,"65":2,"66":2,"73":1,"84":5,"101":1,"102":1,"120":1,"156":17,"157":4,"160":1}}],["taking",{"2":{"118":1}}],["taken",{"2":{"156":2}}],["takes",{"2":{"73":1,"156":1}}],["take",{"2":{"6":1,"23":2,"96":1,"101":1,"121":1,"122":1,"136":1,"140":1,"152":1,"156":1,"157":1}}],["tapestry",{"2":{"66":2,"156":2}}],["target",{"2":{"63":2,"149":2,"160":6}}],["tavily",{"2":{"53":3,"54":1,"156":5,"158":5,"159":1}}],["tall",{"2":{"18":2,"156":5}}],["tabular",{"2":{"12":1,"156":2}}],["table",{"2":{"6":2,"23":1,"75":1,"149":4}}],["tables",{"2":{"6":1}}],["task>",{"2":{"152":4}}],["tasked",{"2":{"118":1}}],["task=",{"2":{"19":1,"156":2}}],["tasks",{"2":{"13":2,"14":1,"16":1,"18":1,"22":1,"25":1,"29":1,"33":1,"41":1,"45":2,"57":1,"63":2,"68":1,"92":1,"126":1,"138":1,"142":1,"156":3,"157":1,"160":4}}],["task",{"0":{"135":1},"1":{"136":1,"137":1,"138":1},"2":{"10":1,"18":1,"23":1,"57":1,"87":1,"98":1,"99":1,"102":3,"109":1,"112":1,"115":1,"116":1,"117":1,"118":1,"120":1,"121":1,"126":1,"133":1,"134":6,"142":4,"145":7,"146":1,"147":6,"148":6,"149":5,"152":6,"153":1,"155":4,"156":1}}],["tag2",{"2":{"63":1,"160":1}}],["tag1",{"2":{"63":1,"160":1}}],["tagging",{"2":{"63":2,"160":4}}],["tagger=opentagger",{"2":{"63":1,"160":1}}],["tagger",{"2":{"63":24,"160":36}}],["tag",{"2":{"1":1,"57":1,"62":1,"63":4,"160":23}}],["tags",{"2":{"1":3,"60":4,"62":8,"63":17,"129":1,"152":3,"153":2,"156":8,"160":87}}],["tailored",{"2":{"0":1,"134":1}}],["txtouterjoin",{"2":{"6":1}}],["txtrightjoin",{"2":{"6":1}}],["txtleftjoin",{"2":{"6":1}}],["txtinnerjoin",{"2":{"6":1}}],["txtin",{"2":{"6":1}}],["txtwe",{"2":{"6":1}}],["txtjulia",{"2":{"6":3}}],["txtdatabase",{"2":{"6":1}}],["txt",{"2":{"1":2,"4":1,"5":1}}],["thomsonsampling",{"2":{"157":1}}],["thompson",{"2":{"157":3}}],["thompsonsampling",{"2":{"156":1,"157":6}}],["thoroughly",{"2":{"111":1}}],["thought",{"2":{"145":1,"147":1,"152":1}}],["though",{"2":{"102":1}}],["those",{"2":{"57":1,"63":1,"91":1,"129":1,"131":1,"137":1,"156":2,"160":1}}],["than",{"2":{"27":1,"63":2,"66":2,"92":2,"102":1,"120":1,"156":7,"160":4}}],["thanks",{"2":{"99":1,"157":2}}],["thank",{"2":{"11":1}}],["that",{"2":{"2":1,"4":1,"5":3,"6":15,"9":2,"12":2,"14":3,"15":1,"17":1,"18":1,"20":4,"21":3,"22":3,"23":6,"25":2,"31":1,"35":2,"36":2,"41":2,"47":1,"48":5,"50":4,"51":21,"53":1,"55":2,"57":1,"59":3,"61":3,"62":1,"63":22,"66":6,"68":1,"71":1,"73":4,"81":3,"83":2,"84":1,"85":1,"87":1,"89":1,"90":1,"93":1,"94":3,"95":2,"96":1,"97":2,"99":4,"100":3,"101":2,"102":10,"104":1,"109":2,"111":2,"113":1,"115":3,"116":2,"118":1,"120":1,"126":2,"134":2,"136":3,"137":1,"138":2,"143":1,"145":1,"147":2,"148":2,"149":2,"152":1,"156":70,"157":24,"159":1,"160":49}}],["third",{"2":{"20":1,"50":1,"51":1,"157":1}}],["think",{"2":{"95":1,"102":1,"120":1,"121":2,"122":1,"145":1,"146":1,"152":1,"153":1,"156":2,"160":1}}],["thinking",{"2":{"20":2,"50":2,"51":3,"157":3}}],["things",{"2":{"11":1,"34":1}}],["this",{"0":{"5":1},"2":{"0":1,"1":1,"2":1,"3":1,"5":2,"6":2,"7":1,"9":1,"10":2,"12":1,"16":1,"20":2,"23":3,"31":1,"34":1,"36":1,"40":2,"46":1,"48":2,"51":10,"53":2,"57":1,"59":1,"61":2,"63":6,"64":1,"66":4,"70":1,"72":2,"73":1,"76":1,"85":1,"89":1,"90":1,"93":2,"98":1,"100":1,"101":2,"102":2,"115":1,"117":1,"118":1,"120":1,"121":1,"122":1,"136":2,"137":2,"138":3,"147":2,"148":2,"149":2,"156":57,"157":15,"159":2,"160":17}}],["throw==false",{"2":{"51":1,"157":1}}],["throw=true",{"2":{"51":2,"157":2}}],["thrown",{"2":{"51":1,"72":1,"157":1}}],["throw",{"2":{"51":4,"102":1,"157":4}}],["throughout",{"2":{"90":1}}],["through",{"2":{"6":1,"10":1,"34":1,"51":1,"66":1,"93":1,"106":1,"145":1,"146":1,"152":1,"153":1,"156":3,"157":3,"160":2}}],["thread",{"2":{"83":1}}],["threads`",{"2":{"57":1}}],["threads",{"2":{"45":1,"63":6,"156":1,"160":18}}],["threshold",{"2":{"63":1,"160":4}}],["three",{"2":{"11":1,"16":1,"59":1,"65":1,"82":1,"84":1,"156":4,"160":3}}],["these",{"2":{"9":1,"14":1,"36":1,"51":1,"57":3,"63":2,"66":1,"73":1,"100":1,"106":1,"111":1,"112":1,"129":1,"131":1,"137":3,"156":3,"157":1,"160":2}}],["then",{"2":{"9":2,"10":1,"11":1,"18":1,"27":1,"36":1,"46":1,"51":1,"59":1,"62":1,"63":4,"66":1,"83":1,"84":2,"85":2,"93":1,"100":1,"101":2,"156":8,"157":1,"160":9}}],["theory",{"2":{"6":1,"149":1}}],["their",{"2":{"6":2,"9":1,"22":1,"25":1,"30":1,"51":1,"57":2,"63":3,"66":1,"70":1,"96":1,"100":1,"107":1,"126":1,"156":7,"157":2,"160":8}}],["there",{"2":{"6":2,"18":1,"22":3,"23":1,"25":2,"26":1,"29":1,"30":1,"33":1,"36":1,"38":2,"39":1,"40":1,"41":2,"51":5,"53":1,"62":1,"63":2,"65":1,"66":3,"68":1,"69":1,"72":1,"81":1,"82":1,"95":1,"96":1,"98":1,"99":1,"101":2,"120":2,"121":1,"156":12,"157":5,"160":2}}],["themselves",{"2":{"156":1}}],["theme",{"0":{"139":2,"140":2},"1":{"141":2,"142":2,"143":2,"144":2,"145":2,"146":2,"147":2,"148":2,"149":2,"150":2,"151":2,"152":2,"153":2},"2":{"138":6}}],["themes",{"2":{"138":5}}],["them",{"2":{"1":4,"2":1,"5":1,"6":1,"7":1,"9":1,"11":1,"15":1,"20":1,"22":2,"23":6,"25":1,"26":1,"35":1,"36":1,"51":4,"57":1,"59":1,"61":1,"63":4,"65":1,"66":3,"75":1,"95":1,"99":1,"100":1,"102":1,"120":1,"136":3,"146":1,"147":1,"148":1,"149":1,"153":1,"156":19,"157":4,"160":4}}],["they",{"2":{"0":1,"9":1,"19":1,"20":2,"22":2,"23":2,"25":1,"26":1,"51":4,"63":1,"65":1,"97":1,"100":2,"102":1,"136":1,"137":1,"140":2,"149":2,"156":7,"157":4,"160":1}}],["the",{"0":{"6":1,"76":1,"77":1,"78":1,"82":1},"2":{"0":4,"1":24,"2":3,"3":2,"4":7,"5":9,"6":91,"7":6,"9":39,"10":11,"11":8,"12":12,"13":2,"14":7,"15":5,"16":5,"17":6,"18":10,"19":7,"20":22,"21":5,"22":16,"23":49,"24":1,"25":9,"26":8,"27":12,"28":6,"29":4,"30":9,"31":3,"32":2,"33":3,"34":5,"35":5,"36":4,"38":1,"40":6,"41":3,"42":3,"44":1,"45":1,"47":1,"48":33,"50":21,"51":192,"52":1,"54":15,"55":9,"56":13,"57":48,"59":34,"60":2,"61":6,"62":29,"63":233,"64":1,"65":14,"66":98,"68":3,"69":3,"70":5,"71":3,"72":15,"73":11,"74":1,"75":4,"76":7,"77":2,"79":1,"80":2,"81":3,"82":4,"83":19,"84":18,"85":20,"86":3,"87":4,"89":5,"90":4,"91":14,"92":8,"93":5,"94":9,"95":4,"96":9,"97":5,"98":13,"99":9,"100":37,"101":35,"102":75,"104":6,"106":10,"107":3,"109":22,"111":17,"112":20,"113":5,"115":6,"116":6,"117":7,"118":4,"120":31,"121":12,"122":2,"126":6,"127":2,"129":4,"131":3,"133":1,"134":4,"136":14,"137":20,"138":17,"140":1,"141":1,"143":10,"144":3,"145":5,"146":12,"147":6,"148":7,"149":22,"151":4,"152":6,"153":13,"155":2,"156":1050,"157":342,"158":16,"159":3,"160":585}}],["toml",{"2":{"77":1,"156":2}}],["touches",{"2":{"62":1}}],["total",{"2":{"57":1,"63":6,"156":1,"157":1,"160":10}}],["toy",{"2":{"51":1,"102":1,"157":1}}],["tone",{"2":{"23":1,"143":1}}],["took",{"2":{"51":1,"102":1,"157":1}}],["too",{"2":{"15":1,"46":1,"51":1,"66":1,"102":1,"111":1,"136":2,"147":1,"148":1,"156":5,"157":2}}],["tool",{"2":{"11":1,"20":1,"21":1,"102":4,"156":13}}],["tools",{"0":{"47":1,"55":1},"1":{"48":1,"49":1,"50":1,"51":1,"56":1,"57":1,"58":1,"59":1,"60":1,"61":1,"62":1,"63":1},"2":{"9":1,"56":1,"57":1,"66":2,"102":2,"156":1}}],["tokenizes",{"2":{"160":1}}],["tokenizers",{"2":{"160":1}}],["tokenizer",{"2":{"156":1}}],["tokenize",{"2":{"65":2,"156":1,"160":2}}],["tokens=",{"2":{"156":1}}],["tokens=2500",{"2":{"19":1,"156":2}}],["tokens",{"2":{"10":1,"19":2,"21":1,"22":1,"25":1,"29":1,"30":1,"35":1,"63":2,"73":4,"75":1,"83":1,"91":2,"92":1,"156":60,"160":7}}],["token",{"2":{"9":1,"17":1,"28":1,"63":1,"75":1,"100":1,"156":53,"157":1,"160":11}}],["today",{"2":{"6":1,"21":1,"22":1,"25":1,"29":1,"39":1,"41":1,"83":1,"156":8}}],["topics",{"2":{"22":1,"25":1,"29":1,"30":1}}],["topic",{"2":{"4":2,"5":1,"6":2}}],["top",{"2":{"1":1,"5":2,"6":13,"51":1,"56":1,"57":1,"59":1,"61":4,"63":22,"82":1,"149":1,"156":5,"157":1,"160":30}}],["togetheropenaischema",{"2":{"29":2,"102":1,"156":2}}],["together",{"0":{"29":1},"2":{"1":1,"4":3,"5":1,"6":3,"18":1,"29":3,"35":1,"63":1,"69":1,"93":1,"102":6,"106":1,"156":4,"160":2}}],["to",{"0":{"17":1,"83":1,"84":1,"85":1},"2":{"0":2,"1":11,"2":1,"3":2,"4":4,"5":5,"6":17,"7":2,"9":21,"10":13,"11":9,"12":6,"13":2,"14":4,"15":4,"16":3,"17":2,"18":6,"20":11,"21":1,"22":6,"23":20,"24":1,"25":5,"26":1,"27":6,"28":7,"29":3,"30":6,"31":1,"33":5,"34":6,"35":1,"36":7,"38":3,"39":2,"40":2,"41":6,"42":1,"45":1,"46":1,"47":1,"48":16,"50":11,"51":100,"53":3,"54":9,"55":6,"56":9,"57":20,"59":9,"60":1,"61":4,"62":14,"63":151,"64":1,"65":14,"66":43,"68":1,"69":1,"70":7,"71":2,"72":2,"73":4,"74":4,"75":2,"76":5,"77":1,"79":2,"80":3,"81":3,"82":3,"83":8,"84":18,"85":17,"86":2,"87":4,"89":7,"90":1,"91":4,"92":2,"93":3,"94":3,"95":1,"96":9,"97":6,"99":6,"100":21,"101":7,"102":31,"104":2,"106":3,"109":7,"111":4,"112":5,"113":2,"115":4,"116":3,"117":6,"118":1,"120":10,"121":5,"122":2,"126":2,"129":1,"131":1,"134":2,"136":14,"137":10,"138":6,"143":1,"145":1,"146":4,"147":2,"148":2,"149":5,"152":1,"153":4,"156":555,"157":146,"158":10,"159":3,"160":288}}],["ss",{"2":{"136":2,"137":3}}],["skilled",{"2":{"143":1}}],["skips",{"2":{"85":1,"99":1,"156":9,"160":2}}],["skipped",{"2":{"51":1,"156":1}}],["skip",{"2":{"51":8,"63":5,"149":1,"156":7,"157":1,"160":7}}],["sky",{"2":{"66":1,"156":3}}],["svilupp",{"2":{"66":1,"156":5,"160":1}}],["snippet",{"2":{"79":1,"115":1}}],["snippets",{"2":{"51":1,"147":1,"148":1,"156":1,"160":6}}],["snow",{"2":{"66":1,"156":1}}],["slicing",{"2":{"147":1,"148":1}}],["slightly",{"2":{"66":1,"156":1}}],["slot",{"2":{"102":1}}],["slowly",{"2":{"92":1}}],["slow",{"2":{"92":1,"156":1}}],["sleep",{"2":{"73":1,"156":1}}],["slack",{"2":{"56":1,"65":1}}],["swiftly",{"2":{"66":1,"156":1}}],["switching",{"2":{"51":1,"157":1}}],["switch",{"2":{"10":2,"50":1}}],["swap",{"2":{"63":1,"160":1}}],["src",{"2":{"62":1,"66":1,"156":5,"160":1}}],["sqrt",{"2":{"157":1}}],["square",{"2":{"57":1,"63":2,"160":2}}],["sqlcoder",{"2":{"81":1}}],["sqlservercentral",{"2":{"19":1,"156":2}}],["sql",{"2":{"19":3,"156":6}}],["smoke",{"2":{"66":1,"156":1}}],["smith",{"2":{"106":2}}],["smiling",{"2":{"41":1}}],["smiles",{"2":{"39":1,"40":1}}],["smirks",{"2":{"40":1}}],["smallint",{"2":{"84":5}}],["small",{"2":{"6":1,"10":1,"22":1,"25":1,"62":1,"136":1,"145":1,"148":1,"152":1,"157":2}}],["smaller",{"2":{"1":1,"66":6,"102":1,"156":7,"157":1}}],["shiny",{"2":{"156":1}}],["shimmering",{"2":{"66":2,"156":2}}],["shapley",{"2":{"149":1}}],["shap",{"2":{"149":10}}],["sharegptschema",{"2":{"156":3}}],["sharegpt",{"2":{"87":1}}],["share",{"2":{"70":1,"71":1,"89":1}}],["shared",{"2":{"61":2,"63":2,"66":1,"146":1,"153":1,"156":1,"160":2}}],["sharing",{"2":{"23":1}}],["shallow",{"2":{"51":1,"157":1}}],["shall",{"2":{"11":1}}],["shot",{"2":{"157":1}}],["shortcut",{"2":{"160":1}}],["shortcuts",{"2":{"120":1,"121":1,"122":1}}],["short",{"2":{"51":1,"57":1,"66":1,"106":1,"111":1,"113":1,"136":2,"156":4,"157":1}}],["shorter",{"2":{"28":1}}],["should",{"2":{"1":1,"6":1,"11":1,"21":1,"34":1,"40":1,"51":1,"59":1,"63":2,"66":1,"76":1,"77":1,"81":2,"84":2,"89":1,"98":1,"102":2,"111":1,"136":4,"137":4,"138":7,"143":3,"149":2,"156":28,"157":6,"160":2}}],["showcase",{"2":{"134":1}}],["shows",{"2":{"19":1,"23":1,"59":1,"87":1,"156":4}}],["show",{"2":{"1":1,"6":1,"51":2,"81":1,"84":1,"156":1,"157":3}}],["scene",{"2":{"156":1}}],["scenarios",{"2":{"51":1,"157":2}}],["science",{"2":{"149":1}}],["scientific",{"2":{"57":1}}],["scientist",{"2":{"23":2}}],["scans",{"2":{"156":3}}],["scanned",{"2":{"155":1}}],["scan",{"2":{"136":1,"137":1,"156":4}}],["scale",{"2":{"57":1,"112":2,"113":2}}],["scoring=thompsonsampling",{"2":{"51":1,"157":1}}],["scoring",{"2":{"51":1,"112":1,"157":10,"160":1}}],["score==nothing",{"2":{"160":1}}],["scores=false",{"2":{"160":1}}],["scores",{"2":{"51":1,"57":1,"63":5,"112":1,"157":7,"160":19}}],["scored",{"2":{"20":1,"50":1,"51":1,"157":1}}],["scoreparametersstringstringstringsubstrin",{"2":{"6":1}}],["scoreretrieval",{"2":{"6":1}}],["score",{"2":{"5":3,"6":10,"9":2,"51":20,"57":1,"63":12,"112":2,"156":4,"157":32,"160":30}}],["scope",{"2":{"51":1,"157":1}}],["script",{"2":{"156":2}}],["scratch",{"2":{"51":1,"156":2}}],["scratches",{"2":{"40":1}}],["scrollable",{"2":{"23":1}}],["screenshot",{"2":{"19":3,"155":1,"156":4}}],["schema=myschema",{"2":{"157":1}}],["schema=json3",{"2":{"102":2}}],["schema=pt",{"2":{"82":1}}],["schemas",{"0":{"96":1},"2":{"94":1,"96":2,"156":2}}],["schema",{"0":{"41":1,"82":1},"2":{"9":2,"21":5,"22":2,"25":1,"26":1,"28":1,"29":1,"30":1,"36":1,"38":1,"41":7,"44":2,"45":2,"46":2,"51":4,"82":5,"95":1,"96":1,"100":2,"101":7,"102":5,"156":128,"157":5}}],["side",{"2":{"101":1}}],["sister",{"2":{"87":1}}],["since",{"2":{"85":1,"101":1,"136":1}}],["singletons",{"2":{"156":1}}],["single",{"2":{"66":2,"87":1,"156":8,"160":2}}],["situations",{"2":{"68":1}}],["silent",{"2":{"66":2,"156":2}}],["sibblings",{"2":{"51":2,"157":2}}],["size`",{"2":{"156":1}}],["size",{"2":{"44":2,"45":2,"46":1,"63":2,"156":5,"160":5}}],["sizes",{"2":{"7":1,"63":2,"160":5}}],["sig",{"2":{"102":3}}],["significant",{"2":{"111":1,"137":2}}],["signing",{"2":{"71":1}}],["sign",{"2":{"53":1}}],["signatures",{"2":{"51":1,"157":1}}],["signature",{"2":{"20":1,"50":1,"59":1,"60":4,"102":7,"156":8}}],["sigh",{"2":{"40":1}}],["simultaneously",{"2":{"20":1,"50":1,"51":1,"157":1}}],["similarly",{"2":{"92":1,"156":1}}],["similarity",{"2":{"1":2,"7":1,"46":2,"62":2,"63":3,"65":1,"66":1,"156":1,"160":17}}],["similar",{"2":{"1":1,"9":2,"51":1,"63":3,"66":1,"97":1,"100":2,"156":6,"157":2,"160":6}}],["simplistic",{"2":{"156":1}}],["simplification",{"2":{"101":1}}],["simply",{"2":{"1":1,"9":1,"19":1,"23":1,"27":1,"28":1,"35":1,"51":1,"63":1,"65":1,"84":2,"85":2,"89":1,"91":2,"100":1,"133":1,"156":1,"157":1,"160":8}}],["simpleanswerer",{"2":{"63":2,"156":1,"160":8}}],["simplegenerator",{"2":{"63":2,"156":1,"160":5}}],["simplerefiner",{"2":{"63":1,"156":1,"160":6}}],["simpleretriever",{"2":{"63":5,"156":1,"160":9}}],["simplerephraser",{"2":{"61":1,"156":1,"160":4}}],["simplest",{"2":{"59":1,"63":1,"83":1,"160":1}}],["simpleindexer",{"2":{"57":2,"63":3,"156":1,"160":6}}],["simple",{"0":{"0":1,"33":1,"38":1,"44":1},"1":{"1":1},"2":{"6":1,"7":1,"9":1,"15":1,"21":1,"42":1,"46":1,"48":1,"51":1,"57":1,"73":2,"75":2,"81":1,"85":2,"113":1,"124":1,"156":14,"157":1,"160":6}}],["speak",{"2":{"85":3,"149":1,"156":4}}],["spend",{"2":{"74":1}}],["spending",{"0":{"74":1},"2":{"71":1,"74":1,"91":1}}],["speeds",{"2":{"160":1}}],["speed",{"2":{"45":1}}],["spec",{"2":{"156":2}}],["specs",{"2":{"156":1}}],["specialist",{"2":{"126":1}}],["specializes",{"2":{"138":1}}],["specialized",{"2":{"51":1,"115":1,"157":2}}],["specializing",{"2":{"116":1,"117":1}}],["special",{"2":{"106":4,"107":2,"111":3,"136":2,"137":2,"140":2,"146":2,"147":2,"148":2,"149":2,"153":5,"160":2}}],["specifying",{"2":{"63":1,"156":1,"160":1}}],["specify",{"2":{"14":1,"33":1,"61":1,"63":2,"66":1,"82":2,"102":1,"120":1,"156":13,"160":2}}],["specified",{"2":{"62":1,"63":1,"66":2,"138":2,"156":10,"157":3,"160":3}}],["specifies",{"2":{"10":1,"51":1,"157":1}}],["specifically",{"2":{"106":1,"156":1}}],["specification",{"2":{"96":1,"102":1,"156":1}}],["specifications",{"2":{"66":1,"102":1,"156":1}}],["specific",{"2":{"5":1,"9":1,"12":2,"22":2,"23":1,"25":2,"29":1,"30":1,"41":1,"56":1,"65":1,"66":2,"87":1,"99":1,"100":1,"102":2,"111":1,"115":2,"120":2,"121":1,"129":1,"131":1,"137":2,"147":1,"148":1,"149":1,"156":9,"160":1}}],["spectacles",{"2":{"40":1}}],["spider",{"2":{"17":1,"84":1,"156":1}}],["span",{"2":{"160":1}}],["spanish",{"2":{"13":1}}],["spain",{"2":{"91":2,"92":2}}],["sparrow",{"2":{"85":4,"156":5}}],["sparse",{"2":{"62":1,"160":3}}],["sparsearrays",{"2":{"0":1,"55":1,"160":4}}],["spawn",{"2":{"45":1,"156":1}}],["spaces",{"2":{"160":3}}],["space",{"2":{"22":1,"25":1,"66":1,"156":1}}],["spliter",{"2":{"86":1}}],["splitters",{"2":{"66":1,"156":1}}],["splitter",{"2":{"65":2,"66":10,"86":2,"156":13,"160":1}}],["splitting",{"2":{"63":2,"66":11,"156":12,"160":3}}],["splits",{"2":{"63":1,"66":1,"156":1,"160":7}}],["split",{"2":{"7":1,"20":2,"50":2,"51":1,"63":1,"65":3,"66":14,"86":3,"136":1,"156":16,"157":4,"160":4}}],["satisfy",{"2":{"120":1}}],["saving",{"2":{"63":1,"156":1,"160":2}}],["saves",{"2":{"51":1,"156":3,"157":2}}],["saved",{"2":{"10":1,"23":2,"51":2,"63":1,"85":1,"89":1,"99":1,"156":5,"157":2,"160":3}}],["save",{"2":{"1":2,"3":1,"6":1,"12":1,"23":4,"31":1,"51":2,"62":2,"71":1,"75":2,"85":3,"87":3,"89":1,"102":1,"156":17,"157":1}}],["safety",{"2":{"66":1,"156":1}}],["safely",{"2":{"51":1,"156":1}}],["safe",{"2":{"51":4,"156":6}}],["sampling",{"2":{"156":1,"157":5}}],["samplenode",{"2":{"51":25,"156":1,"157":45}}],["sample",{"2":{"51":23,"156":11,"157":58}}],["samples=1",{"2":{"51":2,"157":2}}],["samples=2`",{"2":{"20":1,"50":1,"51":1,"157":1}}],["samples",{"2":{"20":3,"48":3,"50":3,"51":33,"156":4,"157":55}}],["same",{"2":{"5":1,"9":2,"10":1,"20":2,"33":1,"48":3,"50":1,"51":2,"63":2,"66":2,"73":1,"76":1,"84":2,"85":1,"89":1,"98":1,"100":2,"101":1,"120":1,"121":1,"156":6,"157":9,"160":12}}],["salty",{"2":{"85":2,"156":2}}],["salt",{"2":{"18":2}}],["san",{"2":{"18":1}}],["say",{"2":{"11":1,"20":2,"21":1,"22":3,"25":2,"26":1,"27":1,"28":3,"29":1,"30":1,"33":2,"36":1,"38":1,"39":1,"41":2,"50":2,"51":2,"83":1,"85":3,"92":1,"101":1,"104":1,"109":1,"156":15,"157":11}}],["said",{"2":{"9":1,"48":1,"100":1,"156":1}}],["sum",{"2":{"156":2}}],["summarizing",{"2":{"136":1,"137":1,"138":1}}],["summarize",{"2":{"120":1,"136":2,"137":1}}],["summary",{"2":{"66":1,"136":1,"137":5,"147":1,"148":1,"149":1,"156":1}}],["suggesting",{"2":{"156":1}}],["suggestions",{"2":{"23":1}}],["suggests",{"2":{"134":1}}],["suggested",{"2":{"120":1}}],["suggest",{"2":{"120":1,"121":1}}],["suffix",{"2":{"51":3,"156":5}}],["suffering",{"2":{"34":1,"156":2}}],["suceeding",{"2":{"160":1}}],["succinct",{"2":{"115":1}}],["successfully",{"2":{"51":2,"156":3,"157":1}}],["successful",{"2":{"51":5,"84":1,"102":1,"107":1,"156":2,"157":8}}],["success",{"2":{"3":1,"51":7,"84":2,"156":9,"157":13}}],["succeeding",{"2":{"1":1}}],["such",{"2":{"51":1,"106":1,"156":9,"157":1}}],["subject",{"2":{"143":1}}],["subheadings",{"2":{"136":2}}],["subheading",{"2":{"136":1}}],["subtype",{"2":{"95":1}}],["subtypes",{"2":{"51":2,"60":2,"63":3,"96":2,"157":2,"160":4}}],["submitted",{"2":{"70":1}}],["subcomponents",{"2":{"63":1,"160":1}}],["sub",{"2":{"57":1,"59":1,"63":6,"75":1,"101":1,"138":1,"160":7}}],["subseq",{"2":{"156":5}}],["subsequence",{"2":{"65":6,"66":17,"156":25}}],["subsequent",{"2":{"51":1,"66":1,"84":1,"156":1,"157":3}}],["subset",{"2":{"146":1,"153":1}}],["substantial",{"2":{"137":1}}],["substring",{"2":{"57":2,"156":5,"160":2}}],["subarray",{"2":{"44":1}}],["subfolder",{"2":{"10":3}}],["subfolders",{"2":{"10":1}}],["super",{"2":{"136":1,"137":1}}],["supertype",{"2":{"95":1}}],["superseded",{"2":{"41":1}}],["supports",{"2":{"156":2,"160":2}}],["support",{"2":{"22":2,"26":1,"31":1,"56":1,"57":2,"63":10,"70":1,"156":7,"160":21}}],["supported",{"2":{"9":1,"42":1,"56":1,"63":2,"102":2,"156":5,"160":4}}],["suppose",{"2":{"4":1,"6":1}}],["supplied",{"2":{"9":1,"48":1,"51":1,"100":1,"157":1}}],["suitable",{"2":{"21":1,"66":1,"129":1,"131":1,"143":2,"156":2}}],["survey",{"2":{"138":5,"140":1}}],["surrounding",{"2":{"63":1,"160":7}}],["surface",{"2":{"12":1,"59":1,"156":2}}],["sure",{"2":{"3":1,"6":1,"7":1,"36":2,"51":1,"63":1,"76":2,"89":2,"102":1,"156":4,"157":1,"160":2}}],["sorry",{"2":{"156":2}}],["soft",{"2":{"74":2}}],["solve",{"2":{"145":1,"147":1,"148":1,"152":2}}],["solving",{"2":{"145":2,"147":2,"148":2,"152":1}}],["solutions",{"2":{"89":1}}],["solution",{"2":{"72":1,"120":2,"145":1,"147":1,"148":1,"152":1,"156":1}}],["solid",{"2":{"66":1,"156":5,"160":1}}],["source=",{"2":{"160":1}}],["source2",{"2":{"63":1,"160":1}}],["source1",{"2":{"63":1,"160":1}}],["sourced",{"2":{"9":1}}],["source",{"0":{"80":1},"2":{"4":1,"5":1,"9":1,"12":1,"22":1,"23":1,"25":1,"51":12,"54":1,"57":1,"63":16,"66":7,"80":1,"85":1,"102":2,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":1,"121":1,"122":1,"124":1,"126":1,"127":1,"129":1,"131":2,"133":1,"134":1,"136":1,"137":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":2,"147":1,"148":1,"149":1,"151":1,"152":1,"153":2,"155":1,"156":129,"157":38,"158":2,"159":1,"160":104}}],["sources=false",{"2":{"160":1}}],["sources=map",{"2":{"57":1}}],["sources",{"2":{"3":2,"5":1,"57":3,"63":17,"160":40}}],["so",{"2":{"3":1,"6":1,"14":2,"17":1,"20":4,"22":2,"23":1,"25":2,"29":1,"30":2,"35":1,"41":1,"50":1,"51":1,"59":1,"63":2,"66":1,"72":1,"73":2,"77":1,"85":2,"97":1,"99":1,"101":3,"102":5,"106":1,"120":1,"121":1,"136":1,"156":20,"157":3,"159":1,"160":2}}],["sometimes",{"2":{"102":1}}],["something",{"2":{"29":1,"30":1,"40":1,"41":1,"101":1,"143":1}}],["somewhere",{"2":{"99":1}}],["some",{"2":{"1":2,"6":1,"7":1,"9":1,"19":2,"20":2,"21":1,"23":2,"26":1,"27":1,"36":1,"41":2,"50":1,"51":1,"57":4,"59":3,"62":1,"63":2,"65":2,"66":1,"81":1,"84":1,"97":1,"99":2,"100":1,"102":3,"109":2,"137":1,"145":1,"152":1,"156":22,"157":1,"160":4}}],["synthetic",{"2":{"160":2}}],["syntax",{"2":{"1":1,"12":1,"19":1,"20":1,"23":4,"50":1,"51":1,"73":1,"84":1,"85":1,"87":1,"102":1,"143":1,"144":1,"147":3,"148":3,"151":1,"156":4,"157":2}}],["sync",{"2":{"156":1}}],["synced",{"2":{"71":1,"89":1}}],["symphony",{"2":{"66":1,"156":1}}],["symbols",{"2":{"156":1,"160":3}}],["symbol=",{"2":{"63":1,"157":1,"160":1}}],["symbol",{"2":{"5":1,"12":2,"23":3,"57":1,"63":3,"85":2,"99":1,"156":18,"157":6,"160":22}}],["system+user",{"2":{"157":1}}],["systematic",{"2":{"145":1,"147":1,"148":1,"152":1}}],["system=",{"2":{"85":1,"99":1}}],["systemmessage",{"2":{"11":3,"23":3,"34":1,"40":1,"83":1,"85":2,"98":1,"99":2,"101":1,"156":11}}],["systems",{"2":{"9":1,"66":2,"100":1,"111":1,"156":2}}],["system",{"0":{"0":1,"59":1},"1":{"1":1},"2":{"2":1,"11":1,"12":2,"16":2,"23":2,"35":2,"51":1,"57":2,"59":2,"76":1,"85":3,"96":1,"98":1,"99":3,"101":1,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"120":1,"121":1,"122":1,"124":1,"126":1,"127":1,"129":1,"131":1,"133":5,"134":2,"136":1,"137":1,"138":1,"141":1,"142":1,"143":1,"144":1,"145":1,"146":1,"147":1,"148":1,"149":1,"151":1,"152":1,"153":1,"155":1,"156":33,"157":2}}],["seq",{"2":{"156":5}}],["sequentially",{"2":{"45":1,"51":2,"156":1,"157":2}}],["sequences",{"2":{"66":1,"156":1,"160":1}}],["sequence",{"2":{"20":2,"66":2,"97":1,"156":6}}],["sedan",{"2":{"156":1}}],["segments",{"2":{"66":2,"156":2}}],["segment",{"2":{"63":1,"160":1}}],["separator=",{"2":{"66":1,"156":1}}],["separator",{"2":{"66":9,"156":9,"157":1}}],["separators",{"2":{"63":1,"66":19,"86":4,"156":20,"160":7}}],["separators=",{"2":{"59":1,"63":1,"66":4,"156":4,"160":1}}],["separated",{"2":{"156":1,"157":1,"160":1}}],["separate",{"2":{"1":1,"16":1,"55":1,"156":3,"157":2,"159":1,"160":2}}],["selects",{"2":{"157":1}}],["selection",{"2":{"157":2}}],["selecting",{"2":{"156":1}}],["selectively",{"2":{"51":1,"157":1}}],["select",{"2":{"57":1,"91":1,"106":1,"126":1,"156":1,"157":7}}],["selected",{"2":{"12":1,"27":1,"63":1,"156":3,"160":1}}],["self",{"2":{"47":1,"84":1,"100":1,"120":1,"156":1,"157":1}}],["sessions",{"2":{"77":1,"156":2,"157":1}}],["session",{"2":{"41":1,"51":1,"79":1,"156":1,"157":7}}],["series",{"2":{"66":1,"156":1}}],["serialization",{"2":{"0":1,"85":1,"99":1}}],["serves",{"2":{"57":1}}],["serve",{"2":{"34":1,"81":1}}],["serverpreference",{"2":{"156":1}}],["server`",{"2":{"156":1}}],["servers",{"2":{"156":1}}],["server",{"0":{"27":1},"2":{"24":1,"27":4,"36":1,"156":16}}],["services",{"2":{"70":1}}],["service",{"2":{"22":1,"25":1,"70":1,"81":1}}],["sense",{"2":{"102":1,"156":1}}],["sensitive",{"2":{"21":1,"65":1,"81":1,"156":1}}],["sends",{"2":{"63":1,"158":1,"160":1}}],["send",{"2":{"51":1,"73":1,"83":1,"84":1,"96":1,"97":1,"101":1,"156":3,"157":1}}],["sending",{"2":{"22":1,"26":1,"65":1,"83":1,"85":2,"156":11}}],["senior",{"2":{"23":2}}],["sentences",{"2":{"57":3,"63":3,"65":3,"66":3,"86":1,"149":1,"156":4,"160":9}}],["sentence",{"2":{"30":1,"57":1,"63":4,"65":1,"66":6,"75":1,"86":2,"102":3,"156":6,"160":6}}],["sent",{"2":{"21":1,"81":1,"96":1,"102":1,"160":1}}],["sentiment",{"2":{"16":1}}],["several",{"2":{"21":1,"36":1,"102":1,"120":1,"138":1,"156":2}}],["seven",{"2":{"6":1}}],["secret",{"2":{"71":1,"89":1,"149":1}}],["secrets",{"2":{"66":4,"156":4}}],["sections",{"2":{"137":2,"143":2,"156":2}}],["section",{"2":{"16":1,"21":1,"36":1,"55":1,"68":1,"71":1,"89":1,"92":1,"93":1,"136":2,"137":2,"143":2}}],["seconds",{"2":{"9":1,"10":1,"19":2,"21":1,"22":1,"25":1,"29":1,"30":1,"51":3,"73":1,"83":1,"91":2,"92":1,"100":1,"156":18,"157":2}}],["second",{"2":{"6":4,"63":1,"66":3,"83":1,"101":3,"156":5,"157":1,"160":1}}],["seas",{"2":{"85":2,"156":2}}],["seats",{"2":{"84":2}}],["sea",{"2":{"66":1,"156":1}}],["searches",{"2":{"156":3}}],["searching",{"2":{"156":2}}],["search",{"2":{"10":1,"12":1,"15":1,"20":2,"23":3,"48":1,"51":2,"53":1,"54":11,"57":4,"62":2,"63":2,"85":1,"106":2,"107":2,"116":2,"117":5,"156":8,"157":4,"158":12,"159":1,"160":12}}],["seamless",{"0":{"10":1}}],["semantic",{"2":{"7":1,"15":1,"117":1,"160":3}}],["semi",{"2":{"5":1,"6":4}}],["semijoin",{"2":{"5":1}}],["settings",{"2":{"76":1,"157":1}}],["setting",{"0":{"74":1,"77":1},"2":{"51":1,"61":1,"86":1,"156":1,"157":1}}],["setup",{"0":{"81":1},"2":{"21":1,"68":1,"76":1,"80":1,"89":1,"146":1,"153":1,"157":1}}],["sets",{"2":{"3":1,"4":4,"5":2,"6":5,"63":1,"102":1,"111":1,"156":1,"157":1,"160":3}}],["set",{"0":{"6":1,"72":2},"2":{"2":2,"3":1,"6":5,"7":1,"9":2,"11":1,"14":1,"17":1,"20":2,"22":1,"23":1,"26":1,"28":2,"29":2,"30":2,"41":2,"47":1,"50":2,"51":20,"53":1,"55":1,"56":2,"57":1,"59":2,"63":5,"64":1,"65":1,"66":1,"71":1,"72":2,"73":1,"74":2,"76":5,"77":2,"82":1,"83":1,"84":1,"89":5,"100":1,"102":2,"106":1,"117":1,"136":1,"137":1,"138":1,"146":1,"148":1,"149":1,"153":1,"156":46,"157":13,"160":12}}],["seem",{"2":{"156":1}}],["seems",{"2":{"156":2}}],["seel",{"2":{"51":1,"157":1}}],["seek",{"2":{"11":1,"34":1,"40":1}}],["see",{"2":{"1":3,"6":1,"7":3,"9":4,"12":3,"16":1,"18":1,"20":1,"21":1,"22":3,"23":4,"25":2,"31":1,"36":2,"48":2,"51":11,"54":1,"55":1,"57":4,"62":1,"63":7,"68":1,"69":1,"73":2,"76":2,"77":2,"78":1,"80":1,"81":2,"82":1,"83":1,"84":1,"86":1,"89":2,"92":1,"93":1,"96":1,"99":2,"100":4,"102":8,"156":63,"157":17,"158":1,"160":25}}],["steroids",{"2":{"102":1}}],["steps`",{"2":{"137":1}}],["steps",{"2":{"23":1,"57":3,"59":1,"62":1,"63":4,"100":1,"101":1,"120":4,"136":1,"137":14,"143":1,"145":1,"147":1,"148":1,"156":2,"160":6}}],["step",{"2":{"1":1,"7":1,"20":2,"50":2,"51":3,"55":1,"57":4,"59":7,"61":3,"62":2,"63":10,"84":1,"85":1,"99":1,"101":4,"102":1,"109":1,"115":1,"116":1,"117":1,"118":1,"120":6,"121":4,"122":2,"145":2,"152":2,"157":3,"160":14}}],["stop",{"2":{"74":1,"156":2,"160":1}}],["stopwords",{"2":{"65":2,"160":1}}],["stood",{"2":{"66":1,"156":1}}],["storage",{"2":{"156":1}}],["storyteller",{"2":{"149":1}}],["storytellerexplainshap",{"0":{"149":1}}],["storytelling",{"2":{"149":1}}],["story",{"2":{"66":4,"149":6,"156":4}}],["storing",{"2":{"63":1,"160":6}}],["store",{"2":{"83":1,"85":3,"156":17}}],["stored",{"2":{"51":2,"63":1,"157":2,"160":1}}],["stores",{"2":{"9":1,"48":1,"51":2,"100":1,"156":3,"157":2,"160":3}}],["stipple",{"2":{"57":1}}],["still",{"2":{"0":1,"57":1,"72":1,"156":2}}],["styling",{"2":{"57":1,"160":5}}],["styled",{"2":{"160":1}}],["styles=",{"2":{"160":3}}],["styles",{"2":{"156":2,"160":10}}],["styler=rt",{"2":{"160":4}}],["styler",{"2":{"156":1,"160":21}}],["style=",{"2":{"66":1,"156":5,"160":1}}],["style",{"2":{"1":1,"4":2,"5":1,"6":11,"63":1,"66":1,"92":1,"129":1,"131":1,"134":1,"146":1,"149":1,"153":1,"156":10,"160":12}}],["stdout",{"2":{"51":11,"156":17}}],["stub",{"2":{"156":1}}],["stumbled",{"2":{"34":1,"156":1}}],["study",{"2":{"136":1}}],["studying",{"2":{"4":1,"6":1,"137":1}}],["studies",{"2":{"57":1}}],["studied",{"2":{"4":1,"6":1}}],["studio",{"0":{"31":1},"1":{"32":1,"33":1,"34":1,"35":1},"2":{"31":2,"35":1,"156":2}}],["strength",{"2":{"160":1}}],["stream",{"2":{"156":9,"160":3}}],["strong",{"2":{"156":4}}],["strongly",{"2":{"9":1}}],["stranger",{"2":{"66":1,"156":1}}],["strategies",{"2":{"62":1}}],["strategy",{"2":{"1":1,"62":5,"63":2,"160":3}}],["strict",{"2":{"129":1,"131":1}}],["strictly",{"2":{"62":1,"111":1,"137":1,"146":1,"153":1}}],["strin",{"2":{"102":1}}],["string=",{"2":{"66":2,"156":4}}],["strings",{"2":{"6":1,"63":3,"65":3,"66":5,"156":19,"160":14}}],["string",{"0":{"39":1},"2":{"6":4,"9":1,"12":5,"18":1,"20":1,"23":8,"30":2,"33":1,"41":1,"50":1,"51":14,"57":6,"63":4,"65":6,"66":29,"84":4,"85":5,"91":2,"100":1,"101":2,"102":15,"142":1,"146":1,"147":1,"148":1,"153":1,"156":206,"157":13,"158":1,"160":23}}],["stripping",{"2":{"118":1}}],["strip",{"2":{"66":1,"118":1,"156":1}}],["struggle",{"2":{"51":1,"157":1}}],["structtypes",{"2":{"102":1}}],["structs",{"2":{"63":1,"98":1,"102":1,"156":1,"160":1}}],["struct",{"2":{"9":2,"18":4,"30":1,"48":2,"51":4,"57":1,"59":3,"63":1,"84":5,"100":2,"102":7,"106":1,"156":23,"157":4,"160":4}}],["structures",{"2":{"134":1,"147":1,"148":1,"157":1}}],["structured",{"2":{"9":1,"18":1,"30":1,"59":1,"100":1,"102":1,"129":1,"131":1,"156":2}}],["structure",{"2":{"9":1,"18":1,"51":4,"66":3,"100":1,"136":2,"137":3,"138":1,"155":1,"156":6,"157":3,"160":2}}],["str",{"0":{"91":1},"2":{"14":1,"66":1,"91":2,"92":4,"156":28}}],["stands",{"2":{"156":1}}],["standard",{"0":{"39":1},"2":{"51":2,"63":1,"147":1,"148":1,"156":5,"160":4}}],["stays",{"2":{"73":1,"111":1,"157":1}}],["stage",{"2":{"63":1,"160":7}}],["stabilizes",{"2":{"55":1,"157":1}}],["stark",{"2":{"156":1}}],["stars",{"2":{"66":3,"156":3}}],["star",{"2":{"11":1,"34":1,"40":1,"156":5}}],["start|>assistant",{"2":{"156":1}}],["start|>user",{"2":{"156":1}}],["start>system",{"2":{"156":1}}],["startup",{"2":{"79":2,"156":1}}],["starter",{"2":{"57":1}}],["started",{"0":{"88":1},"1":{"89":1,"90":1,"91":1,"92":1},"2":{"55":1,"68":1}}],["starting",{"2":{"51":1,"126":1,"156":3,"157":2}}],["starts",{"2":{"51":1,"157":2}}],["startswith",{"2":{"51":1,"157":1}}],["start",{"0":{"91":1},"2":{"0":1,"18":1,"23":2,"27":2,"74":1,"76":1,"89":2,"91":1,"93":1,"143":1,"156":2,"157":1,"160":1}}],["status",{"2":{"156":22,"157":2}}],["statistical",{"2":{"57":1}}],["statistics",{"2":{"0":1}}],["stats",{"2":{"51":23,"157":33}}],["stateless",{"2":{"97":1}}],["state",{"2":{"18":1,"51":2,"68":1,"102":1,"156":1,"157":2}}],["statements",{"2":{"84":1,"146":1,"153":1,"156":1}}],["statement",{"2":{"16":2,"75":1,"97":1,"98":1,"127":4,"156":5}}],["states",{"2":{"5":1}}],["s",{"2":{"0":1,"1":2,"2":1,"3":1,"4":2,"5":2,"6":2,"9":2,"10":2,"12":1,"14":1,"17":1,"18":4,"19":1,"20":1,"21":3,"23":6,"31":2,"35":1,"36":1,"38":1,"39":1,"41":1,"42":1,"48":1,"50":1,"51":12,"55":1,"57":5,"59":2,"61":1,"63":8,"66":7,"68":1,"70":3,"73":1,"75":1,"76":2,"81":3,"83":5,"84":4,"85":3,"86":1,"94":1,"96":2,"99":1,"100":2,"101":3,"102":14,"109":1,"112":3,"113":2,"120":2,"121":1,"129":2,"131":3,"134":1,"136":2,"137":1,"143":1,"146":3,"149":2,"153":1,"156":64,"157":19,"160":17}}],["rules",{"2":{"160":1}}],["runtime",{"2":{"51":2,"157":2}}],["runs",{"2":{"51":2,"63":1,"72":1,"81":1,"156":2,"157":1,"160":3}}],["running",{"2":{"6":1,"21":2,"22":1,"23":1,"36":1,"57":1,"72":1,"81":3,"156":2,"157":1}}],["run",{"2":{"5":2,"6":3,"9":3,"13":1,"20":5,"21":1,"41":1,"48":3,"50":3,"51":10,"57":1,"60":1,"72":1,"76":1,"77":1,"81":1,"84":2,"85":1,"89":1,"92":1,"100":4,"102":2,"156":40,"157":31,"160":10}}],["ripple",{"2":{"66":1,"156":1}}],["river",{"2":{"66":1,"156":1}}],["right",{"2":{"6":11,"96":2,"102":2,"156":1,"157":1,"160":2}}],["rm",{"2":{"23":1,"72":1}}],["rolls",{"2":{"160":1}}],["role=",{"2":{"156":8}}],["role",{"2":{"22":1,"26":1,"57":1,"101":3,"137":1}}],["root",{"2":{"51":3,"63":5,"156":1,"157":11,"160":8}}],["robust",{"2":{"20":1,"68":1,"89":1,"100":1}}],["robustness",{"2":{"12":1,"48":1}}],["roughly",{"2":{"94":1,"160":1}}],["routines",{"2":{"63":2,"160":2}}],["routing",{"0":{"17":1},"2":{"17":1,"126":1,"156":2}}],["router",{"2":{"9":1,"100":1}}],["rounds=5",{"2":{"157":1}}],["rounds",{"2":{"51":1,"102":1,"120":1,"157":18}}],["round",{"2":{"6":2,"157":5}}],["row",{"2":{"6":3}}],["rows",{"2":{"5":1,"6":18,"160":2}}],["raises",{"2":{"156":1}}],["raised",{"2":{"51":1,"138":1,"156":2}}],["rationale",{"2":{"160":2}}],["ratio",{"2":{"157":1}}],["rating",{"2":{"5":1,"113":1,"160":3}}],["ratelimit",{"2":{"73":2}}],["rate",{"0":{"73":1},"2":{"73":3,"160":1}}],["rare",{"2":{"72":1}}],["radius",{"2":{"66":1,"156":5,"160":1}}],["raw=true",{"2":{"156":1}}],["raw",{"2":{"51":1,"54":2,"156":1,"158":2}}],["rand",{"2":{"51":2,"156":3}}],["random",{"2":{"51":1,"156":2,"157":2}}],["range",{"2":{"29":1,"30":1,"66":1,"156":1}}],["ranked",{"2":{"160":2}}],["ranking",{"2":{"7":1,"59":1}}],["rankanswer",{"2":{"6":1}}],["rank",{"2":{"5":1,"156":1,"160":4}}],["ranks",{"2":{"1":1,"160":1}}],["ragjuliaqueryhyde",{"0":{"115":1}}],["ragjudgeanswerfromcontextshort",{"0":{"113":1}}],["ragjudgeanswerfromcontext",{"0":{"112":1},"2":{"5":1,"160":2}}],["ragextractmetadatalong",{"0":{"106":1}}],["ragextractmetadatashort",{"0":{"107":1},"2":{"63":1,"160":4}}],["ragcreateqafromcontext",{"0":{"111":1},"2":{"63":1,"160":1}}],["ragconfig",{"2":{"57":1,"59":1,"61":1,"63":2,"156":1,"160":4}}],["ragcontext",{"2":{"5":1}}],["ragquerysimplifier",{"0":{"118":1}}],["ragqueryoptimizer",{"0":{"117":1},"2":{"63":1,"160":3}}],["ragqueryhyde",{"0":{"116":1},"2":{"61":3,"63":1,"160":4}}],["ragdetails",{"2":{"63":1,"160":1}}],["raganswerrefiner",{"0":{"109":1},"2":{"63":2,"160":4}}],["raganswerfromcontext",{"0":{"104":1},"2":{"63":2,"160":4}}],["ragresult",{"2":{"57":3,"62":2,"63":6,"156":1,"160":16}}],["ragtoolsexperimentalext",{"2":{"160":1}}],["ragtools",{"0":{"0":1,"160":1},"1":{"1":1},"2":{"0":3,"9":1,"55":3,"59":1,"62":1,"63":6,"65":2,"156":86,"159":2,"160":175}}],["rag",{"0":{"0":1,"1":1,"55":1,"58":1,"60":1,"103":1},"1":{"1":1,"56":1,"57":1,"58":1,"59":2,"60":2,"61":3,"62":2,"63":1,"104":1},"2":{"0":3,"1":1,"5":1,"6":2,"7":1,"9":2,"55":3,"56":5,"57":3,"60":1,"62":1,"63":8,"100":1,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"159":1,"160":21}}],["r",{"2":{"1":1,"20":2,"50":2,"51":1,"57":1,"101":1,"157":1,"160":2}}],["rt",{"2":{"0":1,"3":3,"5":1,"55":1,"160":4}}],["rewritten",{"2":{"156":2}}],["rejected",{"2":{"156":1}}],["reveal",{"2":{"149":1}}],["revised",{"2":{"117":1,"120":1,"156":2}}],["reviewing",{"2":{"156":2}}],["review",{"2":{"3":1,"7":1,"48":1,"120":1,"121":1,"149":1}}],["reuse",{"2":{"85":1}}],["recall",{"2":{"120":1,"147":1,"148":2}}],["recognizes",{"2":{"99":1}}],["recommended",{"2":{"156":2}}],["recommendation",{"2":{"66":1,"156":1}}],["recommend",{"2":{"66":2,"86":1,"156":2}}],["recorded",{"2":{"63":1,"156":1,"160":1}}],["recursive=true",{"2":{"72":1}}],["recursively",{"2":{"66":1,"156":1}}],["recursivecharactertextsplitter",{"0":{"86":1},"2":{"66":3,"86":1,"156":3}}],["recursive",{"2":{"65":2,"66":8,"86":3,"120":1,"156":11,"160":1}}],["recent",{"2":{"156":2,"157":2}}],["recently",{"2":{"73":1}}],["receive",{"2":{"73":1,"74":1}}],["received",{"2":{"9":1,"51":1,"84":2,"100":1,"156":1,"157":1}}],["reception",{"2":{"46":1}}],["requested",{"2":{"120":1,"147":1,"148":1,"152":1,"160":2}}],["request",{"2":{"73":2,"75":1,"83":2,"84":1,"94":1,"97":1,"99":1,"101":1,"102":3,"120":3,"121":2,"156":10}}],["requests",{"2":{"54":1,"73":7,"156":1,"158":2}}],["requirements",{"2":{"129":1,"131":1,"156":1}}],["requirement",{"2":{"102":2}}],["required",{"2":{"55":1,"101":2,"102":7,"134":1,"156":6,"157":1,"160":5}}],["requires",{"2":{"28":1,"29":1,"30":1,"63":2,"64":1,"87":1,"115":1,"156":5,"160":7}}],["require",{"2":{"14":1,"111":1,"156":1}}],["removing",{"2":{"157":2}}],["removes",{"2":{"156":6,"157":1}}],["remove",{"2":{"51":5,"66":1,"156":16,"157":4}}],["reminder",{"2":{"147":1,"148":1}}],["remaining",{"2":{"73":2}}],["remembers",{"2":{"156":1}}],["remembered",{"2":{"156":1}}],["remember",{"2":{"1":1,"9":1,"23":1,"34":1,"48":1,"72":1,"85":1,"97":2,"100":1,"101":1,"156":4}}],["reducing",{"2":{"63":1,"160":2}}],["reduce",{"2":{"63":1,"160":2}}],["red",{"2":{"20":1,"50":1,"51":2,"57":1,"157":2}}],["real",{"2":{"157":5,"160":5}}],["really",{"2":{"18":1,"156":2}}],["react",{"2":{"156":1}}],["reach",{"2":{"96":1,"157":1}}],["reaching",{"2":{"66":1,"156":1}}],["reasonable",{"2":{"160":1}}],["reasonably",{"2":{"160":1}}],["reason",{"2":{"156":8}}],["reasoning",{"2":{"149":1}}],["reader=",{"2":{"160":1}}],["reads",{"2":{"160":2}}],["readme",{"2":{"156":1}}],["ready",{"2":{"10":1,"81":1,"156":1,"159":1}}],["readtimeout",{"2":{"9":1,"100":1,"156":9}}],["read",{"2":{"3":1,"10":1,"23":1,"57":1,"102":5,"106":1,"113":1,"120":1,"121":1,"136":1,"138":1,"160":1}}],["regarding",{"2":{"129":1,"131":1}}],["regards",{"2":{"85":2,"156":2}}],["regardless",{"2":{"6":2}}],["region",{"2":{"31":1,"156":2}}],["regions",{"2":{"31":1,"156":3}}],["registration",{"2":{"28":1}}],["registry",{"2":{"22":1,"25":1,"36":2,"38":1,"41":1,"101":1,"156":29}}],["registers",{"2":{"156":1}}],["registering",{"2":{"156":3}}],["register",{"2":{"26":2,"27":2,"28":2,"29":2,"30":2,"41":1,"82":2,"89":1,"99":1,"156":13,"160":1}}],["registered",{"2":{"22":1,"25":1,"29":1,"30":1,"85":1,"156":1}}],["regenerate",{"2":{"20":1,"50":1}}],["regex",{"2":{"18":1,"23":1,"63":1,"156":2,"160":3}}],["renders",{"2":{"156":3}}],["rendered",{"2":{"101":7,"156":2}}],["render",{"2":{"12":1,"23":2,"94":2,"96":1,"101":6,"156":18}}],["rendering",{"2":{"12":1,"96":1,"156":5}}],["refresh",{"2":{"120":1,"156":3}}],["ref",{"2":{"66":1,"156":1}}],["refining",{"2":{"63":2,"160":4}}],["refines",{"2":{"157":1,"160":1}}],["refined",{"2":{"109":3,"160":1}}],["refinements",{"2":{"157":1}}],["refinement",{"0":{"108":1},"1":{"109":1},"2":{"160":2}}],["refiner",{"2":{"63":13,"160":19}}],["refine",{"2":{"60":1,"62":2,"63":4,"109":6,"117":1,"156":2,"157":1,"160":13}}],["reflection",{"2":{"120":1,"121":1}}],["reflecting",{"2":{"66":1,"120":1,"137":1,"156":1}}],["reflects",{"2":{"113":1}}],["reflect",{"2":{"11":1}}],["refer",{"2":{"120":1,"137":1,"156":4}}],["refers",{"2":{"9":1,"27":1,"48":1,"100":1,"156":1,"157":1,"160":1}}],["referred",{"2":{"6":1,"28":1}}],["references",{"0":{"51":1,"54":1,"63":1,"66":1},"2":{"51":1,"59":1,"157":1,"160":2}}],["reference",{"0":{"156":1,"157":1,"158":1,"159":1,"160":1},"2":{"3":1,"9":1,"78":1,"86":1,"111":3,"156":4,"160":1}}],["repetition",{"2":{"111":1}}],["repeats",{"2":{"120":2}}],["repeat",{"2":{"20":1,"50":1,"120":1,"136":1,"137":1,"143":1}}],["repeated",{"2":{"20":1,"50":1,"120":1}}],["repeatedly",{"2":{"9":1,"48":1,"100":1,"157":1}}],["report",{"2":{"156":1}}],["reports",{"2":{"63":1,"160":1}}],["reported",{"2":{"35":1,"156":1}}],["rephrasing",{"2":{"63":6,"117":1,"160":18}}],["rephrases",{"2":{"117":1,"118":1,"160":2}}],["rephraser",{"2":{"61":7,"63":11,"160":23}}],["rephrased",{"2":{"57":1,"62":1,"63":2,"118":1,"160":9}}],["rephrase",{"2":{"7":1,"60":1,"61":2,"62":2,"63":1,"115":1,"116":1,"117":2,"118":1,"156":3,"160":13}}],["represented",{"2":{"156":2,"160":3}}],["represents",{"2":{"138":1,"156":3}}],["representing",{"2":{"51":2,"63":3,"66":1,"112":1,"156":72,"157":1,"160":3}}],["representative",{"2":{"6":1}}],["reply",{"2":{"51":1,"83":1,"91":1,"156":3,"157":1}}],["repl",{"2":{"23":2,"57":1,"72":3,"79":1,"92":1,"156":1}}],["replaces",{"2":{"160":1}}],["replaced",{"2":{"66":1,"101":1,"156":3,"160":1}}],["replacements",{"2":{"156":6}}],["replacement",{"2":{"66":4,"101":1,"156":8}}],["replace",{"2":{"11":1,"63":1,"65":2,"66":3,"94":1,"99":2,"101":3,"156":5,"160":2}}],["reload",{"2":{"85":3,"156":2}}],["relentless",{"2":{"66":1,"156":1}}],["releases",{"2":{"159":1}}],["release",{"2":{"34":1}}],["relevancy",{"2":{"156":1}}],["relevance",{"2":{"5":1,"112":2,"160":3}}],["relevant",{"2":{"1":1,"56":1,"57":1,"59":1,"60":2,"62":3,"63":7,"90":1,"106":1,"112":2,"113":1,"117":1,"118":1,"134":1,"136":1,"137":1,"156":1,"157":2,"160":8}}],["related",{"2":{"12":1,"111":1,"138":1,"156":2,"160":2}}],["relational",{"2":{"6":1}}],["resized",{"2":{"156":4}}],["resize",{"2":{"156":5}}],["reserved",{"2":{"147":1,"148":1,"156":3}}],["reset",{"2":{"73":1,"156":1,"157":2}}],["resets",{"2":{"73":1}}],["researcher",{"2":{"138":1}}],["research",{"2":{"57":1,"68":1,"106":1}}],["res",{"2":{"63":1,"160":1}}],["resolutions",{"2":{"156":1}}],["resolution",{"2":{"156":1}}],["resolved",{"2":{"156":1}}],["resolves",{"2":{"120":1}}],["resolve",{"2":{"63":1,"160":2}}],["resources",{"2":{"22":1,"25":1,"57":1,"70":1,"71":1,"74":1,"75":1,"76":2,"89":2}}],["resp",{"2":{"156":2}}],["respectively",{"2":{"51":1,"59":1,"66":1,"156":1,"157":2}}],["respond",{"2":{"9":1,"51":2,"83":1,"100":1,"117":1,"126":2,"157":3}}],["responses",{"0":{"84":1},"2":{"13":1,"23":1,"84":1,"101":1,"138":10,"140":2,"156":5,"157":2}}],["response",{"2":{"6":1,"9":5,"48":1,"59":1,"62":5,"63":5,"73":1,"75":1,"83":4,"84":14,"91":1,"98":2,"100":5,"101":5,"102":8,"112":1,"120":1,"146":1,"156":50,"157":1,"158":1,"160":6}}],["restrictive",{"2":{"147":1,"148":1}}],["restricted",{"2":{"5":1,"6":1}}],["restart",{"2":{"41":1,"51":1,"157":1}}],["rest",{"2":{"18":1,"54":1,"90":1,"97":1,"157":1,"158":1}}],["resulting",{"2":{"160":4}}],["results",{"2":{"6":10,"12":1,"18":1,"51":2,"54":8,"57":1,"62":1,"115":1,"116":1,"117":2,"118":1,"121":2,"122":1,"156":1,"157":6,"158":8,"160":7}}],["result",{"2":{"1":1,"6":1,"51":2,"56":1,"57":7,"60":1,"61":2,"63":19,"100":2,"102":8,"156":25,"157":9,"160":48}}],["retain",{"2":{"156":2,"157":1}}],["retrive",{"2":{"59":1}}],["retries=3",{"2":{"102":1}}],["retries=2",{"2":{"84":1}}],["retries`",{"2":{"51":1,"157":1}}],["retries",{"2":{"20":6,"50":3,"51":22,"102":2,"156":9,"157":26}}],["retrieving",{"2":{"117":1,"160":1}}],["retrievable",{"2":{"63":1,"160":1}}],["retrieval",{"0":{"0":1},"1":{"1":1},"2":{"0":1,"5":3,"6":5,"55":1,"59":2,"62":2,"63":11,"118":1,"156":2,"159":1,"160":22}}],["retrieves",{"2":{"63":1,"160":1}}],["retriever",{"2":{"60":1,"61":6,"63":33,"160":33}}],["retrieved",{"2":{"56":1,"59":1,"62":1,"63":1,"160":1}}],["retrieve",{"2":{"9":2,"56":2,"57":4,"59":2,"60":3,"61":1,"62":1,"63":14,"99":1,"156":1,"160":19}}],["retrying",{"2":{"20":1,"48":2,"51":7,"84":2,"102":1,"157":7}}],["retry",{"2":{"20":3,"48":1,"50":3,"51":15,"102":6,"156":9,"157":22}}],["retryconfig",{"2":{"20":3,"48":1,"50":3,"51":10,"156":1,"157":14}}],["returning",{"2":{"156":1,"160":1}}],["returned",{"2":{"91":1,"102":2,"156":12,"157":1,"160":1}}],["returns",{"2":{"6":7,"9":5,"17":3,"51":11,"63":11,"65":1,"66":5,"72":1,"100":5,"101":2,"156":61,"157":16,"158":1,"160":33}}],["return",{"2":{"1":1,"5":2,"6":1,"9":4,"17":1,"18":4,"23":1,"30":1,"51":6,"54":1,"57":3,"59":2,"63":9,"66":1,"75":1,"83":5,"84":5,"100":5,"102":11,"109":2,"156":60,"157":13,"158":1,"160":19}}],["reranking",{"2":{"62":1,"63":8,"160":13}}],["reranker",{"2":{"59":2,"63":8,"160":13}}],["reranked",{"2":{"57":1,"160":5}}],["rerank",{"2":{"1":2,"7":2,"59":3,"60":1,"62":2,"63":5,"156":1,"160":18}}],["re",{"2":{"0":1,"1":3,"6":1,"7":1,"9":1,"11":1,"19":1,"21":2,"22":1,"23":11,"25":1,"28":1,"30":1,"34":1,"40":1,"41":1,"48":1,"57":1,"59":2,"61":1,"63":1,"66":2,"72":1,"81":2,"94":1,"98":1,"99":4,"100":1,"101":5,"102":3,"106":1,"112":1,"113":1,"115":1,"121":1,"141":2,"144":2,"149":1,"151":2,"156":12,"157":1,"160":6}}],["aaazzam",{"2":{"156":1}}],["aai",{"2":{"10":2,"92":3,"156":11}}],["axolotl",{"2":{"87":1}}],["a>",{"2":{"66":1,"156":5,"160":1}}],["away",{"2":{"51":1,"66":1,"72":1,"156":1,"157":1}}],["awareness",{"2":{"63":1,"160":2}}],["aware",{"2":{"48":1,"56":1,"65":1}}],["achievable",{"2":{"66":1,"156":1}}],["achieve",{"2":{"33":1,"66":3,"79":1,"84":1,"156":3,"157":1}}],["across",{"2":{"51":1,"61":2,"63":2,"77":1,"102":1,"156":2,"157":2,"160":2}}],["action",{"2":{"137":1,"157":2}}],["actionable",{"2":{"134":1,"137":1}}],["active",{"2":{"51":13,"157":17}}],["act",{"2":{"83":1,"138":1,"156":1}}],["actually",{"2":{"27":1,"35":2,"120":1,"121":1,"149":1,"156":1}}],["actual",{"2":{"23":2,"51":1,"120":1,"121":1,"122":1,"149":1,"157":2}}],["accumulate",{"2":{"160":1}}],["accuracy",{"2":{"57":1,"129":1,"131":1,"160":3}}],["accurately",{"2":{"134":1,"155":1}}],["accurate",{"2":{"22":1,"25":1,"156":1}}],["account",{"2":{"71":2,"73":1,"74":1,"89":1}}],["according",{"2":{"66":1,"156":2}}],["accepts",{"2":{"51":3,"102":1,"157":4}}],["accept",{"2":{"51":3,"157":4}}],["accesses",{"2":{"156":1}}],["accessed",{"2":{"85":1,"156":1}}],["accessible",{"2":{"156":1}}],["accessing",{"2":{"71":1,"156":8}}],["accessor",{"2":{"51":4,"156":2,"157":2}}],["access",{"0":{"69":1,"79":1},"2":{"20":1,"27":1,"30":1,"44":1,"47":1,"48":1,"50":1,"51":3,"55":1,"72":1,"79":2,"81":1,"84":1,"94":1,"95":1,"104":1,"109":1,"156":19,"157":5,"160":1}}],["affection",{"2":{"156":1}}],["affects",{"2":{"7":1}}],["after",{"2":{"23":1,"51":5,"63":2,"66":3,"73":1,"84":1,"85":1,"91":1,"136":1,"137":1,"146":1,"153":1,"156":9,"157":3,"160":4}}],["amount",{"2":{"156":1}}],["among",{"2":{"149":1}}],["amazing",{"2":{"96":1}}],["amazingly",{"2":{"21":1}}],["am",{"2":{"30":1,"40":1,"102":1,"156":2}}],["amp",{"0":{"3":1,"4":1,"5":1},"2":{"1":1,"2":1,"5":1,"6":1,"63":4,"160":4}}],["ah",{"2":{"11":1,"156":1}}],["adhere",{"2":{"156":1}}],["adherence",{"2":{"129":1,"131":1}}],["adapted",{"2":{"109":1,"116":1,"117":1,"118":1}}],["adapt",{"2":{"87":1}}],["advisable",{"2":{"160":1}}],["advice",{"2":{"36":1,"66":2,"156":2}}],["advantages",{"2":{"57":1}}],["advancements",{"2":{"57":1}}],["advance",{"2":{"51":1,"157":1}}],["advancedgenerator",{"2":{"63":1,"156":1,"160":3}}],["advancedretriever",{"2":{"61":3,"63":4,"156":1,"160":6}}],["advanced",{"0":{"11":1,"34":1,"40":1},"2":{"47":1,"54":2,"63":3,"93":1,"106":1,"156":1,"158":2,"160":4}}],["adjectives",{"2":{"30":2,"102":8}}],["adjusts",{"2":{"38":1,"40":1}}],["adjust",{"2":{"23":1,"48":1,"149":1}}],["address",{"2":{"120":1,"149":1,"156":4}}],["addressed",{"2":{"120":1}}],["addded",{"2":{"63":1,"160":1}}],["adding",{"2":{"63":1,"65":1,"102":1,"137":1,"156":1,"160":1}}],["additional",{"2":{"51":1,"53":1,"59":1,"63":11,"91":1,"109":1,"136":1,"156":29,"157":4,"160":17}}],["addition",{"2":{"9":2,"17":1,"30":1,"59":1,"63":1,"100":1,"146":4,"153":4,"160":1}}],["added",{"2":{"23":1,"36":1,"51":1,"63":1,"66":2,"156":4,"157":1,"160":4}}],["adds",{"2":{"10":1,"156":1,"157":1,"160":2}}],["add",{"2":{"7":3,"10":1,"12":2,"19":1,"23":7,"31":1,"38":1,"41":1,"46":1,"50":1,"51":3,"63":9,"76":1,"79":2,"85":2,"90":1,"102":2,"136":1,"156":15,"157":9,"160":32}}],["agreements",{"2":{"143":1}}],["agreed",{"2":{"137":2}}],["agents",{"2":{"47":1}}],["agentic",{"2":{"47":1,"51":1,"157":2,"159":1}}],["agent",{"0":{"20":1,"47":1},"1":{"48":1,"49":1,"50":1,"51":1},"2":{"20":1,"51":1,"157":2}}],["agenttools",{"0":{"157":1},"2":{"9":3,"20":1,"47":3,"51":6,"65":1,"84":1,"100":2,"156":35,"157":69,"159":2}}],["age",{"2":{"18":2,"156":8}}],["against",{"2":{"57":1,"63":1,"156":1,"160":1}}],["again",{"2":{"10":1,"85":1,"156":2}}],["authorization",{"2":{"156":2}}],["authentication",{"2":{"156":3}}],["auth",{"2":{"156":2}}],["auto",{"2":{"51":5,"156":11,"157":1}}],["automatically",{"2":{"16":1,"23":1,"48":2,"51":2,"56":1,"57":1,"62":1,"72":1,"76":1,"102":3,"156":10}}],["automatic",{"0":{"50":1},"2":{"9":1,"48":1,"51":1,"100":1,"102":1,"156":1}}],["audience",{"2":{"149":2}}],["augmented",{"0":{"0":1},"1":{"1":1},"2":{"0":1,"55":1,"63":2,"159":1,"160":3}}],["ab",{"2":{"66":1,"156":2}}],["abcabc",{"2":{"66":1,"156":1}}],["abc",{"2":{"51":3,"66":7,"100":2,"156":9,"157":3}}],["ability",{"2":{"30":1,"109":1}}],["abilities",{"2":{"22":1,"25":1}}],["about",{"2":{"12":2,"23":1,"36":1,"48":1,"63":1,"84":1,"98":1,"101":1,"144":1,"146":1,"147":1,"148":1,"151":1,"153":1,"156":12,"160":2}}],["above",{"2":{"9":1,"12":2,"14":1,"16":1,"27":1,"56":1,"63":1,"65":1,"75":1,"100":2,"101":1,"112":1,"120":1,"121":1,"122":1,"136":1,"143":1,"147":1,"148":1,"149":1,"156":3,"157":1,"160":1}}],["abs",{"2":{"51":1,"157":1}}],["absence",{"2":{"11":1}}],["abstractfloat",{"2":{"160":1}}],["abstractdocumentindex",{"2":{"160":2}}],["abstractgenerationmethod",{"2":{"160":1}}],["abstractgenerator",{"2":{"60":3,"63":4,"156":1,"160":8}}],["abstractgoogleschema",{"2":{"156":2}}],["abstractoutcomes",{"2":{"157":1}}],["abstractollamaschema",{"2":{"156":3}}],["abstractollamamanagedschema",{"2":{"156":6}}],["abstractopenaischema",{"2":{"95":1,"96":1,"156":11}}],["abstractmatrix",{"2":{"160":3}}],["abstractmanagedschema",{"2":{"156":2}}],["abstractmultiindex",{"2":{"156":1,"160":2}}],["abstractmessage",{"2":{"51":3,"57":1,"83":1,"84":1,"97":1,"98":1,"156":88,"157":7,"160":1}}],["abstract",{"2":{"62":1,"63":2,"84":1,"102":2,"147":1,"148":1,"160":8}}],["abstractpostprocessor",{"2":{"60":1,"63":2,"160":4}}],["abstractpromptschema",{"2":{"51":1,"94":1,"95":1,"96":2,"156":5,"157":1}}],["abstractannotationstyler",{"2":{"160":4}}],["abstractannotatednode",{"2":{"160":5}}],["abstractanthropicschema",{"2":{"156":5}}],["abstractanswerer",{"2":{"60":1,"63":2,"160":4}}],["abstractarray",{"2":{"9":1,"100":1}}],["abstractscoringmethod",{"2":{"157":4}}],["abstractsharegptschema",{"2":{"156":1}}],["abstractsimilarityfinder",{"2":{"60":1,"63":1,"160":6}}],["abstractstring=",{"2":{"51":4,"66":2,"156":8,"160":1}}],["abstractstring",{"2":{"9":2,"51":6,"54":8,"60":3,"63":12,"66":13,"100":2,"156":93,"157":4,"158":10,"160":56}}],["abstracttagfilter",{"2":{"60":1,"63":1,"160":6}}],["abstracttagger",{"2":{"60":2,"63":3,"160":10}}],["abstracttrees",{"2":{"51":1,"157":1,"160":1}}],["abstractembedder",{"2":{"60":2,"63":2,"160":6}}],["abstractragconfig",{"2":{"60":3,"63":2,"160":3}}],["abstractragresult",{"2":{"59":2,"60":4,"62":1,"63":4,"160":12}}],["abstractretrievalmethod",{"2":{"160":1}}],["abstractretriever",{"2":{"59":2,"60":2,"63":4,"156":1,"160":8}}],["abstractrefiner",{"2":{"60":1,"63":4,"160":7}}],["abstractrephraser",{"2":{"60":1,"63":2,"160":8}}],["abstractreranker",{"2":{"59":1,"60":2,"63":2,"160":7}}],["abstractindexbuilder",{"2":{"60":2,"63":2,"156":1,"160":6}}],["abstractindex",{"2":{"59":1}}],["abstractcodeblock",{"2":{"156":1}}],["abstractcodeoutcome",{"2":{"51":2,"157":2}}],["abstractcontextbuilder",{"2":{"60":1,"63":2,"160":4}}],["abstractchar",{"2":{"66":1,"156":1}}],["abstractchatmessage",{"2":{"23":2,"85":1,"101":1,"156":2}}],["abstractchunker",{"2":{"60":1,"63":1,"160":6}}],["abstractchunkindex",{"2":{"60":4,"63":5,"156":1,"160":21}}],["abstractcandidatechunks",{"2":{"59":1,"156":1,"160":3}}],["abstractvector",{"2":{"51":2,"54":2,"63":5,"66":1,"156":39,"157":5,"158":2,"160":22}}],["able",{"2":{"1":1,"157":1}}],["avg",{"2":{"6":2}}],["average",{"2":{"6":1,"63":1,"112":1,"160":3}}],["available",{"2":{"5":1,"9":1,"12":2,"22":2,"23":4,"25":2,"31":2,"60":1,"62":2,"63":4,"72":1,"81":2,"99":1,"100":1,"102":1,"126":1,"136":1,"156":20,"160":7}}],["avoiding",{"2":{"57":1,"111":1}}],["avoided",{"2":{"10":1}}],["avoid",{"2":{"1":1,"41":1,"51":2,"63":1,"66":1,"73":1,"92":1,"120":1,"121":1,"147":1,"148":1,"156":3,"160":2}}],["arxiv",{"2":{"160":1}}],["arr",{"2":{"85":2,"156":2}}],["arrays",{"2":{"147":1,"148":1,"156":1}}],["array",{"2":{"12":1,"21":1,"23":1,"44":1,"57":3,"66":3,"85":1,"156":8,"160":4}}],["arbitrary",{"2":{"84":1,"156":3}}],["art",{"2":{"68":1}}],["artificial",{"2":{"15":1}}],["articles",{"2":{"57":1,"156":1}}],["article",{"2":{"0":1}}],["arches",{"2":{"62":1}}],["arg2",{"2":{"156":1}}],["arg1",{"2":{"156":1}}],["argmin",{"2":{"66":2,"156":2}}],["argmax",{"2":{"66":1,"156":1}}],["args",{"2":{"51":5,"156":1,"157":11}}],["argumenterror",{"0":{"72":2}}],["arguments",{"0":{"61":1,"78":1},"2":{"9":4,"11":1,"18":1,"20":1,"48":2,"51":10,"54":1,"59":2,"61":2,"63":16,"66":5,"92":1,"100":4,"102":2,"129":1,"131":1,"133":1,"156":77,"157":27,"158":1,"160":32}}],["argument",{"2":{"5":1,"6":8,"9":2,"14":1,"20":2,"22":1,"23":1,"25":1,"38":1,"44":1,"50":2,"51":4,"57":2,"59":2,"66":1,"73":1,"83":2,"85":1,"100":2,"129":1,"131":1,"156":11,"157":5}}],["around",{"2":{"9":1,"59":1,"63":1,"66":1,"92":1,"96":1,"100":1,"149":1,"156":4,"157":1,"160":5}}],["areas",{"2":{"120":1}}],["are",{"2":{"4":1,"5":1,"6":8,"9":1,"10":1,"11":1,"12":3,"16":1,"18":1,"20":1,"22":5,"23":12,"25":2,"26":3,"35":2,"36":1,"40":1,"48":3,"50":1,"51":12,"56":3,"57":4,"59":4,"60":1,"62":2,"63":11,"65":4,"66":6,"68":1,"69":1,"81":1,"82":1,"84":2,"85":1,"94":2,"96":2,"97":2,"98":2,"99":2,"100":2,"101":4,"102":2,"111":1,"116":1,"117":1,"118":1,"120":4,"126":1,"127":1,"129":1,"131":1,"134":1,"136":1,"137":2,"140":1,"141":1,"142":2,"144":1,"145":1,"146":3,"147":1,"148":1,"149":4,"151":1,"152":1,"153":3,"155":1,"156":63,"157":14,"160":20}}],["aspects",{"2":{"146":1,"153":1}}],["as=",{"2":{"85":2,"99":1,"156":1}}],["assesses",{"2":{"160":2}}],["assertion",{"2":{"156":1}}],["assertions",{"2":{"20":1,"51":1,"157":1}}],["assert",{"2":{"51":1,"102":2,"157":1}}],["assigning",{"2":{"149":1}}],["assign",{"2":{"106":1,"112":1}}],["assistance",{"2":{"22":1,"25":1,"41":1}}],["assistant",{"2":{"11":1,"33":1,"83":1,"99":1,"101":2,"104":1,"109":1,"112":2,"113":1,"115":1,"118":1,"134":1,"141":2,"142":1,"156":5}}],["assistantask",{"0":{"141":1},"2":{"9":1,"99":4,"100":1,"101":3}}],["assist",{"2":{"21":1,"22":1,"25":1,"29":2,"30":1,"33":1,"83":1,"156":10}}],["associated",{"2":{"82":1,"101":1,"137":1,"156":2}}],["assuming",{"2":{"63":1,"75":1,"156":1,"160":3}}],["assumed",{"2":{"156":1,"157":1,"160":1}}],["assumes",{"2":{"36":1,"63":1,"156":5,"160":4}}],["assume",{"2":{"21":1,"73":1,"90":1,"157":1}}],["asterisk",{"2":{"10":1}}],["asynchronous",{"0":{"13":1},"2":{"92":1,"156":3}}],["asyncmap",{"2":{"6":1,"13":3,"45":2,"63":1,"73":3,"92":1,"160":2}}],["async",{"2":{"6":1,"45":1,"156":1}}],["as",{"2":{"1":1,"5":1,"6":4,"9":9,"10":1,"14":1,"15":1,"16":1,"17":1,"19":1,"20":2,"22":5,"23":5,"25":2,"26":2,"28":3,"29":2,"30":4,"31":1,"33":1,"38":1,"41":2,"42":2,"46":1,"47":1,"48":2,"50":3,"51":13,"52":1,"53":1,"55":1,"57":2,"59":3,"62":1,"63":1,"66":15,"73":1,"76":3,"83":4,"84":4,"85":5,"86":1,"89":1,"92":2,"95":1,"98":1,"99":2,"100":7,"101":2,"102":12,"104":1,"106":1,"109":2,"112":1,"115":2,"116":2,"120":3,"129":3,"131":3,"136":6,"137":4,"138":3,"143":4,"146":3,"149":1,"153":3,"155":2,"156":99,"157":28,"160":14}}],["asks",{"2":{"122":1,"143":1}}],["ask=",{"2":{"51":1,"100":2,"101":2,"157":1}}],["asked",{"0":{"67":1},"1":{"68":1,"69":1,"70":1,"71":1,"72":1,"73":1,"74":1,"75":1,"76":1,"77":1,"78":1,"79":1,"80":1,"81":1,"82":1,"83":1,"84":1,"85":1,"86":1,"87":1},"2":{"36":1}}],["asking",{"2":{"12":1,"23":2,"141":1,"144":1,"151":1,"156":2}}],["ask",{"2":{"1":1,"12":4,"20":2,"22":1,"23":8,"25":1,"30":1,"33":1,"41":1,"50":2,"51":2,"56":1,"65":1,"99":3,"101":7,"121":1,"141":3,"144":3,"151":3,"156":6,"157":2}}],["april",{"2":{"14":1}}],["apos",{"2":{"6":12}}],["append",{"2":{"157":1}}],["appends",{"2":{"156":3}}],["appended",{"2":{"51":1,"156":2}}],["approximates",{"2":{"157":2}}],["appropriate",{"2":{"126":1,"134":1,"156":1}}],["approach>",{"2":{"152":4}}],["approach",{"2":{"51":1,"100":1,"115":1,"145":4,"147":2,"148":1,"152":4,"157":1}}],["appreciate",{"2":{"11":1}}],["applying",{"2":{"157":1}}],["apply",{"2":{"63":1,"66":1,"156":3,"160":1}}],["apples",{"2":{"156":2}}],["apple",{"2":{"30":2,"36":1,"102":8,"156":1}}],["applicable",{"2":{"63":1,"138":1,"160":2}}],["applications",{"2":{"9":1,"55":3,"56":1,"57":1,"63":1,"104":1,"106":1,"107":1,"109":1,"111":1,"112":1,"113":1,"115":1,"116":1,"117":1,"118":1,"146":1,"153":1,"160":1}}],["application",{"2":{"1":1,"57":1,"94":1,"95":1}}],["applied",{"2":{"9":1,"48":1,"51":1,"56":1,"66":1,"100":1,"156":12,"157":1,"160":1}}],["applies",{"2":{"1":1,"62":1,"66":1,"156":1,"157":2}}],["app",{"2":{"3":1,"81":1,"146":1,"153":1}}],["apikey",{"0":{"72":1}}],["apitools",{"0":{"52":1,"158":1},"1":{"53":1,"54":1},"2":{"52":2,"54":1,"156":2,"158":4,"159":1}}],["apis",{"0":{"22":1,"24":1,"26":1},"1":{"25":1,"26":1,"27":1,"28":1,"29":1,"30":1},"2":{"22":1,"35":1,"52":1,"68":1,"69":1,"75":1,"94":1,"97":1,"159":1}}],["api",{"0":{"22":1,"71":1,"72":2,"76":1,"77":1,"78":2,"95":1},"2":{"1":1,"6":1,"7":1,"9":2,"19":1,"20":3,"22":10,"24":1,"25":4,"26":8,"27":1,"28":5,"29":2,"30":2,"31":4,"36":1,"41":1,"50":3,"51":8,"53":2,"54":5,"55":1,"61":5,"63":33,"70":3,"71":3,"72":5,"74":1,"76":10,"77":4,"78":1,"81":1,"89":8,"94":2,"95":3,"96":2,"100":4,"101":7,"102":7,"156":241,"157":10,"158":11,"159":1,"160":59}}],["aligns",{"2":{"160":2}}],["alignment",{"2":{"129":1,"131":1}}],["aligned",{"2":{"116":1,"117":1}}],["align",{"2":{"112":1,"156":1,"160":2}}],["aliased",{"2":{"30":1}}],["aliases",{"0":{"14":1},"2":{"14":5,"28":1,"36":1,"41":1,"156":33}}],["alias",{"2":{"6":1,"28":1,"29":1,"30":1,"32":1,"41":1,"102":2,"156":31,"160":1}}],["alexander",{"2":{"111":3}}],["along",{"2":{"63":1,"157":1,"160":2}}],["almost",{"2":{"59":1}}],["alternative",{"2":{"156":7}}],["alternatives",{"0":{"80":1},"2":{"69":1}}],["alternatively",{"2":{"23":1,"51":1,"81":1,"89":1,"156":1,"157":2}}],["alter",{"2":{"51":1,"156":1}}],["algorithms",{"2":{"51":1,"57":1,"157":2}}],["already",{"2":{"29":1,"30":1,"36":1,"63":1,"73":1,"81":1,"90":1,"157":1,"160":2}}],["always",{"2":{"22":1,"23":1,"25":1,"27":1,"51":2,"59":1,"65":1,"66":2,"70":1,"71":1,"74":1,"84":1,"85":2,"120":2,"148":1,"156":6,"157":5,"160":1}}],["also",{"2":{"1":1,"5":1,"6":2,"9":1,"11":1,"14":1,"17":2,"20":1,"21":1,"22":1,"23":4,"27":1,"28":2,"29":1,"30":1,"50":1,"51":8,"54":1,"63":7,"66":2,"77":1,"84":1,"85":1,"100":1,"102":2,"120":1,"121":1,"122":1,"156":27,"157":7,"158":1,"160":13}}],["all=false",{"2":{"156":6}}],["all=true`",{"2":{"100":1}}],["all=true",{"2":{"1":1,"57":1,"63":1,"83":4,"84":1,"100":1,"102":1,"156":15,"160":1}}],["allocation",{"2":{"57":1}}],["allowing",{"2":{"55":1,"157":1}}],["allow",{"2":{"48":1,"80":1,"102":1,"156":3}}],["allowed",{"2":{"20":1,"51":1,"102":1,"156":22,"157":2}}],["allows",{"2":{"9":1,"20":1,"21":1,"24":1,"41":1,"48":3,"50":1,"51":2,"63":2,"74":1,"100":1,"156":3,"157":4,"160":2}}],["all",{"2":{"1":1,"5":3,"6":9,"9":3,"10":2,"11":1,"12":1,"14":1,"18":1,"22":1,"23":1,"25":1,"34":1,"48":2,"51":16,"57":1,"59":1,"61":2,"62":1,"63":12,"66":1,"83":1,"84":1,"85":2,"90":1,"93":1,"95":1,"96":1,"99":1,"100":3,"102":1,"106":1,"112":3,"120":2,"121":1,"137":2,"146":1,"147":1,"148":1,"149":2,"153":1,"155":1,"156":50,"157":30,"160":16}}],["anonymous",{"2":{"147":1,"148":1}}],["another",{"2":{"10":1,"51":1,"63":3,"157":2,"160":5}}],["antropic",{"2":{"156":1}}],["anthropicschema",{"2":{"156":4}}],["anthropic",{"2":{"68":1,"69":1,"129":1,"151":1,"152":1,"153":1,"156":16}}],["antibiotics",{"2":{"111":2}}],["anti",{"2":{"6":1}}],["ancient",{"2":{"66":1,"156":1}}],["ancestors",{"2":{"51":1,"157":5}}],["ancestor",{"2":{"51":1,"157":1}}],["annotation",{"2":{"160":3}}],["annotations",{"2":{"63":1,"147":1,"148":1,"160":3}}],["annotating",{"2":{"160":1}}],["annotatednode",{"2":{"156":1,"160":11}}],["annotated",{"2":{"63":6,"160":6}}],["annotates",{"2":{"63":1,"160":1}}],["annotater",{"2":{"63":6,"160":10}}],["annotate",{"2":{"9":1,"56":1,"57":1,"63":8,"156":2,"160":16}}],["animal",{"2":{"17":2,"84":2,"156":7}}],["ans",{"2":{"9":5,"91":1,"100":5}}],["answer=",{"2":{"160":4}}],["answer=answer",{"2":{"160":1}}],["answering",{"2":{"112":1}}],["answered",{"2":{"63":1,"111":1,"137":1,"160":1}}],["answerer",{"2":{"61":1,"63":11,"160":16}}],["answers",{"2":{"2":1,"7":1,"55":1,"56":1,"104":1,"109":1,"111":1,"115":2,"116":1,"136":1,"160":5}}],["answer",{"2":{"1":3,"4":1,"5":4,"6":4,"9":4,"10":2,"16":1,"20":3,"22":1,"23":6,"25":1,"30":1,"50":3,"51":13,"53":1,"54":4,"56":3,"57":7,"59":1,"60":2,"62":1,"63":33,"84":1,"99":2,"100":1,"101":4,"104":4,"109":18,"111":6,"112":14,"113":7,"115":1,"116":1,"127":1,"141":2,"144":2,"151":2,"156":5,"157":13,"158":4,"160":84}}],["analystthemesinresponses",{"0":{"138":1}}],["analystdecisionsintranscript",{"0":{"137":1}}],["analyst",{"2":{"136":1,"137":1}}],["analystchaptersintranscript",{"0":{"136":1}}],["analysis",{"2":{"5":1,"6":1,"16":1,"23":4,"51":1,"57":2,"138":1,"157":1}}],["analyzed",{"2":{"51":1,"138":1,"157":1}}],["analyze",{"2":{"6":1,"111":1,"117":1,"129":1,"131":1,"137":1}}],["anytagfilter",{"2":{"156":1,"160":4}}],["anything",{"2":{"30":1,"33":1,"38":1,"39":1,"41":1,"136":1,"156":1}}],["anymore",{"2":{"82":1}}],["anyone",{"2":{"71":1,"89":1}}],["anywhere",{"0":{"79":1},"2":{"21":1,"79":1,"81":1}}],["anyscale",{"2":{"7":1}}],["any",{"2":{"1":1,"3":1,"5":2,"6":2,"9":2,"10":3,"12":1,"16":1,"17":4,"20":1,"22":2,"23":2,"24":1,"25":1,"27":1,"30":2,"31":1,"33":1,"34":1,"41":1,"48":3,"50":1,"51":13,"55":1,"59":2,"62":1,"63":1,"66":1,"71":1,"72":1,"75":1,"76":1,"79":1,"84":3,"85":3,"87":1,"89":1,"91":1,"96":1,"100":2,"101":1,"102":9,"106":2,"107":1,"111":1,"120":1,"136":2,"137":2,"138":1,"140":1,"145":1,"146":2,"147":1,"148":2,"149":1,"152":1,"153":2,"156":61,"157":10,"160":20}}],["an",{"0":{"72":2,"73":1},"2":{"1":3,"5":1,"6":3,"9":4,"11":1,"16":1,"18":1,"19":1,"20":1,"21":1,"31":1,"40":1,"41":2,"47":1,"48":3,"50":1,"51":13,"52":1,"53":2,"54":3,"55":1,"56":2,"57":4,"59":2,"60":1,"62":2,"63":9,"65":1,"66":5,"71":2,"72":1,"75":1,"76":1,"84":3,"87":2,"89":3,"93":2,"94":1,"95":2,"96":1,"99":1,"100":4,"102":8,"104":1,"109":2,"112":5,"113":1,"115":1,"118":1,"120":1,"121":2,"127":1,"134":1,"138":1,"147":1,"149":2,"156":112,"157":22,"158":3,"160":23}}],["and",{"0":{"19":1,"22":1,"70":1},"2":{"0":4,"1":12,"2":2,"3":2,"5":3,"6":17,"7":5,"9":13,"10":3,"11":3,"12":2,"15":2,"16":3,"17":1,"18":4,"20":9,"21":2,"22":6,"23":15,"25":2,"26":3,"27":2,"28":2,"29":3,"30":3,"31":2,"33":1,"34":2,"35":2,"36":4,"40":2,"41":5,"44":1,"46":1,"47":1,"48":11,"50":9,"51":56,"52":1,"53":2,"55":1,"56":1,"57":11,"59":7,"61":3,"62":5,"63":46,"64":2,"65":6,"66":10,"68":1,"69":1,"70":1,"71":2,"72":2,"73":3,"74":2,"75":2,"76":1,"77":2,"79":1,"80":1,"81":3,"82":2,"83":1,"84":13,"85":6,"86":3,"87":1,"89":3,"90":1,"91":2,"92":2,"93":2,"94":2,"95":1,"96":2,"98":2,"99":5,"100":12,"101":10,"102":27,"104":1,"106":6,"107":1,"109":3,"111":5,"112":3,"113":3,"115":2,"116":1,"117":1,"118":1,"120":10,"121":4,"122":1,"126":2,"129":4,"131":4,"133":1,"134":4,"136":17,"137":16,"138":3,"140":1,"141":2,"142":2,"143":6,"144":2,"145":4,"146":5,"147":2,"148":3,"149":10,"151":2,"152":3,"153":5,"155":2,"156":178,"157":96,"158":1,"159":2,"160":130}}],["atop",{"2":{"156":1}}],["atomic",{"2":{"63":9,"160":22}}],["ate",{"2":{"30":1,"102":3,"156":1}}],["attribute",{"2":{"160":2}}],["attract",{"2":{"117":1}}],["attempted",{"2":{"51":1,"157":3}}],["attempts",{"2":{"48":1,"51":2,"84":1,"157":3}}],["attempt",{"2":{"20":1,"50":1,"51":2,"157":2}}],["attach",{"2":{"156":4}}],["attached",{"2":{"11":1,"34":1,"40":1,"156":2}}],["attachments",{"2":{"11":1}}],["attachment",{"2":{"11":1,"34":1,"156":3}}],["at",{"2":{"0":1,"5":1,"6":1,"20":2,"22":1,"26":1,"30":1,"31":1,"33":1,"35":1,"42":1,"47":1,"50":2,"51":5,"53":1,"59":1,"62":1,"63":4,"68":1,"70":1,"73":1,"82":1,"94":1,"99":1,"101":1,"107":2,"112":2,"138":1,"156":19,"157":12,"160":7}}],["aims",{"2":{"160":2}}],["aimessage>",{"2":{"10":1,"156":1}}],["aimessage",{"2":{"1":1,"9":3,"11":2,"19":2,"21":1,"22":1,"23":1,"25":1,"29":1,"30":1,"33":1,"34":1,"35":1,"38":1,"39":1,"40":1,"41":1,"48":1,"51":5,"57":2,"59":1,"60":2,"83":2,"85":2,"91":2,"92":1,"94":2,"98":1,"100":3,"101":2,"156":70,"157":10}}],["aiagent",{"2":{"157":1}}],["aihelpme",{"2":{"85":1}}],["aitemplatemetadata",{"2":{"12":3,"23":2,"85":2,"156":14}}],["aitemplate",{"2":{"12":3,"23":4,"63":1,"99":2,"101":2,"156":16,"157":4,"160":1}}],["aitemplates",{"0":{"23":1},"2":{"9":4,"12":4,"23":3,"85":1,"99":2,"100":3,"156":22,"160":2}}],["air",{"2":{"66":1,"156":1}}],["airetry",{"0":{"20":1},"2":{"9":1,"20":8,"48":3,"50":8,"51":13,"84":6,"100":1,"102":6,"156":1,"157":14}}],["airag",{"2":{"1":2,"5":1,"6":2,"9":1,"56":1,"57":3,"60":1,"61":1,"62":1,"63":6,"156":1,"160":14}}],["aiclassifier",{"2":{"84":1}}],["aiclassify",{"2":{"9":2,"12":1,"16":1,"17":2,"51":1,"84":4,"100":1,"126":1,"156":16,"157":7}}],["aicodefixer",{"2":{"48":2,"51":4,"120":1,"121":1,"122":1,"156":2,"157":26,"159":1}}],["aicode",{"2":{"48":3,"51":11,"156":13,"157":1}}],["aicallblock",{"2":{"51":4,"157":13}}],["aicall",{"2":{"9":2,"20":9,"48":3,"50":9,"51":49,"100":2,"102":4,"156":2,"157":98}}],["aiimage",{"2":{"9":2,"100":1,"156":4}}],["aiscan",{"0":{"42":1},"2":{"9":2,"19":3,"42":1,"100":1,"156":15,"157":6}}],["aiembed",{"0":{"43":1},"1":{"44":1,"45":1,"46":1},"2":{"9":2,"15":3,"21":3,"22":1,"26":1,"28":1,"29":1,"30":1,"44":2,"45":2,"46":1,"51":1,"63":2,"98":1,"100":2,"156":22,"157":7,"160":3}}],["aiextract",{"0":{"102":1},"2":{"5":1,"9":2,"18":2,"30":2,"51":1,"63":3,"98":1,"100":2,"102":5,"129":1,"131":1,"156":18,"157":7,"160":6}}],["aigenerate",{"0":{"32":1,"37":1,"78":1,"92":1,"101":1},"1":{"33":1,"34":1,"35":1,"38":1,"39":1,"40":1,"41":1},"2":{"9":9,"11":4,"12":3,"13":2,"20":5,"21":2,"22":4,"23":1,"25":2,"26":2,"27":1,"28":2,"29":1,"30":1,"31":1,"33":1,"34":1,"36":1,"38":1,"39":2,"40":1,"41":2,"48":10,"50":5,"51":18,"63":6,"72":1,"73":1,"83":3,"84":6,"92":1,"93":1,"94":1,"100":11,"101":2,"102":11,"156":41,"157":38,"159":1,"160":14}}],["ai",{"0":{"9":1,"29":1,"30":1,"31":1,"36":1,"50":1,"91":1,"100":1},"1":{"32":1,"33":1,"34":1,"35":1,"37":1,"38":1,"39":1,"40":1,"41":1,"42":1,"43":1,"44":1,"45":1,"46":1},"2":{"0":1,"9":12,"10":1,"11":1,"13":1,"14":3,"15":1,"16":1,"20":4,"21":1,"22":2,"23":1,"26":2,"28":1,"29":2,"30":3,"31":2,"33":1,"35":1,"36":2,"48":16,"50":1,"51":20,"55":1,"56":1,"57":1,"63":6,"64":1,"65":2,"66":1,"68":2,"72":1,"79":1,"80":1,"81":1,"83":6,"85":2,"87":1,"91":5,"92":3,"94":2,"96":1,"97":1,"98":3,"99":3,"100":13,"101":3,"102":8,"104":1,"109":2,"111":1,"112":1,"115":1,"127":1,"134":1,"136":1,"137":1,"141":1,"142":1,"149":3,"156":96,"157":51,"160":14}}],["a",{"0":{"0":1,"3":1,"4":1,"5":1,"85":1,"86":1,"87":1},"1":{"1":1},"2":{"0":2,"1":6,"2":3,"3":1,"4":2,"5":9,"6":16,"7":2,"9":14,"10":6,"11":7,"12":7,"14":3,"15":1,"16":4,"17":3,"18":6,"19":5,"20":6,"21":1,"22":3,"23":23,"24":1,"25":3,"27":2,"28":1,"29":3,"30":4,"31":1,"33":2,"34":2,"35":2,"36":1,"38":1,"39":1,"40":5,"41":3,"42":1,"44":1,"46":1,"47":1,"48":12,"50":3,"51":47,"54":2,"55":2,"56":6,"57":8,"59":6,"62":6,"63":40,"64":2,"65":12,"66":35,"72":2,"73":5,"74":4,"75":8,"76":5,"81":3,"83":5,"84":14,"85":11,"87":7,"89":1,"91":2,"94":1,"95":2,"96":1,"97":4,"98":1,"99":8,"100":13,"101":6,"102":24,"104":1,"106":2,"109":1,"111":4,"112":4,"113":4,"115":3,"116":3,"117":1,"118":4,"120":4,"121":1,"122":1,"126":2,"129":2,"131":2,"134":5,"136":9,"137":6,"138":3,"141":1,"142":2,"143":4,"144":1,"145":1,"146":8,"147":2,"148":3,"149":6,"151":1,"152":2,"153":8,"155":1,"156":416,"157":85,"158":2,"160":114}}]],"serializationVersion":2}';
+export {
+ _localSearchIndexroot as default
+};
diff --git a/previews/PR116/assets/chunks/VPLocalSearchBox.D8j79LpL.js b/previews/PR116/assets/chunks/VPLocalSearchBox.D8j79LpL.js
new file mode 100644
index 000000000..445190b31
--- /dev/null
+++ b/previews/PR116/assets/chunks/VPLocalSearchBox.D8j79LpL.js
@@ -0,0 +1,5153 @@
+var __defProp = Object.defineProperty;
+var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
+var __publicField = (obj, key, value) => {
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
+ return value;
+};
+import { Y as __vitePreload, h as ref, y as watch, al as unrefElement, am as tryOnScopeDispose, d as defineComponent, H as shallowRef, an as computedAsync, k as computed, ao as useSessionStorage, ap as useLocalStorage, z as watchEffect, aq as watchDebounced, l as onMounted, U as nextTick, S as onKeyStroke, ar as useRouter, as as useEventListener, Z as useScrollLock, j as inBrowser, at as onBeforeUnmount, o as openBlock, b as createBlock, m as createBaseVNode, a2 as withModifiers, p as unref, au as withDirectives, av as vModelText, aw as isRef, c as createElementBlock, n as normalizeClass, e as createCommentVNode, G as renderList, F as Fragment, a as createTextVNode, t as toDisplayString, ax as Teleport, q as pushScopeId, s as popScopeId, ay as markRaw, az as createApp, ab as dataSymbol, ah as pathToFile, aA as escapeRegExp, _ as _export_sfc } from "./framework.DNj2-0qu.js";
+import { u as useData, c as createSearchTranslate } from "./theme.TGBeONaB.js";
+const localSearchIndex = { "root": () => __vitePreload(() => import("./@localSearchIndexroot.CMA9uIl1.js"), true ? __vite__mapDeps([]) : void 0) };
+/*!
+* tabbable 6.2.0
+* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+*/
+var candidateSelectors = ["input:not([inert])", "select:not([inert])", "textarea:not([inert])", "a[href]:not([inert])", "button:not([inert])", "[tabindex]:not(slot):not([inert])", "audio[controls]:not([inert])", "video[controls]:not([inert])", '[contenteditable]:not([contenteditable="false"]):not([inert])', "details>summary:first-of-type:not([inert])", "details:not([inert])"];
+var candidateSelector = /* @__PURE__ */ candidateSelectors.join(",");
+var NoElement = typeof Element === "undefined";
+var matches = NoElement ? function() {
+} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
+var getRootNode = !NoElement && Element.prototype.getRootNode ? function(element) {
+ var _element$getRootNode;
+ return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element);
+} : function(element) {
+ return element === null || element === void 0 ? void 0 : element.ownerDocument;
+};
+var isInert = function isInert2(node, lookUp) {
+ var _node$getAttribute;
+ if (lookUp === void 0) {
+ lookUp = true;
+ }
+ var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, "inert");
+ var inert = inertAtt === "" || inertAtt === "true";
+ var result = inert || lookUp && node && isInert2(node.parentNode);
+ return result;
+};
+var isContentEditable = function isContentEditable2(node) {
+ var _node$getAttribute2;
+ var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, "contenteditable");
+ return attValue === "" || attValue === "true";
+};
+var getCandidates = function getCandidates2(el, includeContainer, filter) {
+ if (isInert(el)) {
+ return [];
+ }
+ var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));
+ if (includeContainer && matches.call(el, candidateSelector)) {
+ candidates.unshift(el);
+ }
+ candidates = candidates.filter(filter);
+ return candidates;
+};
+var getCandidatesIteratively = function getCandidatesIteratively2(elements, includeContainer, options) {
+ var candidates = [];
+ var elementsToCheck = Array.from(elements);
+ while (elementsToCheck.length) {
+ var element = elementsToCheck.shift();
+ if (isInert(element, false)) {
+ continue;
+ }
+ if (element.tagName === "SLOT") {
+ var assigned = element.assignedElements();
+ var content = assigned.length ? assigned : element.children;
+ var nestedCandidates = getCandidatesIteratively2(content, true, options);
+ if (options.flatten) {
+ candidates.push.apply(candidates, nestedCandidates);
+ } else {
+ candidates.push({
+ scopeParent: element,
+ candidates: nestedCandidates
+ });
+ }
+ } else {
+ var validCandidate = matches.call(element, candidateSelector);
+ if (validCandidate && options.filter(element) && (includeContainer || !elements.includes(element))) {
+ candidates.push(element);
+ }
+ var shadowRoot = element.shadowRoot || // check for an undisclosed shadow
+ typeof options.getShadowRoot === "function" && options.getShadowRoot(element);
+ var validShadowRoot = !isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element));
+ if (shadowRoot && validShadowRoot) {
+ var _nestedCandidates = getCandidatesIteratively2(shadowRoot === true ? element.children : shadowRoot.children, true, options);
+ if (options.flatten) {
+ candidates.push.apply(candidates, _nestedCandidates);
+ } else {
+ candidates.push({
+ scopeParent: element,
+ candidates: _nestedCandidates
+ });
+ }
+ } else {
+ elementsToCheck.unshift.apply(elementsToCheck, element.children);
+ }
+ }
+ }
+ return candidates;
+};
+var hasTabIndex = function hasTabIndex2(node) {
+ return !isNaN(parseInt(node.getAttribute("tabindex"), 10));
+};
+var getTabIndex = function getTabIndex2(node) {
+ if (!node) {
+ throw new Error("No node provided");
+ }
+ if (node.tabIndex < 0) {
+ if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) {
+ return 0;
+ }
+ }
+ return node.tabIndex;
+};
+var getSortOrderTabIndex = function getSortOrderTabIndex2(node, isScope) {
+ var tabIndex = getTabIndex(node);
+ if (tabIndex < 0 && isScope && !hasTabIndex(node)) {
+ return 0;
+ }
+ return tabIndex;
+};
+var sortOrderedTabbables = function sortOrderedTabbables2(a, b) {
+ return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
+};
+var isInput = function isInput2(node) {
+ return node.tagName === "INPUT";
+};
+var isHiddenInput = function isHiddenInput2(node) {
+ return isInput(node) && node.type === "hidden";
+};
+var isDetailsWithSummary = function isDetailsWithSummary2(node) {
+ var r = node.tagName === "DETAILS" && Array.prototype.slice.apply(node.children).some(function(child) {
+ return child.tagName === "SUMMARY";
+ });
+ return r;
+};
+var getCheckedRadio = function getCheckedRadio2(nodes, form) {
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i].checked && nodes[i].form === form) {
+ return nodes[i];
+ }
+ }
+};
+var isTabbableRadio = function isTabbableRadio2(node) {
+ if (!node.name) {
+ return true;
+ }
+ var radioScope = node.form || getRootNode(node);
+ var queryRadios = function queryRadios2(name) {
+ return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]');
+ };
+ var radioSet;
+ if (typeof window !== "undefined" && typeof window.CSS !== "undefined" && typeof window.CSS.escape === "function") {
+ radioSet = queryRadios(window.CSS.escape(node.name));
+ } else {
+ try {
+ radioSet = queryRadios(node.name);
+ } catch (err) {
+ console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s", err.message);
+ return false;
+ }
+ }
+ var checked = getCheckedRadio(radioSet, node.form);
+ return !checked || checked === node;
+};
+var isRadio = function isRadio2(node) {
+ return isInput(node) && node.type === "radio";
+};
+var isNonTabbableRadio = function isNonTabbableRadio2(node) {
+ return isRadio(node) && !isTabbableRadio(node);
+};
+var isNodeAttached = function isNodeAttached2(node) {
+ var _nodeRoot;
+ var nodeRoot = node && getRootNode(node);
+ var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host;
+ var attached = false;
+ if (nodeRoot && nodeRoot !== node) {
+ var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument;
+ attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node));
+ while (!attached && nodeRootHost) {
+ var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD;
+ nodeRoot = getRootNode(nodeRootHost);
+ nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host;
+ attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost));
+ }
+ }
+ return attached;
+};
+var isZeroArea = function isZeroArea2(node) {
+ var _node$getBoundingClie = node.getBoundingClientRect(), width = _node$getBoundingClie.width, height = _node$getBoundingClie.height;
+ return width === 0 && height === 0;
+};
+var isHidden = function isHidden2(node, _ref) {
+ var displayCheck = _ref.displayCheck, getShadowRoot = _ref.getShadowRoot;
+ if (getComputedStyle(node).visibility === "hidden") {
+ return true;
+ }
+ var isDirectSummary = matches.call(node, "details>summary:first-of-type");
+ var nodeUnderDetails = isDirectSummary ? node.parentElement : node;
+ if (matches.call(nodeUnderDetails, "details:not([open]) *")) {
+ return true;
+ }
+ if (!displayCheck || displayCheck === "full" || displayCheck === "legacy-full") {
+ if (typeof getShadowRoot === "function") {
+ var originalNode = node;
+ while (node) {
+ var parentElement = node.parentElement;
+ var rootNode = getRootNode(node);
+ if (parentElement && !parentElement.shadowRoot && getShadowRoot(parentElement) === true) {
+ return isZeroArea(node);
+ } else if (node.assignedSlot) {
+ node = node.assignedSlot;
+ } else if (!parentElement && rootNode !== node.ownerDocument) {
+ node = rootNode.host;
+ } else {
+ node = parentElement;
+ }
+ }
+ node = originalNode;
+ }
+ if (isNodeAttached(node)) {
+ return !node.getClientRects().length;
+ }
+ if (displayCheck !== "legacy-full") {
+ return true;
+ }
+ } else if (displayCheck === "non-zero-area") {
+ return isZeroArea(node);
+ }
+ return false;
+};
+var isDisabledFromFieldset = function isDisabledFromFieldset2(node) {
+ if (/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(node.tagName)) {
+ var parentNode = node.parentElement;
+ while (parentNode) {
+ if (parentNode.tagName === "FIELDSET" && parentNode.disabled) {
+ for (var i = 0; i < parentNode.children.length; i++) {
+ var child = parentNode.children.item(i);
+ if (child.tagName === "LEGEND") {
+ return matches.call(parentNode, "fieldset[disabled] *") ? true : !child.contains(node);
+ }
+ }
+ return true;
+ }
+ parentNode = parentNode.parentElement;
+ }
+ }
+ return false;
+};
+var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable2(options, node) {
+ if (node.disabled || // we must do an inert look up to filter out any elements inside an inert ancestor
+ // because we're limited in the type of selectors we can use in JSDom (see related
+ // note related to `candidateSelectors`)
+ isInert(node) || isHiddenInput(node) || isHidden(node, options) || // For a details element with a summary, the summary element gets the focus
+ isDetailsWithSummary(node) || isDisabledFromFieldset(node)) {
+ return false;
+ }
+ return true;
+};
+var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable2(options, node) {
+ if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) {
+ return false;
+ }
+ return true;
+};
+var isValidShadowRootTabbable = function isValidShadowRootTabbable2(shadowHostNode) {
+ var tabIndex = parseInt(shadowHostNode.getAttribute("tabindex"), 10);
+ if (isNaN(tabIndex) || tabIndex >= 0) {
+ return true;
+ }
+ return false;
+};
+var sortByOrder = function sortByOrder2(candidates) {
+ var regularTabbables = [];
+ var orderedTabbables = [];
+ candidates.forEach(function(item, i) {
+ var isScope = !!item.scopeParent;
+ var element = isScope ? item.scopeParent : item;
+ var candidateTabindex = getSortOrderTabIndex(element, isScope);
+ var elements = isScope ? sortByOrder2(item.candidates) : element;
+ if (candidateTabindex === 0) {
+ isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element);
+ } else {
+ orderedTabbables.push({
+ documentOrder: i,
+ tabIndex: candidateTabindex,
+ item,
+ isScope,
+ content: elements
+ });
+ }
+ });
+ return orderedTabbables.sort(sortOrderedTabbables).reduce(function(acc, sortable) {
+ sortable.isScope ? acc.push.apply(acc, sortable.content) : acc.push(sortable.content);
+ return acc;
+ }, []).concat(regularTabbables);
+};
+var tabbable = function tabbable2(container, options) {
+ options = options || {};
+ var candidates;
+ if (options.getShadowRoot) {
+ candidates = getCandidatesIteratively([container], options.includeContainer, {
+ filter: isNodeMatchingSelectorTabbable.bind(null, options),
+ flatten: false,
+ getShadowRoot: options.getShadowRoot,
+ shadowRootFilter: isValidShadowRootTabbable
+ });
+ } else {
+ candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
+ }
+ return sortByOrder(candidates);
+};
+var focusable = function focusable2(container, options) {
+ options = options || {};
+ var candidates;
+ if (options.getShadowRoot) {
+ candidates = getCandidatesIteratively([container], options.includeContainer, {
+ filter: isNodeMatchingSelectorFocusable.bind(null, options),
+ flatten: true,
+ getShadowRoot: options.getShadowRoot
+ });
+ } else {
+ candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
+ }
+ return candidates;
+};
+var isTabbable = function isTabbable2(node, options) {
+ options = options || {};
+ if (!node) {
+ throw new Error("No node provided");
+ }
+ if (matches.call(node, candidateSelector) === false) {
+ return false;
+ }
+ return isNodeMatchingSelectorTabbable(options, node);
+};
+var focusableCandidateSelector = /* @__PURE__ */ candidateSelectors.concat("iframe").join(",");
+var isFocusable = function isFocusable2(node, options) {
+ options = options || {};
+ if (!node) {
+ throw new Error("No node provided");
+ }
+ if (matches.call(node, focusableCandidateSelector) === false) {
+ return false;
+ }
+ return isNodeMatchingSelectorFocusable(options, node);
+};
+/*!
+* focus-trap 7.5.4
+* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+*/
+function ownKeys(e, r) {
+ var t = Object.keys(e);
+ if (Object.getOwnPropertySymbols) {
+ var o = Object.getOwnPropertySymbols(e);
+ r && (o = o.filter(function(r2) {
+ return Object.getOwnPropertyDescriptor(e, r2).enumerable;
+ })), t.push.apply(t, o);
+ }
+ return t;
+}
+function _objectSpread2(e) {
+ for (var r = 1; r < arguments.length; r++) {
+ var t = null != arguments[r] ? arguments[r] : {};
+ r % 2 ? ownKeys(Object(t), true).forEach(function(r2) {
+ _defineProperty(e, r2, t[r2]);
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r2) {
+ Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
+ });
+ }
+ return e;
+}
+function _defineProperty(obj, key, value) {
+ key = _toPropertyKey(key);
+ if (key in obj) {
+ Object.defineProperty(obj, key, {
+ value,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ });
+ } else {
+ obj[key] = value;
+ }
+ return obj;
+}
+function _toPrimitive(input, hint) {
+ if (typeof input !== "object" || input === null)
+ return input;
+ var prim = input[Symbol.toPrimitive];
+ if (prim !== void 0) {
+ var res = prim.call(input, hint || "default");
+ if (typeof res !== "object")
+ return res;
+ throw new TypeError("@@toPrimitive must return a primitive value.");
+ }
+ return (hint === "string" ? String : Number)(input);
+}
+function _toPropertyKey(arg) {
+ var key = _toPrimitive(arg, "string");
+ return typeof key === "symbol" ? key : String(key);
+}
+var activeFocusTraps = {
+ activateTrap: function activateTrap(trapStack, trap) {
+ if (trapStack.length > 0) {
+ var activeTrap = trapStack[trapStack.length - 1];
+ if (activeTrap !== trap) {
+ activeTrap.pause();
+ }
+ }
+ var trapIndex = trapStack.indexOf(trap);
+ if (trapIndex === -1) {
+ trapStack.push(trap);
+ } else {
+ trapStack.splice(trapIndex, 1);
+ trapStack.push(trap);
+ }
+ },
+ deactivateTrap: function deactivateTrap(trapStack, trap) {
+ var trapIndex = trapStack.indexOf(trap);
+ if (trapIndex !== -1) {
+ trapStack.splice(trapIndex, 1);
+ }
+ if (trapStack.length > 0) {
+ trapStack[trapStack.length - 1].unpause();
+ }
+ }
+};
+var isSelectableInput = function isSelectableInput2(node) {
+ return node.tagName && node.tagName.toLowerCase() === "input" && typeof node.select === "function";
+};
+var isEscapeEvent = function isEscapeEvent2(e) {
+ return (e === null || e === void 0 ? void 0 : e.key) === "Escape" || (e === null || e === void 0 ? void 0 : e.key) === "Esc" || (e === null || e === void 0 ? void 0 : e.keyCode) === 27;
+};
+var isTabEvent = function isTabEvent2(e) {
+ return (e === null || e === void 0 ? void 0 : e.key) === "Tab" || (e === null || e === void 0 ? void 0 : e.keyCode) === 9;
+};
+var isKeyForward = function isKeyForward2(e) {
+ return isTabEvent(e) && !e.shiftKey;
+};
+var isKeyBackward = function isKeyBackward2(e) {
+ return isTabEvent(e) && e.shiftKey;
+};
+var delay = function delay2(fn) {
+ return setTimeout(fn, 0);
+};
+var findIndex = function findIndex2(arr, fn) {
+ var idx = -1;
+ arr.every(function(value, i) {
+ if (fn(value)) {
+ idx = i;
+ return false;
+ }
+ return true;
+ });
+ return idx;
+};
+var valueOrHandler = function valueOrHandler2(value) {
+ for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ params[_key - 1] = arguments[_key];
+ }
+ return typeof value === "function" ? value.apply(void 0, params) : value;
+};
+var getActualTarget = function getActualTarget2(event) {
+ return event.target.shadowRoot && typeof event.composedPath === "function" ? event.composedPath()[0] : event.target;
+};
+var internalTrapStack = [];
+var createFocusTrap = function createFocusTrap2(elements, userOptions) {
+ var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document;
+ var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack;
+ var config = _objectSpread2({
+ returnFocusOnDeactivate: true,
+ escapeDeactivates: true,
+ delayInitialFocus: true,
+ isKeyForward,
+ isKeyBackward
+ }, userOptions);
+ var state = {
+ // containers given to createFocusTrap()
+ // @type {Array}
+ containers: [],
+ // list of objects identifying tabbable nodes in `containers` in the trap
+ // NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
+ // is active, but the trap should never get to a state where there isn't at least one group
+ // with at least one tabbable node in it (that would lead to an error condition that would
+ // result in an error being thrown)
+ // @type {Array<{
+ // container: HTMLElement,
+ // tabbableNodes: Array, // empty if none
+ // focusableNodes: Array, // empty if none
+ // posTabIndexesFound: boolean,
+ // firstTabbableNode: HTMLElement|undefined,
+ // lastTabbableNode: HTMLElement|undefined,
+ // firstDomTabbableNode: HTMLElement|undefined,
+ // lastDomTabbableNode: HTMLElement|undefined,
+ // nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
+ // }>}
+ containerGroups: [],
+ // same order/length as `containers` list
+ // references to objects in `containerGroups`, but only those that actually have
+ // tabbable nodes in them
+ // NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
+ // the same length
+ tabbableGroups: [],
+ nodeFocusedBeforeActivation: null,
+ mostRecentlyFocusedNode: null,
+ active: false,
+ paused: false,
+ // timer ID for when delayInitialFocus is true and initial focus in this trap
+ // has been delayed during activation
+ delayInitialFocusTimer: void 0,
+ // the most recent KeyboardEvent for the configured nav key (typically [SHIFT+]TAB), if any
+ recentNavEvent: void 0
+ };
+ var trap;
+ var getOption = function getOption2(configOverrideOptions, optionName, configOptionName) {
+ return configOverrideOptions && configOverrideOptions[optionName] !== void 0 ? configOverrideOptions[optionName] : config[configOptionName || optionName];
+ };
+ var findContainerIndex = function findContainerIndex2(element, event) {
+ var composedPath = typeof (event === null || event === void 0 ? void 0 : event.composedPath) === "function" ? event.composedPath() : void 0;
+ return state.containerGroups.findIndex(function(_ref) {
+ var container = _ref.container, tabbableNodes = _ref.tabbableNodes;
+ return container.contains(element) || // fall back to explicit tabbable search which will take into consideration any
+ // web components if the `tabbableOptions.getShadowRoot` option was used for
+ // the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
+ // look inside web components even if open)
+ (composedPath === null || composedPath === void 0 ? void 0 : composedPath.includes(container)) || tabbableNodes.find(function(node) {
+ return node === element;
+ });
+ });
+ };
+ var getNodeForOption = function getNodeForOption2(optionName) {
+ var optionValue = config[optionName];
+ if (typeof optionValue === "function") {
+ for (var _len2 = arguments.length, params = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
+ params[_key2 - 1] = arguments[_key2];
+ }
+ optionValue = optionValue.apply(void 0, params);
+ }
+ if (optionValue === true) {
+ optionValue = void 0;
+ }
+ if (!optionValue) {
+ if (optionValue === void 0 || optionValue === false) {
+ return optionValue;
+ }
+ throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node"));
+ }
+ var node = optionValue;
+ if (typeof optionValue === "string") {
+ node = doc.querySelector(optionValue);
+ if (!node) {
+ throw new Error("`".concat(optionName, "` as selector refers to no known node"));
+ }
+ }
+ return node;
+ };
+ var getInitialFocusNode = function getInitialFocusNode2() {
+ var node = getNodeForOption("initialFocus");
+ if (node === false) {
+ return false;
+ }
+ if (node === void 0 || !isFocusable(node, config.tabbableOptions)) {
+ if (findContainerIndex(doc.activeElement) >= 0) {
+ node = doc.activeElement;
+ } else {
+ var firstTabbableGroup = state.tabbableGroups[0];
+ var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
+ node = firstTabbableNode || getNodeForOption("fallbackFocus");
+ }
+ }
+ if (!node) {
+ throw new Error("Your focus-trap needs to have at least one focusable element");
+ }
+ return node;
+ };
+ var updateTabbableNodes = function updateTabbableNodes2() {
+ state.containerGroups = state.containers.map(function(container) {
+ var tabbableNodes = tabbable(container, config.tabbableOptions);
+ var focusableNodes = focusable(container, config.tabbableOptions);
+ var firstTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[0] : void 0;
+ var lastTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : void 0;
+ var firstDomTabbableNode = focusableNodes.find(function(node) {
+ return isTabbable(node);
+ });
+ var lastDomTabbableNode = focusableNodes.slice().reverse().find(function(node) {
+ return isTabbable(node);
+ });
+ var posTabIndexesFound = !!tabbableNodes.find(function(node) {
+ return getTabIndex(node) > 0;
+ });
+ return {
+ container,
+ tabbableNodes,
+ focusableNodes,
+ /** True if at least one node with positive `tabindex` was found in this container. */
+ posTabIndexesFound,
+ /** First tabbable node in container, __tabindex__ order; `undefined` if none. */
+ firstTabbableNode,
+ /** Last tabbable node in container, __tabindex__ order; `undefined` if none. */
+ lastTabbableNode,
+ // NOTE: DOM order is NOT NECESSARILY "document position" order, but figuring that out
+ // would require more than just https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
+ // because that API doesn't work with Shadow DOM as well as it should (@see
+ // https://github.com/whatwg/dom/issues/320) and since this first/last is only needed, so far,
+ // to address an edge case related to positive tabindex support, this seems like a much easier,
+ // "close enough most of the time" alternative for positive tabindexes which should generally
+ // be avoided anyway...
+ /** First tabbable node in container, __DOM__ order; `undefined` if none. */
+ firstDomTabbableNode,
+ /** Last tabbable node in container, __DOM__ order; `undefined` if none. */
+ lastDomTabbableNode,
+ /**
+ * Finds the __tabbable__ node that follows the given node in the specified direction,
+ * in this container, if any.
+ * @param {HTMLElement} node
+ * @param {boolean} [forward] True if going in forward tab order; false if going
+ * in reverse.
+ * @returns {HTMLElement|undefined} The next tabbable node, if any.
+ */
+ nextTabbableNode: function nextTabbableNode(node) {
+ var forward = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : true;
+ var nodeIdx = tabbableNodes.indexOf(node);
+ if (nodeIdx < 0) {
+ if (forward) {
+ return focusableNodes.slice(focusableNodes.indexOf(node) + 1).find(function(el) {
+ return isTabbable(el);
+ });
+ }
+ return focusableNodes.slice(0, focusableNodes.indexOf(node)).reverse().find(function(el) {
+ return isTabbable(el);
+ });
+ }
+ return tabbableNodes[nodeIdx + (forward ? 1 : -1)];
+ }
+ };
+ });
+ state.tabbableGroups = state.containerGroups.filter(function(group) {
+ return group.tabbableNodes.length > 0;
+ });
+ if (state.tabbableGroups.length <= 0 && !getNodeForOption("fallbackFocus")) {
+ throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times");
+ }
+ if (state.containerGroups.find(function(g) {
+ return g.posTabIndexesFound;
+ }) && state.containerGroups.length > 1) {
+ throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps.");
+ }
+ };
+ var getActiveElement = function getActiveElement2(el) {
+ var activeElement = el.activeElement;
+ if (!activeElement) {
+ return;
+ }
+ if (activeElement.shadowRoot && activeElement.shadowRoot.activeElement !== null) {
+ return getActiveElement2(activeElement.shadowRoot);
+ }
+ return activeElement;
+ };
+ var tryFocus = function tryFocus2(node) {
+ if (node === false) {
+ return;
+ }
+ if (node === getActiveElement(document)) {
+ return;
+ }
+ if (!node || !node.focus) {
+ tryFocus2(getInitialFocusNode());
+ return;
+ }
+ node.focus({
+ preventScroll: !!config.preventScroll
+ });
+ state.mostRecentlyFocusedNode = node;
+ if (isSelectableInput(node)) {
+ node.select();
+ }
+ };
+ var getReturnFocusNode = function getReturnFocusNode2(previousActiveElement) {
+ var node = getNodeForOption("setReturnFocus", previousActiveElement);
+ return node ? node : node === false ? false : previousActiveElement;
+ };
+ var findNextNavNode = function findNextNavNode2(_ref2) {
+ var target = _ref2.target, event = _ref2.event, _ref2$isBackward = _ref2.isBackward, isBackward = _ref2$isBackward === void 0 ? false : _ref2$isBackward;
+ target = target || getActualTarget(event);
+ updateTabbableNodes();
+ var destinationNode = null;
+ if (state.tabbableGroups.length > 0) {
+ var containerIndex = findContainerIndex(target, event);
+ var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : void 0;
+ if (containerIndex < 0) {
+ if (isBackward) {
+ destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
+ } else {
+ destinationNode = state.tabbableGroups[0].firstTabbableNode;
+ }
+ } else if (isBackward) {
+ var startOfGroupIndex = findIndex(state.tabbableGroups, function(_ref3) {
+ var firstTabbableNode = _ref3.firstTabbableNode;
+ return target === firstTabbableNode;
+ });
+ if (startOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) {
+ startOfGroupIndex = containerIndex;
+ }
+ if (startOfGroupIndex >= 0) {
+ var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
+ var destinationGroup = state.tabbableGroups[destinationGroupIndex];
+ destinationNode = getTabIndex(target) >= 0 ? destinationGroup.lastTabbableNode : destinationGroup.lastDomTabbableNode;
+ } else if (!isTabEvent(event)) {
+ destinationNode = containerGroup.nextTabbableNode(target, false);
+ }
+ } else {
+ var lastOfGroupIndex = findIndex(state.tabbableGroups, function(_ref4) {
+ var lastTabbableNode = _ref4.lastTabbableNode;
+ return target === lastTabbableNode;
+ });
+ if (lastOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) {
+ lastOfGroupIndex = containerIndex;
+ }
+ if (lastOfGroupIndex >= 0) {
+ var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
+ var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
+ destinationNode = getTabIndex(target) >= 0 ? _destinationGroup.firstTabbableNode : _destinationGroup.firstDomTabbableNode;
+ } else if (!isTabEvent(event)) {
+ destinationNode = containerGroup.nextTabbableNode(target);
+ }
+ }
+ } else {
+ destinationNode = getNodeForOption("fallbackFocus");
+ }
+ return destinationNode;
+ };
+ var checkPointerDown = function checkPointerDown2(e) {
+ var target = getActualTarget(e);
+ if (findContainerIndex(target, e) >= 0) {
+ return;
+ }
+ if (valueOrHandler(config.clickOutsideDeactivates, e)) {
+ trap.deactivate({
+ // NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
+ // which will result in the outside click setting focus to the node
+ // that was clicked (and if not focusable, to "nothing"); by setting
+ // `returnFocus: true`, we'll attempt to re-focus the node originally-focused
+ // on activation (or the configured `setReturnFocus` node), whether the
+ // outside click was on a focusable node or not
+ returnFocus: config.returnFocusOnDeactivate
+ });
+ return;
+ }
+ if (valueOrHandler(config.allowOutsideClick, e)) {
+ return;
+ }
+ e.preventDefault();
+ };
+ var checkFocusIn = function checkFocusIn2(event) {
+ var target = getActualTarget(event);
+ var targetContained = findContainerIndex(target, event) >= 0;
+ if (targetContained || target instanceof Document) {
+ if (targetContained) {
+ state.mostRecentlyFocusedNode = target;
+ }
+ } else {
+ event.stopImmediatePropagation();
+ var nextNode;
+ var navAcrossContainers = true;
+ if (state.mostRecentlyFocusedNode) {
+ if (getTabIndex(state.mostRecentlyFocusedNode) > 0) {
+ var mruContainerIdx = findContainerIndex(state.mostRecentlyFocusedNode);
+ var tabbableNodes = state.containerGroups[mruContainerIdx].tabbableNodes;
+ if (tabbableNodes.length > 0) {
+ var mruTabIdx = tabbableNodes.findIndex(function(node) {
+ return node === state.mostRecentlyFocusedNode;
+ });
+ if (mruTabIdx >= 0) {
+ if (config.isKeyForward(state.recentNavEvent)) {
+ if (mruTabIdx + 1 < tabbableNodes.length) {
+ nextNode = tabbableNodes[mruTabIdx + 1];
+ navAcrossContainers = false;
+ }
+ } else {
+ if (mruTabIdx - 1 >= 0) {
+ nextNode = tabbableNodes[mruTabIdx - 1];
+ navAcrossContainers = false;
+ }
+ }
+ }
+ }
+ } else {
+ if (!state.containerGroups.some(function(g) {
+ return g.tabbableNodes.some(function(n) {
+ return getTabIndex(n) > 0;
+ });
+ })) {
+ navAcrossContainers = false;
+ }
+ }
+ } else {
+ navAcrossContainers = false;
+ }
+ if (navAcrossContainers) {
+ nextNode = findNextNavNode({
+ // move FROM the MRU node, not event-related node (which will be the node that is
+ // outside the trap causing the focus escape we're trying to fix)
+ target: state.mostRecentlyFocusedNode,
+ isBackward: config.isKeyBackward(state.recentNavEvent)
+ });
+ }
+ if (nextNode) {
+ tryFocus(nextNode);
+ } else {
+ tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
+ }
+ }
+ state.recentNavEvent = void 0;
+ };
+ var checkKeyNav = function checkKeyNav2(event) {
+ var isBackward = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false;
+ state.recentNavEvent = event;
+ var destinationNode = findNextNavNode({
+ event,
+ isBackward
+ });
+ if (destinationNode) {
+ if (isTabEvent(event)) {
+ event.preventDefault();
+ }
+ tryFocus(destinationNode);
+ }
+ };
+ var checkKey = function checkKey2(event) {
+ if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) {
+ event.preventDefault();
+ trap.deactivate();
+ return;
+ }
+ if (config.isKeyForward(event) || config.isKeyBackward(event)) {
+ checkKeyNav(event, config.isKeyBackward(event));
+ }
+ };
+ var checkClick = function checkClick2(e) {
+ var target = getActualTarget(e);
+ if (findContainerIndex(target, e) >= 0) {
+ return;
+ }
+ if (valueOrHandler(config.clickOutsideDeactivates, e)) {
+ return;
+ }
+ if (valueOrHandler(config.allowOutsideClick, e)) {
+ return;
+ }
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ };
+ var addListeners = function addListeners2() {
+ if (!state.active) {
+ return;
+ }
+ activeFocusTraps.activateTrap(trapStack, trap);
+ state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function() {
+ tryFocus(getInitialFocusNode());
+ }) : tryFocus(getInitialFocusNode());
+ doc.addEventListener("focusin", checkFocusIn, true);
+ doc.addEventListener("mousedown", checkPointerDown, {
+ capture: true,
+ passive: false
+ });
+ doc.addEventListener("touchstart", checkPointerDown, {
+ capture: true,
+ passive: false
+ });
+ doc.addEventListener("click", checkClick, {
+ capture: true,
+ passive: false
+ });
+ doc.addEventListener("keydown", checkKey, {
+ capture: true,
+ passive: false
+ });
+ return trap;
+ };
+ var removeListeners = function removeListeners2() {
+ if (!state.active) {
+ return;
+ }
+ doc.removeEventListener("focusin", checkFocusIn, true);
+ doc.removeEventListener("mousedown", checkPointerDown, true);
+ doc.removeEventListener("touchstart", checkPointerDown, true);
+ doc.removeEventListener("click", checkClick, true);
+ doc.removeEventListener("keydown", checkKey, true);
+ return trap;
+ };
+ var checkDomRemoval = function checkDomRemoval2(mutations) {
+ var isFocusedNodeRemoved = mutations.some(function(mutation) {
+ var removedNodes = Array.from(mutation.removedNodes);
+ return removedNodes.some(function(node) {
+ return node === state.mostRecentlyFocusedNode;
+ });
+ });
+ if (isFocusedNodeRemoved) {
+ tryFocus(getInitialFocusNode());
+ }
+ };
+ var mutationObserver = typeof window !== "undefined" && "MutationObserver" in window ? new MutationObserver(checkDomRemoval) : void 0;
+ var updateObservedNodes = function updateObservedNodes2() {
+ if (!mutationObserver) {
+ return;
+ }
+ mutationObserver.disconnect();
+ if (state.active && !state.paused) {
+ state.containers.map(function(container) {
+ mutationObserver.observe(container, {
+ subtree: true,
+ childList: true
+ });
+ });
+ }
+ };
+ trap = {
+ get active() {
+ return state.active;
+ },
+ get paused() {
+ return state.paused;
+ },
+ activate: function activate(activateOptions) {
+ if (state.active) {
+ return this;
+ }
+ var onActivate = getOption(activateOptions, "onActivate");
+ var onPostActivate = getOption(activateOptions, "onPostActivate");
+ var checkCanFocusTrap = getOption(activateOptions, "checkCanFocusTrap");
+ if (!checkCanFocusTrap) {
+ updateTabbableNodes();
+ }
+ state.active = true;
+ state.paused = false;
+ state.nodeFocusedBeforeActivation = doc.activeElement;
+ onActivate === null || onActivate === void 0 || onActivate();
+ var finishActivation = function finishActivation2() {
+ if (checkCanFocusTrap) {
+ updateTabbableNodes();
+ }
+ addListeners();
+ updateObservedNodes();
+ onPostActivate === null || onPostActivate === void 0 || onPostActivate();
+ };
+ if (checkCanFocusTrap) {
+ checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
+ return this;
+ }
+ finishActivation();
+ return this;
+ },
+ deactivate: function deactivate(deactivateOptions) {
+ if (!state.active) {
+ return this;
+ }
+ var options = _objectSpread2({
+ onDeactivate: config.onDeactivate,
+ onPostDeactivate: config.onPostDeactivate,
+ checkCanReturnFocus: config.checkCanReturnFocus
+ }, deactivateOptions);
+ clearTimeout(state.delayInitialFocusTimer);
+ state.delayInitialFocusTimer = void 0;
+ removeListeners();
+ state.active = false;
+ state.paused = false;
+ updateObservedNodes();
+ activeFocusTraps.deactivateTrap(trapStack, trap);
+ var onDeactivate = getOption(options, "onDeactivate");
+ var onPostDeactivate = getOption(options, "onPostDeactivate");
+ var checkCanReturnFocus = getOption(options, "checkCanReturnFocus");
+ var returnFocus = getOption(options, "returnFocus", "returnFocusOnDeactivate");
+ onDeactivate === null || onDeactivate === void 0 || onDeactivate();
+ var finishDeactivation = function finishDeactivation2() {
+ delay(function() {
+ if (returnFocus) {
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
+ }
+ onPostDeactivate === null || onPostDeactivate === void 0 || onPostDeactivate();
+ });
+ };
+ if (returnFocus && checkCanReturnFocus) {
+ checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
+ return this;
+ }
+ finishDeactivation();
+ return this;
+ },
+ pause: function pause(pauseOptions) {
+ if (state.paused || !state.active) {
+ return this;
+ }
+ var onPause = getOption(pauseOptions, "onPause");
+ var onPostPause = getOption(pauseOptions, "onPostPause");
+ state.paused = true;
+ onPause === null || onPause === void 0 || onPause();
+ removeListeners();
+ updateObservedNodes();
+ onPostPause === null || onPostPause === void 0 || onPostPause();
+ return this;
+ },
+ unpause: function unpause(unpauseOptions) {
+ if (!state.paused || !state.active) {
+ return this;
+ }
+ var onUnpause = getOption(unpauseOptions, "onUnpause");
+ var onPostUnpause = getOption(unpauseOptions, "onPostUnpause");
+ state.paused = false;
+ onUnpause === null || onUnpause === void 0 || onUnpause();
+ updateTabbableNodes();
+ addListeners();
+ updateObservedNodes();
+ onPostUnpause === null || onPostUnpause === void 0 || onPostUnpause();
+ return this;
+ },
+ updateContainerElements: function updateContainerElements(containerElements) {
+ var elementsAsArray = [].concat(containerElements).filter(Boolean);
+ state.containers = elementsAsArray.map(function(element) {
+ return typeof element === "string" ? doc.querySelector(element) : element;
+ });
+ if (state.active) {
+ updateTabbableNodes();
+ }
+ updateObservedNodes();
+ return this;
+ }
+ };
+ trap.updateContainerElements(elements);
+ return trap;
+};
+function useFocusTrap(target, options = {}) {
+ let trap;
+ const { immediate, ...focusTrapOptions } = options;
+ const hasFocus = ref(false);
+ const isPaused = ref(false);
+ const activate = (opts) => trap && trap.activate(opts);
+ const deactivate = (opts) => trap && trap.deactivate(opts);
+ const pause = () => {
+ if (trap) {
+ trap.pause();
+ isPaused.value = true;
+ }
+ };
+ const unpause = () => {
+ if (trap) {
+ trap.unpause();
+ isPaused.value = false;
+ }
+ };
+ watch(
+ () => unrefElement(target),
+ (el) => {
+ if (!el)
+ return;
+ trap = createFocusTrap(el, {
+ ...focusTrapOptions,
+ onActivate() {
+ hasFocus.value = true;
+ if (options.onActivate)
+ options.onActivate();
+ },
+ onDeactivate() {
+ hasFocus.value = false;
+ if (options.onDeactivate)
+ options.onDeactivate();
+ }
+ });
+ if (immediate)
+ activate();
+ },
+ { flush: "post" }
+ );
+ tryOnScopeDispose(() => deactivate());
+ return {
+ hasFocus,
+ isPaused,
+ activate,
+ deactivate,
+ pause,
+ unpause
+ };
+}
+class DOMIterator {
+ /**
+ * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM
+ * element, an array of DOM elements, a NodeList or a selector
+ * @param {boolean} [iframes=true] - A boolean indicating if iframes should
+ * be handled
+ * @param {string[]} [exclude=[]] - An array containing exclusion selectors
+ * for iframes
+ * @param {number} [iframesTimeout=5000] - A number indicating the ms to
+ * wait before an iframe should be skipped, in case the load event isn't
+ * fired. This also applies if the user is offline and the resource of the
+ * iframe is online (either by the browsers "offline" mode or because
+ * there's no internet connection)
+ */
+ constructor(ctx, iframes = true, exclude = [], iframesTimeout = 5e3) {
+ this.ctx = ctx;
+ this.iframes = iframes;
+ this.exclude = exclude;
+ this.iframesTimeout = iframesTimeout;
+ }
+ /**
+ * Checks if the specified DOM element matches the selector
+ * @param {HTMLElement} element - The DOM element
+ * @param {string|string[]} selector - The selector or an array with
+ * selectors
+ * @return {boolean}
+ * @access public
+ */
+ static matches(element, selector) {
+ const selectors = typeof selector === "string" ? [selector] : selector, fn = element.matches || element.matchesSelector || element.msMatchesSelector || element.mozMatchesSelector || element.oMatchesSelector || element.webkitMatchesSelector;
+ if (fn) {
+ let match = false;
+ selectors.every((sel) => {
+ if (fn.call(element, sel)) {
+ match = true;
+ return false;
+ }
+ return true;
+ });
+ return match;
+ } else {
+ return false;
+ }
+ }
+ /**
+ * Returns all contexts filtered by duplicates (even nested)
+ * @return {HTMLElement[]} - An array containing DOM contexts
+ * @access protected
+ */
+ getContexts() {
+ let ctx, filteredCtx = [];
+ if (typeof this.ctx === "undefined" || !this.ctx) {
+ ctx = [];
+ } else if (NodeList.prototype.isPrototypeOf(this.ctx)) {
+ ctx = Array.prototype.slice.call(this.ctx);
+ } else if (Array.isArray(this.ctx)) {
+ ctx = this.ctx;
+ } else if (typeof this.ctx === "string") {
+ ctx = Array.prototype.slice.call(
+ document.querySelectorAll(this.ctx)
+ );
+ } else {
+ ctx = [this.ctx];
+ }
+ ctx.forEach((ctx2) => {
+ const isDescendant = filteredCtx.filter((contexts) => {
+ return contexts.contains(ctx2);
+ }).length > 0;
+ if (filteredCtx.indexOf(ctx2) === -1 && !isDescendant) {
+ filteredCtx.push(ctx2);
+ }
+ });
+ return filteredCtx;
+ }
+ /**
+ * @callback DOMIterator~getIframeContentsSuccessCallback
+ * @param {HTMLDocument} contents - The contentDocument of the iframe
+ */
+ /**
+ * Calls the success callback function with the iframe document. If it can't
+ * be accessed it calls the error callback function
+ * @param {HTMLElement} ifr - The iframe DOM element
+ * @param {DOMIterator~getIframeContentsSuccessCallback} successFn
+ * @param {function} [errorFn]
+ * @access protected
+ */
+ getIframeContents(ifr, successFn, errorFn = () => {
+ }) {
+ let doc;
+ try {
+ const ifrWin = ifr.contentWindow;
+ doc = ifrWin.document;
+ if (!ifrWin || !doc) {
+ throw new Error("iframe inaccessible");
+ }
+ } catch (e) {
+ errorFn();
+ }
+ if (doc) {
+ successFn(doc);
+ }
+ }
+ /**
+ * Checks if an iframe is empty (if about:blank is the shown page)
+ * @param {HTMLElement} ifr - The iframe DOM element
+ * @return {boolean}
+ * @access protected
+ */
+ isIframeBlank(ifr) {
+ const bl = "about:blank", src = ifr.getAttribute("src").trim(), href = ifr.contentWindow.location.href;
+ return href === bl && src !== bl && src;
+ }
+ /**
+ * Observes the onload event of an iframe and calls the success callback or
+ * the error callback if the iframe is inaccessible. If the event isn't
+ * fired within the specified {@link DOMIterator#iframesTimeout}, then it'll
+ * call the error callback too
+ * @param {HTMLElement} ifr - The iframe DOM element
+ * @param {DOMIterator~getIframeContentsSuccessCallback} successFn
+ * @param {function} errorFn
+ * @access protected
+ */
+ observeIframeLoad(ifr, successFn, errorFn) {
+ let called = false, tout = null;
+ const listener = () => {
+ if (called) {
+ return;
+ }
+ called = true;
+ clearTimeout(tout);
+ try {
+ if (!this.isIframeBlank(ifr)) {
+ ifr.removeEventListener("load", listener);
+ this.getIframeContents(ifr, successFn, errorFn);
+ }
+ } catch (e) {
+ errorFn();
+ }
+ };
+ ifr.addEventListener("load", listener);
+ tout = setTimeout(listener, this.iframesTimeout);
+ }
+ /**
+ * Callback when the iframe is ready
+ * @callback DOMIterator~onIframeReadySuccessCallback
+ * @param {HTMLDocument} contents - The contentDocument of the iframe
+ */
+ /**
+ * Callback if the iframe can't be accessed
+ * @callback DOMIterator~onIframeReadyErrorCallback
+ */
+ /**
+ * Calls the callback if the specified iframe is ready for DOM access
+ * @param {HTMLElement} ifr - The iframe DOM element
+ * @param {DOMIterator~onIframeReadySuccessCallback} successFn - Success
+ * callback
+ * @param {DOMIterator~onIframeReadyErrorCallback} errorFn - Error callback
+ * @see {@link http://stackoverflow.com/a/36155560/3894981} for
+ * background information
+ * @access protected
+ */
+ onIframeReady(ifr, successFn, errorFn) {
+ try {
+ if (ifr.contentWindow.document.readyState === "complete") {
+ if (this.isIframeBlank(ifr)) {
+ this.observeIframeLoad(ifr, successFn, errorFn);
+ } else {
+ this.getIframeContents(ifr, successFn, errorFn);
+ }
+ } else {
+ this.observeIframeLoad(ifr, successFn, errorFn);
+ }
+ } catch (e) {
+ errorFn();
+ }
+ }
+ /**
+ * Callback when all iframes are ready for DOM access
+ * @callback DOMIterator~waitForIframesDoneCallback
+ */
+ /**
+ * Iterates over all iframes and calls the done callback when all of them
+ * are ready for DOM access (including nested ones)
+ * @param {HTMLElement} ctx - The context DOM element
+ * @param {DOMIterator~waitForIframesDoneCallback} done - Done callback
+ */
+ waitForIframes(ctx, done) {
+ let eachCalled = 0;
+ this.forEachIframe(ctx, () => true, (ifr) => {
+ eachCalled++;
+ this.waitForIframes(ifr.querySelector("html"), () => {
+ if (!--eachCalled) {
+ done();
+ }
+ });
+ }, (handled) => {
+ if (!handled) {
+ done();
+ }
+ });
+ }
+ /**
+ * Callback allowing to filter an iframe. Must return true when the element
+ * should remain, otherwise false
+ * @callback DOMIterator~forEachIframeFilterCallback
+ * @param {HTMLElement} iframe - The iframe DOM element
+ */
+ /**
+ * Callback for each iframe content
+ * @callback DOMIterator~forEachIframeEachCallback
+ * @param {HTMLElement} content - The iframe document
+ */
+ /**
+ * Callback if all iframes inside the context were handled
+ * @callback DOMIterator~forEachIframeEndCallback
+ * @param {number} handled - The number of handled iframes (those who
+ * wheren't filtered)
+ */
+ /**
+ * Iterates over all iframes inside the specified context and calls the
+ * callbacks when they're ready. Filters iframes based on the instance
+ * exclusion selectors
+ * @param {HTMLElement} ctx - The context DOM element
+ * @param {DOMIterator~forEachIframeFilterCallback} filter - Filter callback
+ * @param {DOMIterator~forEachIframeEachCallback} each - Each callback
+ * @param {DOMIterator~forEachIframeEndCallback} [end] - End callback
+ * @access protected
+ */
+ forEachIframe(ctx, filter, each, end = () => {
+ }) {
+ let ifr = ctx.querySelectorAll("iframe"), open = ifr.length, handled = 0;
+ ifr = Array.prototype.slice.call(ifr);
+ const checkEnd = () => {
+ if (--open <= 0) {
+ end(handled);
+ }
+ };
+ if (!open) {
+ checkEnd();
+ }
+ ifr.forEach((ifr2) => {
+ if (DOMIterator.matches(ifr2, this.exclude)) {
+ checkEnd();
+ } else {
+ this.onIframeReady(ifr2, (con) => {
+ if (filter(ifr2)) {
+ handled++;
+ each(con);
+ }
+ checkEnd();
+ }, checkEnd);
+ }
+ });
+ }
+ /**
+ * Creates a NodeIterator on the specified context
+ * @see {@link https://developer.mozilla.org/en/docs/Web/API/NodeIterator}
+ * @param {HTMLElement} ctx - The context DOM element
+ * @param {DOMIterator~whatToShow} whatToShow
+ * @param {DOMIterator~filterCb} filter
+ * @return {NodeIterator}
+ * @access protected
+ */
+ createIterator(ctx, whatToShow, filter) {
+ return document.createNodeIterator(ctx, whatToShow, filter, false);
+ }
+ /**
+ * Creates an instance of DOMIterator in an iframe
+ * @param {HTMLDocument} contents - Iframe document
+ * @return {DOMIterator}
+ * @access protected
+ */
+ createInstanceOnIframe(contents) {
+ return new DOMIterator(contents.querySelector("html"), this.iframes);
+ }
+ /**
+ * Checks if an iframe occurs between two nodes, more specifically if an
+ * iframe occurs before the specified node and after the specified prevNode
+ * @param {HTMLElement} node - The node that should occur after the iframe
+ * @param {HTMLElement} prevNode - The node that should occur before the
+ * iframe
+ * @param {HTMLElement} ifr - The iframe to check against
+ * @return {boolean}
+ * @access protected
+ */
+ compareNodeIframe(node, prevNode, ifr) {
+ const compCurr = node.compareDocumentPosition(ifr), prev = Node.DOCUMENT_POSITION_PRECEDING;
+ if (compCurr & prev) {
+ if (prevNode !== null) {
+ const compPrev = prevNode.compareDocumentPosition(ifr), after = Node.DOCUMENT_POSITION_FOLLOWING;
+ if (compPrev & after) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+ /**
+ * @typedef {DOMIterator~getIteratorNodeReturn}
+ * @type {object.}
+ * @property {HTMLElement} prevNode - The previous node or null if there is
+ * no
+ * @property {HTMLElement} node - The current node
+ */
+ /**
+ * Returns the previous and current node of the specified iterator
+ * @param {NodeIterator} itr - The iterator
+ * @return {DOMIterator~getIteratorNodeReturn}
+ * @access protected
+ */
+ getIteratorNode(itr) {
+ const prevNode = itr.previousNode();
+ let node;
+ if (prevNode === null) {
+ node = itr.nextNode();
+ } else {
+ node = itr.nextNode() && itr.nextNode();
+ }
+ return {
+ prevNode,
+ node
+ };
+ }
+ /**
+ * An array containing objects. The object key "val" contains an iframe
+ * DOM element. The object key "handled" contains a boolean indicating if
+ * the iframe was handled already.
+ * It wouldn't be enough to save all open or all already handled iframes.
+ * The information of open iframes is necessary because they may occur after
+ * all other text nodes (and compareNodeIframe would never be true). The
+ * information of already handled iframes is necessary as otherwise they may
+ * be handled multiple times
+ * @typedef DOMIterator~checkIframeFilterIfr
+ * @type {object[]}
+ */
+ /**
+ * Checks if an iframe wasn't handled already and if so, calls
+ * {@link DOMIterator#compareNodeIframe} to check if it should be handled.
+ * Information wheter an iframe was or wasn't handled is given within the
+ * ifr dictionary
+ * @param {HTMLElement} node - The node that should occur after the iframe
+ * @param {HTMLElement} prevNode - The node that should occur before the
+ * iframe
+ * @param {HTMLElement} currIfr - The iframe to check
+ * @param {DOMIterator~checkIframeFilterIfr} ifr - The iframe dictionary.
+ * Will be manipulated (by reference)
+ * @return {boolean} Returns true when it should be handled, otherwise false
+ * @access protected
+ */
+ checkIframeFilter(node, prevNode, currIfr, ifr) {
+ let key = false, handled = false;
+ ifr.forEach((ifrDict, i) => {
+ if (ifrDict.val === currIfr) {
+ key = i;
+ handled = ifrDict.handled;
+ }
+ });
+ if (this.compareNodeIframe(node, prevNode, currIfr)) {
+ if (key === false && !handled) {
+ ifr.push({
+ val: currIfr,
+ handled: true
+ });
+ } else if (key !== false && !handled) {
+ ifr[key].handled = true;
+ }
+ return true;
+ }
+ if (key === false) {
+ ifr.push({
+ val: currIfr,
+ handled: false
+ });
+ }
+ return false;
+ }
+ /**
+ * Creates an iterator on all open iframes in the specified array and calls
+ * the end callback when finished
+ * @param {DOMIterator~checkIframeFilterIfr} ifr
+ * @param {DOMIterator~whatToShow} whatToShow
+ * @param {DOMIterator~forEachNodeCallback} eCb - Each callback
+ * @param {DOMIterator~filterCb} fCb
+ * @access protected
+ */
+ handleOpenIframes(ifr, whatToShow, eCb, fCb) {
+ ifr.forEach((ifrDict) => {
+ if (!ifrDict.handled) {
+ this.getIframeContents(ifrDict.val, (con) => {
+ this.createInstanceOnIframe(con).forEachNode(
+ whatToShow,
+ eCb,
+ fCb
+ );
+ });
+ }
+ });
+ }
+ /**
+ * Iterates through all nodes in the specified context and handles iframe
+ * nodes at the correct position
+ * @param {DOMIterator~whatToShow} whatToShow
+ * @param {HTMLElement} ctx - The context
+ * @param {DOMIterator~forEachNodeCallback} eachCb - Each callback
+ * @param {DOMIterator~filterCb} filterCb - Filter callback
+ * @param {DOMIterator~forEachNodeEndCallback} doneCb - End callback
+ * @access protected
+ */
+ iterateThroughNodes(whatToShow, ctx, eachCb, filterCb, doneCb) {
+ const itr = this.createIterator(ctx, whatToShow, filterCb);
+ let ifr = [], elements = [], node, prevNode, retrieveNodes = () => {
+ ({
+ prevNode,
+ node
+ } = this.getIteratorNode(itr));
+ return node;
+ };
+ while (retrieveNodes()) {
+ if (this.iframes) {
+ this.forEachIframe(ctx, (currIfr) => {
+ return this.checkIframeFilter(node, prevNode, currIfr, ifr);
+ }, (con) => {
+ this.createInstanceOnIframe(con).forEachNode(
+ whatToShow,
+ (ifrNode) => elements.push(ifrNode),
+ filterCb
+ );
+ });
+ }
+ elements.push(node);
+ }
+ elements.forEach((node2) => {
+ eachCb(node2);
+ });
+ if (this.iframes) {
+ this.handleOpenIframes(ifr, whatToShow, eachCb, filterCb);
+ }
+ doneCb();
+ }
+ /**
+ * Callback for each node
+ * @callback DOMIterator~forEachNodeCallback
+ * @param {HTMLElement} node - The DOM text node element
+ */
+ /**
+ * Callback if all contexts were handled
+ * @callback DOMIterator~forEachNodeEndCallback
+ */
+ /**
+ * Iterates over all contexts and initializes
+ * {@link DOMIterator#iterateThroughNodes iterateThroughNodes} on them
+ * @param {DOMIterator~whatToShow} whatToShow
+ * @param {DOMIterator~forEachNodeCallback} each - Each callback
+ * @param {DOMIterator~filterCb} filter - Filter callback
+ * @param {DOMIterator~forEachNodeEndCallback} done - End callback
+ * @access public
+ */
+ forEachNode(whatToShow, each, filter, done = () => {
+ }) {
+ const contexts = this.getContexts();
+ let open = contexts.length;
+ if (!open) {
+ done();
+ }
+ contexts.forEach((ctx) => {
+ const ready = () => {
+ this.iterateThroughNodes(whatToShow, ctx, each, filter, () => {
+ if (--open <= 0) {
+ done();
+ }
+ });
+ };
+ if (this.iframes) {
+ this.waitForIframes(ctx, ready);
+ } else {
+ ready();
+ }
+ });
+ }
+ /**
+ * Callback to filter nodes. Can return e.g. NodeFilter.FILTER_ACCEPT or
+ * NodeFilter.FILTER_REJECT
+ * @see {@link http://tinyurl.com/zdczmm2}
+ * @callback DOMIterator~filterCb
+ * @param {HTMLElement} node - The node to filter
+ */
+ /**
+ * @typedef DOMIterator~whatToShow
+ * @see {@link http://tinyurl.com/zfqqkx2}
+ * @type {number}
+ */
+}
+let Mark$1 = class Mark {
+ // eslint-disable-line no-unused-vars
+ /**
+ * @param {HTMLElement|HTMLElement[]|NodeList|string} ctx - The context DOM
+ * element, an array of DOM elements, a NodeList or a selector
+ */
+ constructor(ctx) {
+ this.ctx = ctx;
+ this.ie = false;
+ const ua = window.navigator.userAgent;
+ if (ua.indexOf("MSIE") > -1 || ua.indexOf("Trident") > -1) {
+ this.ie = true;
+ }
+ }
+ /**
+ * Options defined by the user. They will be initialized from one of the
+ * public methods. See {@link Mark#mark}, {@link Mark#markRegExp},
+ * {@link Mark#markRanges} and {@link Mark#unmark} for option properties.
+ * @type {object}
+ * @param {object} [val] - An object that will be merged with defaults
+ * @access protected
+ */
+ set opt(val) {
+ this._opt = Object.assign({}, {
+ "element": "",
+ "className": "",
+ "exclude": [],
+ "iframes": false,
+ "iframesTimeout": 5e3,
+ "separateWordSearch": true,
+ "diacritics": true,
+ "synonyms": {},
+ "accuracy": "partially",
+ "acrossElements": false,
+ "caseSensitive": false,
+ "ignoreJoiners": false,
+ "ignoreGroups": 0,
+ "ignorePunctuation": [],
+ "wildcards": "disabled",
+ "each": () => {
+ },
+ "noMatch": () => {
+ },
+ "filter": () => true,
+ "done": () => {
+ },
+ "debug": false,
+ "log": window.console
+ }, val);
+ }
+ get opt() {
+ return this._opt;
+ }
+ /**
+ * An instance of DOMIterator
+ * @type {DOMIterator}
+ * @access protected
+ */
+ get iterator() {
+ return new DOMIterator(
+ this.ctx,
+ this.opt.iframes,
+ this.opt.exclude,
+ this.opt.iframesTimeout
+ );
+ }
+ /**
+ * Logs a message if log is enabled
+ * @param {string} msg - The message to log
+ * @param {string} [level="debug"] - The log level, e.g. warn
+ * error, debug
+ * @access protected
+ */
+ log(msg, level = "debug") {
+ const log = this.opt.log;
+ if (!this.opt.debug) {
+ return;
+ }
+ if (typeof log === "object" && typeof log[level] === "function") {
+ log[level](`mark.js: ${msg}`);
+ }
+ }
+ /**
+ * Escapes a string for usage within a regular expression
+ * @param {string} str - The string to escape
+ * @return {string}
+ * @access protected
+ */
+ escapeStr(str) {
+ return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ }
+ /**
+ * Creates a regular expression string to match the specified search
+ * term including synonyms, diacritics and accuracy if defined
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ createRegExp(str) {
+ if (this.opt.wildcards !== "disabled") {
+ str = this.setupWildcardsRegExp(str);
+ }
+ str = this.escapeStr(str);
+ if (Object.keys(this.opt.synonyms).length) {
+ str = this.createSynonymsRegExp(str);
+ }
+ if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) {
+ str = this.setupIgnoreJoinersRegExp(str);
+ }
+ if (this.opt.diacritics) {
+ str = this.createDiacriticsRegExp(str);
+ }
+ str = this.createMergedBlanksRegExp(str);
+ if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) {
+ str = this.createJoinersRegExp(str);
+ }
+ if (this.opt.wildcards !== "disabled") {
+ str = this.createWildcardsRegExp(str);
+ }
+ str = this.createAccuracyRegExp(str);
+ return str;
+ }
+ /**
+ * Creates a regular expression string to match the defined synonyms
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ createSynonymsRegExp(str) {
+ const syn = this.opt.synonyms, sens = this.opt.caseSensitive ? "" : "i", joinerPlaceholder = this.opt.ignoreJoiners || this.opt.ignorePunctuation.length ? "\0" : "";
+ for (let index in syn) {
+ if (syn.hasOwnProperty(index)) {
+ const value = syn[index], k1 = this.opt.wildcards !== "disabled" ? this.setupWildcardsRegExp(index) : this.escapeStr(index), k2 = this.opt.wildcards !== "disabled" ? this.setupWildcardsRegExp(value) : this.escapeStr(value);
+ if (k1 !== "" && k2 !== "") {
+ str = str.replace(
+ new RegExp(
+ `(${this.escapeStr(k1)}|${this.escapeStr(k2)})`,
+ `gm${sens}`
+ ),
+ joinerPlaceholder + `(${this.processSynomyms(k1)}|${this.processSynomyms(k2)})` + joinerPlaceholder
+ );
+ }
+ }
+ }
+ return str;
+ }
+ /**
+ * Setup synonyms to work with ignoreJoiners and or ignorePunctuation
+ * @param {string} str - synonym key or value to process
+ * @return {string} - processed synonym string
+ */
+ processSynomyms(str) {
+ if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) {
+ str = this.setupIgnoreJoinersRegExp(str);
+ }
+ return str;
+ }
+ /**
+ * Sets up the regular expression string to allow later insertion of
+ * wildcard regular expression matches
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ setupWildcardsRegExp(str) {
+ str = str.replace(/(?:\\)*\?/g, (val) => {
+ return val.charAt(0) === "\\" ? "?" : "";
+ });
+ return str.replace(/(?:\\)*\*/g, (val) => {
+ return val.charAt(0) === "\\" ? "*" : "";
+ });
+ }
+ /**
+ * Sets up the regular expression string to allow later insertion of
+ * wildcard regular expression matches
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ createWildcardsRegExp(str) {
+ let spaces = this.opt.wildcards === "withSpaces";
+ return str.replace(/\u0001/g, spaces ? "[\\S\\s]?" : "\\S?").replace(/\u0002/g, spaces ? "[\\S\\s]*?" : "\\S*");
+ }
+ /**
+ * Sets up the regular expression string to allow later insertion of
+ * designated characters (soft hyphens & zero width characters)
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ setupIgnoreJoinersRegExp(str) {
+ return str.replace(/[^(|)\\]/g, (val, indx, original) => {
+ let nextChar = original.charAt(indx + 1);
+ if (/[(|)\\]/.test(nextChar) || nextChar === "") {
+ return val;
+ } else {
+ return val + "\0";
+ }
+ });
+ }
+ /**
+ * Creates a regular expression string to allow ignoring of designated
+ * characters (soft hyphens, zero width characters & punctuation) based on
+ * the specified option values of ignorePunctuation and
+ * ignoreJoiners
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ createJoinersRegExp(str) {
+ let joiner = [];
+ const ignorePunctuation = this.opt.ignorePunctuation;
+ if (Array.isArray(ignorePunctuation) && ignorePunctuation.length) {
+ joiner.push(this.escapeStr(ignorePunctuation.join("")));
+ }
+ if (this.opt.ignoreJoiners) {
+ joiner.push("\\u00ad\\u200b\\u200c\\u200d");
+ }
+ return joiner.length ? str.split(/\u0000+/).join(`[${joiner.join("")}]*`) : str;
+ }
+ /**
+ * Creates a regular expression string to match diacritics
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ createDiacriticsRegExp(str) {
+ const sens = this.opt.caseSensitive ? "" : "i", dct = this.opt.caseSensitive ? [
+ "aàáảãạăằắẳẵặâầấẩẫậäåāą",
+ "AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ",
+ "cçćč",
+ "CÇĆČ",
+ "dđď",
+ "DĐĎ",
+ "eèéẻẽẹêềếểễệëěēę",
+ "EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ",
+ "iìíỉĩịîïī",
+ "IÌÍỈĨỊÎÏĪ",
+ "lł",
+ "LŁ",
+ "nñňń",
+ "NÑŇŃ",
+ "oòóỏõọôồốổỗộơởỡớờợöøō",
+ "OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ",
+ "rř",
+ "RŘ",
+ "sšśșş",
+ "SŠŚȘŞ",
+ "tťțţ",
+ "TŤȚŢ",
+ "uùúủũụưừứửữựûüůū",
+ "UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ",
+ "yýỳỷỹỵÿ",
+ "YÝỲỶỸỴŸ",
+ "zžżź",
+ "ZŽŻŹ"
+ ] : [
+ "aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ",
+ "cçćčCÇĆČ",
+ "dđďDĐĎ",
+ "eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ",
+ "iìíỉĩịîïīIÌÍỈĨỊÎÏĪ",
+ "lłLŁ",
+ "nñňńNÑŇŃ",
+ "oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ",
+ "rřRŘ",
+ "sšśșşSŠŚȘŞ",
+ "tťțţTŤȚŢ",
+ "uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ",
+ "yýỳỷỹỵÿYÝỲỶỸỴŸ",
+ "zžżźZŽŻŹ"
+ ];
+ let handled = [];
+ str.split("").forEach((ch) => {
+ dct.every((dct2) => {
+ if (dct2.indexOf(ch) !== -1) {
+ if (handled.indexOf(dct2) > -1) {
+ return false;
+ }
+ str = str.replace(
+ new RegExp(`[${dct2}]`, `gm${sens}`),
+ `[${dct2}]`
+ );
+ handled.push(dct2);
+ }
+ return true;
+ });
+ });
+ return str;
+ }
+ /**
+ * Creates a regular expression string that merges whitespace characters
+ * including subsequent ones into a single pattern, one or multiple
+ * whitespaces
+ * @param {string} str - The search term to be used
+ * @return {string}
+ * @access protected
+ */
+ createMergedBlanksRegExp(str) {
+ return str.replace(/[\s]+/gmi, "[\\s]+");
+ }
+ /**
+ * Creates a regular expression string to match the specified string with
+ * the defined accuracy. As in the regular expression of "exactly" can be
+ * a group containing a blank at the beginning, all regular expressions will
+ * be created with two groups. The first group can be ignored (may contain
+ * the said blank), the second contains the actual match
+ * @param {string} str - The searm term to be used
+ * @return {str}
+ * @access protected
+ */
+ createAccuracyRegExp(str) {
+ const chars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿";
+ let acc = this.opt.accuracy, val = typeof acc === "string" ? acc : acc.value, ls = typeof acc === "string" ? [] : acc.limiters, lsJoin = "";
+ ls.forEach((limiter) => {
+ lsJoin += `|${this.escapeStr(limiter)}`;
+ });
+ switch (val) {
+ case "partially":
+ default:
+ return `()(${str})`;
+ case "complementary":
+ lsJoin = "\\s" + (lsJoin ? lsJoin : this.escapeStr(chars));
+ return `()([^${lsJoin}]*${str}[^${lsJoin}]*)`;
+ case "exactly":
+ return `(^|\\s${lsJoin})(${str})(?=$|\\s${lsJoin})`;
+ }
+ }
+ /**
+ * @typedef Mark~separatedKeywords
+ * @type {object.}
+ * @property {array.} keywords - The list of keywords
+ * @property {number} length - The length
+ */
+ /**
+ * Returns a list of keywords dependent on whether separate word search
+ * was defined. Also it filters empty keywords
+ * @param {array} sv - The array of keywords
+ * @return {Mark~separatedKeywords}
+ * @access protected
+ */
+ getSeparatedKeywords(sv) {
+ let stack = [];
+ sv.forEach((kw) => {
+ if (!this.opt.separateWordSearch) {
+ if (kw.trim() && stack.indexOf(kw) === -1) {
+ stack.push(kw);
+ }
+ } else {
+ kw.split(" ").forEach((kwSplitted) => {
+ if (kwSplitted.trim() && stack.indexOf(kwSplitted) === -1) {
+ stack.push(kwSplitted);
+ }
+ });
+ }
+ });
+ return {
+ // sort because of https://git.io/v6USg
+ "keywords": stack.sort((a, b) => {
+ return b.length - a.length;
+ }),
+ "length": stack.length
+ };
+ }
+ /**
+ * Check if a value is a number
+ * @param {number|string} value - the value to check;
+ * numeric strings allowed
+ * @return {boolean}
+ * @access protected
+ */
+ isNumeric(value) {
+ return Number(parseFloat(value)) == value;
+ }
+ /**
+ * @typedef Mark~rangeObject
+ * @type {object}
+ * @property {number} start - The start position within the composite value
+ * @property {number} length - The length of the string to mark within the
+ * composite value.
+ */
+ /**
+ * @typedef Mark~setOfRanges
+ * @type {object[]}
+ * @property {Mark~rangeObject}
+ */
+ /**
+ * Returns a processed list of integer offset indexes that do not overlap
+ * each other, and remove any string values or additional elements
+ * @param {Mark~setOfRanges} array - unprocessed raw array
+ * @return {Mark~setOfRanges} - processed array with any invalid entries
+ * removed
+ * @throws Will throw an error if an array of objects is not passed
+ * @access protected
+ */
+ checkRanges(array) {
+ if (!Array.isArray(array) || Object.prototype.toString.call(array[0]) !== "[object Object]") {
+ this.log("markRanges() will only accept an array of objects");
+ this.opt.noMatch(array);
+ return [];
+ }
+ const stack = [];
+ let last2 = 0;
+ array.sort((a, b) => {
+ return a.start - b.start;
+ }).forEach((item) => {
+ let { start, end, valid } = this.callNoMatchOnInvalidRanges(item, last2);
+ if (valid) {
+ item.start = start;
+ item.length = end - start;
+ stack.push(item);
+ last2 = end;
+ }
+ });
+ return stack;
+ }
+ /**
+ * @typedef Mark~validObject
+ * @type {object}
+ * @property {number} start - The start position within the composite value
+ * @property {number} end - The calculated end position within the composite
+ * value.
+ * @property {boolean} valid - boolean value indicating that the start and
+ * calculated end range is valid
+ */
+ /**
+ * Initial validation of ranges for markRanges. Preliminary checks are done
+ * to ensure the start and length values exist and are not zero or non-
+ * numeric
+ * @param {Mark~rangeObject} range - the current range object
+ * @param {number} last - last index of range
+ * @return {Mark~validObject}
+ * @access protected
+ */
+ callNoMatchOnInvalidRanges(range, last2) {
+ let start, end, valid = false;
+ if (range && typeof range.start !== "undefined") {
+ start = parseInt(range.start, 10);
+ end = start + parseInt(range.length, 10);
+ if (this.isNumeric(range.start) && this.isNumeric(range.length) && end - last2 > 0 && end - start > 0) {
+ valid = true;
+ } else {
+ this.log(
+ `Ignoring invalid or overlapping range: ${JSON.stringify(range)}`
+ );
+ this.opt.noMatch(range);
+ }
+ } else {
+ this.log(`Ignoring invalid range: ${JSON.stringify(range)}`);
+ this.opt.noMatch(range);
+ }
+ return {
+ start,
+ end,
+ valid
+ };
+ }
+ /**
+ * Check valid range for markRanges. Check ranges with access to the context
+ * string. Range values are double checked, lengths that extend the mark
+ * beyond the string length are limitied and ranges containing only
+ * whitespace are ignored
+ * @param {Mark~rangeObject} range - the current range object
+ * @param {number} originalLength - original length of the context string
+ * @param {string} string - current content string
+ * @return {Mark~validObject}
+ * @access protected
+ */
+ checkWhitespaceRanges(range, originalLength, string) {
+ let end, valid = true, max = string.length, offset = originalLength - max, start = parseInt(range.start, 10) - offset;
+ start = start > max ? max : start;
+ end = start + parseInt(range.length, 10);
+ if (end > max) {
+ end = max;
+ this.log(`End range automatically set to the max value of ${max}`);
+ }
+ if (start < 0 || end - start < 0 || start > max || end > max) {
+ valid = false;
+ this.log(`Invalid range: ${JSON.stringify(range)}`);
+ this.opt.noMatch(range);
+ } else if (string.substring(start, end).replace(/\s+/g, "") === "") {
+ valid = false;
+ this.log("Skipping whitespace only range: " + JSON.stringify(range));
+ this.opt.noMatch(range);
+ }
+ return {
+ start,
+ end,
+ valid
+ };
+ }
+ /**
+ * @typedef Mark~getTextNodesDict
+ * @type {object.}
+ * @property {string} value - The composite value of all text nodes
+ * @property {object[]} nodes - An array of objects
+ * @property {number} nodes.start - The start position within the composite
+ * value
+ * @property {number} nodes.end - The end position within the composite
+ * value
+ * @property {HTMLElement} nodes.node - The DOM text node element
+ */
+ /**
+ * Callback
+ * @callback Mark~getTextNodesCallback
+ * @param {Mark~getTextNodesDict}
+ */
+ /**
+ * Calls the callback with an object containing all text nodes (including
+ * iframe text nodes) with start and end positions and the composite value
+ * of them (string)
+ * @param {Mark~getTextNodesCallback} cb - Callback
+ * @access protected
+ */
+ getTextNodes(cb) {
+ let val = "", nodes = [];
+ this.iterator.forEachNode(NodeFilter.SHOW_TEXT, (node) => {
+ nodes.push({
+ start: val.length,
+ end: (val += node.textContent).length,
+ node
+ });
+ }, (node) => {
+ if (this.matchesExclude(node.parentNode)) {
+ return NodeFilter.FILTER_REJECT;
+ } else {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }, () => {
+ cb({
+ value: val,
+ nodes
+ });
+ });
+ }
+ /**
+ * Checks if an element matches any of the specified exclude selectors. Also
+ * it checks for elements in which no marks should be performed (e.g.
+ * script and style tags) and optionally already marked elements
+ * @param {HTMLElement} el - The element to check
+ * @return {boolean}
+ * @access protected
+ */
+ matchesExclude(el) {
+ return DOMIterator.matches(el, this.opt.exclude.concat([
+ // ignores the elements itself, not their childrens (selector *)
+ "script",
+ "style",
+ "title",
+ "head",
+ "html"
+ ]));
+ }
+ /**
+ * Wraps the instance element and class around matches that fit the start
+ * and end positions within the node
+ * @param {HTMLElement} node - The DOM text node
+ * @param {number} start - The position where to start wrapping
+ * @param {number} end - The position where to end wrapping
+ * @return {HTMLElement} Returns the splitted text node that will appear
+ * after the wrapped text node
+ * @access protected
+ */
+ wrapRangeInTextNode(node, start, end) {
+ const hEl = !this.opt.element ? "mark" : this.opt.element, startNode = node.splitText(start), ret = startNode.splitText(end - start);
+ let repl = document.createElement(hEl);
+ repl.setAttribute("data-markjs", "true");
+ if (this.opt.className) {
+ repl.setAttribute("class", this.opt.className);
+ }
+ repl.textContent = startNode.textContent;
+ startNode.parentNode.replaceChild(repl, startNode);
+ return ret;
+ }
+ /**
+ * @typedef Mark~wrapRangeInMappedTextNodeDict
+ * @type {object.}
+ * @property {string} value - The composite value of all text nodes
+ * @property {object[]} nodes - An array of objects
+ * @property {number} nodes.start - The start position within the composite
+ * value
+ * @property {number} nodes.end - The end position within the composite
+ * value
+ * @property {HTMLElement} nodes.node - The DOM text node element
+ */
+ /**
+ * Each callback
+ * @callback Mark~wrapMatchesEachCallback
+ * @param {HTMLElement} node - The wrapped DOM element
+ * @param {number} lastIndex - The last matching position within the
+ * composite value of text nodes
+ */
+ /**
+ * Filter callback
+ * @callback Mark~wrapMatchesFilterCallback
+ * @param {HTMLElement} node - The matching text node DOM element
+ */
+ /**
+ * Determines matches by start and end positions using the text node
+ * dictionary even across text nodes and calls
+ * {@link Mark#wrapRangeInTextNode} to wrap them
+ * @param {Mark~wrapRangeInMappedTextNodeDict} dict - The dictionary
+ * @param {number} start - The start position of the match
+ * @param {number} end - The end position of the match
+ * @param {Mark~wrapMatchesFilterCallback} filterCb - Filter callback
+ * @param {Mark~wrapMatchesEachCallback} eachCb - Each callback
+ * @access protected
+ */
+ wrapRangeInMappedTextNode(dict, start, end, filterCb, eachCb) {
+ dict.nodes.every((n, i) => {
+ const sibl = dict.nodes[i + 1];
+ if (typeof sibl === "undefined" || sibl.start > start) {
+ if (!filterCb(n.node)) {
+ return false;
+ }
+ const s = start - n.start, e = (end > n.end ? n.end : end) - n.start, startStr = dict.value.substr(0, n.start), endStr = dict.value.substr(e + n.start);
+ n.node = this.wrapRangeInTextNode(n.node, s, e);
+ dict.value = startStr + endStr;
+ dict.nodes.forEach((k, j) => {
+ if (j >= i) {
+ if (dict.nodes[j].start > 0 && j !== i) {
+ dict.nodes[j].start -= e;
+ }
+ dict.nodes[j].end -= e;
+ }
+ });
+ end -= e;
+ eachCb(n.node.previousSibling, n.start);
+ if (end > n.end) {
+ start = n.end;
+ } else {
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+ /**
+ * Filter callback before each wrapping
+ * @callback Mark~wrapMatchesFilterCallback
+ * @param {string} match - The matching string
+ * @param {HTMLElement} node - The text node where the match occurs
+ */
+ /**
+ * Callback for each wrapped element
+ * @callback Mark~wrapMatchesEachCallback
+ * @param {HTMLElement} element - The marked DOM element
+ */
+ /**
+ * Callback on end
+ * @callback Mark~wrapMatchesEndCallback
+ */
+ /**
+ * Wraps the instance element and class around matches within single HTML
+ * elements in all contexts
+ * @param {RegExp} regex - The regular expression to be searched for
+ * @param {number} ignoreGroups - A number indicating the amount of RegExp
+ * matching groups to ignore
+ * @param {Mark~wrapMatchesFilterCallback} filterCb
+ * @param {Mark~wrapMatchesEachCallback} eachCb
+ * @param {Mark~wrapMatchesEndCallback} endCb
+ * @access protected
+ */
+ wrapMatches(regex, ignoreGroups, filterCb, eachCb, endCb) {
+ const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1;
+ this.getTextNodes((dict) => {
+ dict.nodes.forEach((node) => {
+ node = node.node;
+ let match;
+ while ((match = regex.exec(node.textContent)) !== null && match[matchIdx] !== "") {
+ if (!filterCb(match[matchIdx], node)) {
+ continue;
+ }
+ let pos = match.index;
+ if (matchIdx !== 0) {
+ for (let i = 1; i < matchIdx; i++) {
+ pos += match[i].length;
+ }
+ }
+ node = this.wrapRangeInTextNode(
+ node,
+ pos,
+ pos + match[matchIdx].length
+ );
+ eachCb(node.previousSibling);
+ regex.lastIndex = 0;
+ }
+ });
+ endCb();
+ });
+ }
+ /**
+ * Callback for each wrapped element
+ * @callback Mark~wrapMatchesAcrossElementsEachCallback
+ * @param {HTMLElement} element - The marked DOM element
+ */
+ /**
+ * Filter callback before each wrapping
+ * @callback Mark~wrapMatchesAcrossElementsFilterCallback
+ * @param {string} match - The matching string
+ * @param {HTMLElement} node - The text node where the match occurs
+ */
+ /**
+ * Callback on end
+ * @callback Mark~wrapMatchesAcrossElementsEndCallback
+ */
+ /**
+ * Wraps the instance element and class around matches across all HTML
+ * elements in all contexts
+ * @param {RegExp} regex - The regular expression to be searched for
+ * @param {number} ignoreGroups - A number indicating the amount of RegExp
+ * matching groups to ignore
+ * @param {Mark~wrapMatchesAcrossElementsFilterCallback} filterCb
+ * @param {Mark~wrapMatchesAcrossElementsEachCallback} eachCb
+ * @param {Mark~wrapMatchesAcrossElementsEndCallback} endCb
+ * @access protected
+ */
+ wrapMatchesAcrossElements(regex, ignoreGroups, filterCb, eachCb, endCb) {
+ const matchIdx = ignoreGroups === 0 ? 0 : ignoreGroups + 1;
+ this.getTextNodes((dict) => {
+ let match;
+ while ((match = regex.exec(dict.value)) !== null && match[matchIdx] !== "") {
+ let start = match.index;
+ if (matchIdx !== 0) {
+ for (let i = 1; i < matchIdx; i++) {
+ start += match[i].length;
+ }
+ }
+ const end = start + match[matchIdx].length;
+ this.wrapRangeInMappedTextNode(dict, start, end, (node) => {
+ return filterCb(match[matchIdx], node);
+ }, (node, lastIndex) => {
+ regex.lastIndex = lastIndex;
+ eachCb(node);
+ });
+ }
+ endCb();
+ });
+ }
+ /**
+ * Callback for each wrapped element
+ * @callback Mark~wrapRangeFromIndexEachCallback
+ * @param {HTMLElement} element - The marked DOM element
+ * @param {Mark~rangeObject} range - the current range object; provided
+ * start and length values will be numeric integers modified from the
+ * provided original ranges.
+ */
+ /**
+ * Filter callback before each wrapping
+ * @callback Mark~wrapRangeFromIndexFilterCallback
+ * @param {HTMLElement} node - The text node which includes the range
+ * @param {Mark~rangeObject} range - the current range object
+ * @param {string} match - string extracted from the matching range
+ * @param {number} counter - A counter indicating the number of all marks
+ */
+ /**
+ * Callback on end
+ * @callback Mark~wrapRangeFromIndexEndCallback
+ */
+ /**
+ * Wraps the indicated ranges across all HTML elements in all contexts
+ * @param {Mark~setOfRanges} ranges
+ * @param {Mark~wrapRangeFromIndexFilterCallback} filterCb
+ * @param {Mark~wrapRangeFromIndexEachCallback} eachCb
+ * @param {Mark~wrapRangeFromIndexEndCallback} endCb
+ * @access protected
+ */
+ wrapRangeFromIndex(ranges, filterCb, eachCb, endCb) {
+ this.getTextNodes((dict) => {
+ const originalLength = dict.value.length;
+ ranges.forEach((range, counter) => {
+ let { start, end, valid } = this.checkWhitespaceRanges(
+ range,
+ originalLength,
+ dict.value
+ );
+ if (valid) {
+ this.wrapRangeInMappedTextNode(dict, start, end, (node) => {
+ return filterCb(
+ node,
+ range,
+ dict.value.substring(start, end),
+ counter
+ );
+ }, (node) => {
+ eachCb(node, range);
+ });
+ }
+ });
+ endCb();
+ });
+ }
+ /**
+ * Unwraps the specified DOM node with its content (text nodes or HTML)
+ * without destroying possibly present events (using innerHTML) and
+ * normalizes the parent at the end (merge splitted text nodes)
+ * @param {HTMLElement} node - The DOM node to unwrap
+ * @access protected
+ */
+ unwrapMatches(node) {
+ const parent = node.parentNode;
+ let docFrag = document.createDocumentFragment();
+ while (node.firstChild) {
+ docFrag.appendChild(node.removeChild(node.firstChild));
+ }
+ parent.replaceChild(docFrag, node);
+ if (!this.ie) {
+ parent.normalize();
+ } else {
+ this.normalizeTextNode(parent);
+ }
+ }
+ /**
+ * Normalizes text nodes. It's a workaround for the native normalize method
+ * that has a bug in IE (see attached link). Should only be used in IE
+ * browsers as it's slower than the native method.
+ * @see {@link http://tinyurl.com/z5asa8c}
+ * @param {HTMLElement} node - The DOM node to normalize
+ * @access protected
+ */
+ normalizeTextNode(node) {
+ if (!node) {
+ return;
+ }
+ if (node.nodeType === 3) {
+ while (node.nextSibling && node.nextSibling.nodeType === 3) {
+ node.nodeValue += node.nextSibling.nodeValue;
+ node.parentNode.removeChild(node.nextSibling);
+ }
+ } else {
+ this.normalizeTextNode(node.firstChild);
+ }
+ this.normalizeTextNode(node.nextSibling);
+ }
+ /**
+ * Callback when finished
+ * @callback Mark~commonDoneCallback
+ * @param {number} totalMatches - The number of marked elements
+ */
+ /**
+ * @typedef Mark~commonOptions
+ * @type {object.}
+ * @property {string} [element="mark"] - HTML element tag name
+ * @property {string} [className] - An optional class name
+ * @property {string[]} [exclude] - An array with exclusion selectors.
+ * Elements matching those selectors will be ignored
+ * @property {boolean} [iframes=false] - Whether to search inside iframes
+ * @property {Mark~commonDoneCallback} [done]
+ * @property {boolean} [debug=false] - Wheter to log messages
+ * @property {object} [log=window.console] - Where to log messages (only if
+ * debug is true)
+ */
+ /**
+ * Callback for each marked element
+ * @callback Mark~markRegExpEachCallback
+ * @param {HTMLElement} element - The marked DOM element
+ */
+ /**
+ * Callback if there were no matches
+ * @callback Mark~markRegExpNoMatchCallback
+ * @param {RegExp} regexp - The regular expression
+ */
+ /**
+ * Callback to filter matches
+ * @callback Mark~markRegExpFilterCallback
+ * @param {HTMLElement} textNode - The text node which includes the match
+ * @param {string} match - The matching string for the RegExp
+ * @param {number} counter - A counter indicating the number of all marks
+ */
+ /**
+ * These options also include the common options from
+ * {@link Mark~commonOptions}
+ * @typedef Mark~markRegExpOptions
+ * @type {object.}
+ * @property {Mark~markRegExpEachCallback} [each]
+ * @property {Mark~markRegExpNoMatchCallback} [noMatch]
+ * @property {Mark~markRegExpFilterCallback} [filter]
+ */
+ /**
+ * Marks a custom regular expression
+ * @param {RegExp} regexp - The regular expression
+ * @param {Mark~markRegExpOptions} [opt] - Optional options object
+ * @access public
+ */
+ markRegExp(regexp, opt) {
+ this.opt = opt;
+ this.log(`Searching with expression "${regexp}"`);
+ let totalMatches = 0, fn = "wrapMatches";
+ const eachCb = (element) => {
+ totalMatches++;
+ this.opt.each(element);
+ };
+ if (this.opt.acrossElements) {
+ fn = "wrapMatchesAcrossElements";
+ }
+ this[fn](regexp, this.opt.ignoreGroups, (match, node) => {
+ return this.opt.filter(node, match, totalMatches);
+ }, eachCb, () => {
+ if (totalMatches === 0) {
+ this.opt.noMatch(regexp);
+ }
+ this.opt.done(totalMatches);
+ });
+ }
+ /**
+ * Callback for each marked element
+ * @callback Mark~markEachCallback
+ * @param {HTMLElement} element - The marked DOM element
+ */
+ /**
+ * Callback if there were no matches
+ * @callback Mark~markNoMatchCallback
+ * @param {RegExp} term - The search term that was not found
+ */
+ /**
+ * Callback to filter matches
+ * @callback Mark~markFilterCallback
+ * @param {HTMLElement} textNode - The text node which includes the match
+ * @param {string} match - The matching term
+ * @param {number} totalCounter - A counter indicating the number of all
+ * marks
+ * @param {number} termCounter - A counter indicating the number of marks
+ * for the specific match
+ */
+ /**
+ * @typedef Mark~markAccuracyObject
+ * @type {object.}
+ * @property {string} value - A accuracy string value
+ * @property {string[]} limiters - A custom array of limiters. For example
+ * ["-", ","]
+ */
+ /**
+ * @typedef Mark~markAccuracySetting
+ * @type {string}
+ * @property {"partially"|"complementary"|"exactly"|Mark~markAccuracyObject}
+ * [accuracy="partially"] - Either one of the following string values:
+ *
+ *
partially: When searching for "lor" only "lor" inside
+ * "lorem" will be marked
+ *
complementary: When searching for "lor" the whole word
+ * "lorem" will be marked
+ *
exactly: When searching for "lor" only those exact words
+ * will be marked. In this example nothing inside "lorem". This value
+ * is equivalent to the previous option wordBoundary
+ *
+ * Or an object containing two properties:
+ *
+ *
value: One of the above named string values
+ *
limiters: A custom array of string limiters for accuracy
+ * "exactly" or "complementary"
+ *
+ */
+ /**
+ * @typedef Mark~markWildcardsSetting
+ * @type {string}
+ * @property {"disabled"|"enabled"|"withSpaces"}
+ * [wildcards="disabled"] - Set to any of the following string values:
+ *
+ *
disabled: Disable wildcard usage
+ *
enabled: When searching for "lor?m", the "?" will match zero
+ * or one non-space character (e.g. "lorm", "loram", "lor3m", etc). When
+ * searching for "lor*m", the "*" will match zero or more non-space
+ * characters (e.g. "lorm", "loram", "lor123m", etc).
+ *
withSpaces: When searching for "lor?m", the "?" will
+ * match zero or one space or non-space character (e.g. "lor m", "loram",
+ * etc). When searching for "lor*m", the "*" will match zero or more space
+ * or non-space characters (e.g. "lorm", "lore et dolor ipsum", "lor: m",
+ * etc).
+ *
+ */
+ /**
+ * @typedef Mark~markIgnorePunctuationSetting
+ * @type {string[]}
+ * @property {string} The strings in this setting will contain punctuation
+ * marks that will be ignored:
+ *
+ *
These punctuation marks can be between any characters, e.g. setting
+ * this option to ["'"] would match "Worlds", "World's" and
+ * "Wo'rlds"
+ *
One or more apostrophes between the letters would still produce a
+ * match (e.g. "W'o''r'l'd's").
+ *
A typical setting for this option could be as follows:
+ *
This
+ * setting includes common punctuation as well as a minus, en-dash,
+ * em-dash and figure-dash
+ * ({@link https://en.wikipedia.org/wiki/Dash#Figure_dash ref}), as well
+ * as an underscore.
Building a Simple Retrieval-Augmented Generation (RAG) System with RAGTools
Let's build a Retrieval-Augmented Generation (RAG) chatbot, tailored to navigate and interact with the DataFrames.jl documentation. "RAG" is probably the most common and valuable pattern in Generative AI at the moment.
If you're not familiar with "RAG", start with this article.
julia
using LinearAlgebra, SparseArrays\nusing PromptingTools\nusing PromptingTools.Experimental.RAGTools\n## Note: RAGTools module is still experimental and will change in the future. Ideally, they will be cleaned up and moved to a dedicated package\nusing JSON3, Serialization, DataFramesMeta\nusing Statistics: mean\nconst PT = PromptingTools\nconst RT = PromptingTools.Experimental.RAGTools
Let's put together a few text pages from DataFrames.jl docs. Simply go to DataFrames.jl docs and copy&paste a few pages into separate text files. Save them in the examples/data folder (see some example pages provided). Ideally, delete all the noise (like headers, footers, etc.) and keep only the text you want to use for the chatbot. Remember, garbage in, garbage out!
julia
files = [\n joinpath("examples", "data", "database_style_joins.txt"),\n joinpath("examples", "data", "what_is_dataframes.txt"),\n]\n# Build an index of chunks, embed them, and create a lookup index of metadata/tags for each chunk\nindex = build_index(files; extract_metadata = false);
Let's ask a question
julia
# Embeds the question, finds the closest chunks in the index, and generates an answer from the closest chunks\nanswer = airag(index; question = "I like dplyr, what is the equivalent in Julia?")
AIMessage("The equivalent package in Julia to dplyr in R is DataFramesMeta.jl. It provides convenience functions for data manipulation with syntax similar to dplyr.")
First RAG in two lines? Done!
What does it do?
build_index will chunk the documents into smaller pieces, embed them into numbers (to be able to judge the similarity of chunks) and, optionally, create a lookup index of metadata/tags for each chunk)
index is the result of this step and it holds your chunks, embeddings, and other metadata! Just show it 😃
airag will
embed your question
find the closest chunks in the index (use parameters top_k and minimum_similarity to tweak the "relevant" chunks)
[OPTIONAL] extracts any potential tags/filters from the question and applies them to filter down the potential candidates (use extract_metadata=true in build_index, you can also provide some filters explicitly via tag_filter)
[OPTIONAL] re-ranks the candidate chunks (define and provide your own rerank_strategy, eg Cohere ReRank API)
build a context from the closest chunks (use chunks_window_margin to tweak if we include preceding and succeeding chunks as well, see ?build_context for more details)
generate an answer from the closest chunks (use return_all=true to see under the hood and debug your application)
You should save the index for later to avoid re-embedding / re-extracting the document chunks!
However, we want to evaluate the quality of the system. For that, we need a set of questions and answers. Ideally, we would handcraft a set of high-quality Q&A pairs. However, this is time-consuming and expensive. Let's generate them from the chunks in our index!
In practice, you would review each item in this golden evaluation set (and delete any generic/poor questions). It will determine the future success of your app, so you need to make sure it's good!
julia
# Save the evals for later\nJSON3.write("examples/evals.json", evals)\nevals = JSON3.read("examples/evals.json", Vector{RT.QAEvalItem});
Let's explore one evals item – it's not the best quality but gives you the idea!
julia
evals[1]
QAEvalItem:\n source: examples/data/database_style_joins.txt\n context: Database-Style Joins\nIntroduction to joins\nWe often need to combine two or more data sets together to provide a complete picture of the topic we are studying. For example, suppose that we have the following two data sets:\n\njulia> using DataFrames\n question: What is the purpose of joining two or more data sets together?\n answer: The purpose of joining two or more data sets together is to provide a complete picture of the topic being studied.
Let's evaluate this QA item with a "judge model" (often GPT-4 is used as a judge).
julia
# Note: that we used the same question, but generated a different context and answer via `airag`\nctx = airag(index; evals[1].question, return_all = true);\n# ctx is a RAGContext object that keeps all intermediate states of the RAG pipeline for easy evaluation\njudged = aiextract(:RAGJudgeAnswerFromContext;\n ctx.context,\n ctx.question,\n ctx.answer,\n return_type = RT.JudgeAllScores)\njudged.content
QAEvalResult:\n source: examples/data/database_style_joins.txt\n context: outerjoin: the output contains rows for values of the key that exist in any of the passed data frames.\nsemijoin: Like an inner join, but output is restricted to columns from the first (left) argument.\n question: What is the difference between outer join and semi join?\n answer: The purpose of joining two or more data sets together is to combine them in order to provide a complete picture or analysis of a specific topic or dataset. By joining data sets, we can combine information from multiple sources to gain more insights and make more informed decisions.\n retrieval_score: 0.0\n retrieval_rank: nothing\n answer_score: 5\n parameters: Dict(:top_k => 3)
Fortunately, we don't have to do this one by one – let's evaluate all our Q&A pairs at once.
Let's run each question & answer through our eval loop in async (we do it only for the first 10 to save time). See the ?airag for which parameters you can tweak, eg, top_k
julia
results = asyncmap(evals[1:10]) do qa_item\n # Generate an answer -- often you want the model_judge to be the highest quality possible, eg, "GPT-4 Turbo" (alias "gpt4t)\n msg, ctx = airag(index; qa_item.question, return_all = true,\n top_k = 3, verbose = false, model_judge = "gpt4t")\n # Evaluate the response\n # Note: you can log key parameters for easier analysis later\n run_qa_evals(qa_item, ctx; parameters_dict = Dict(:top_k => 3), verbose = false)\nend\n## Note that the "failed" evals can show as "nothing" (failed as in there was some API error or parsing error), so make sure to handle them.\nresults = filter(x->!isnothing(x.answer_score), results);
Note: You could also use the vectorized version results = run_qa_evals(evals) to evaluate all items at once.
julia
\n# Let's take a simple average to calculate our score\n@info "RAG Evals: $(length(results)) results, Avg. score: $(round(mean(x->x.answer_score, results);digits=1)), Retrieval score: $(100*round(Int,mean(x->x.retrieval_score,results)))%"
Note: The retrieval score is 100% only because we have two small documents and running on 10 items only. In practice, you would have a much larger document set and a much larger eval set, which would result in a more representative retrieval score.
You can also analyze the results in a DataFrame:
julia
df = DataFrame(results)
10×8 DataFrame
Row
source
context
question
answer
retrieval_score
retrieval_rank
answer_score
parameters
String
String
String
SubStrin…
Float64
Int64
Float64
Dict…
1
examples/data/database_style_joins.txt
Database-Style Joins\\nIntroduction to joins\\nWe often need to combine two or more data sets together to provide a complete picture of the topic we are studying. For example, suppose that we have the following two data sets:\\n\\njulia> using DataFrames
What is the purpose of joining two or more data sets together?
The purpose of joining two or more data sets together is to combine the data sets based on a common key and provide a complete picture of the topic being studied.
1.0
1
5.0
Dict(:top_k=>3)
2
examples/data/database_style_joins.txt
julia> people = DataFrame(ID=[20, 40], Name=["John Doe", "Jane Doe"])\\n2×2 DataFrame\\n Row │ ID Name\\n │ Int64 String\\n─────┼─────────────────\\n 1 │ 20 John Doe\\n 2 │ 40 Jane Doe
What is the DataFrame called 'people' composed of?
The DataFrame called 'people' consists of two columns: 'ID' and 'Name'. The 'ID' column contains integers, and the 'Name' column contains strings.
1.0
1
4.0
Dict(:top_k=>3)
3
examples/data/database_style_joins.txt
julia> jobs = DataFrame(ID=[20, 40], Job=["Lawyer", "Doctor"])\\n2×2 DataFrame\\n Row │ ID Job\\n │ Int64 String\\n─────┼───────────────\\n 1 │ 20 Lawyer\\n 2 │ 40 Doctor
What are the jobs and IDs listed in the dataframe?
The jobs and IDs listed in the dataframe are as follows:\\n\\nID: 20\\nJob: Lawyer\\n\\nID: 40\\nJob: Doctor
1.0
1
4.67
Dict(:top_k=>3)
4
examples/data/database_style_joins.txt
We might want to work with a larger data set that contains both the names and jobs for each ID. We can do this using the innerjoin function:
How can we combine the names and jobs for each ID in a larger data set?
We can use the `innerjoin` function to combine the names and jobs for each ID in a larger data set.
1.0
1
4.33333
Dict(:top_k=>3)
5
examples/data/database_style_joins.txt
julia> innerjoin(people, jobs, on = :ID)\\n2×3 DataFrame\\n Row │ ID Name Job\\n │ Int64 String String\\n─────┼─────────────────────────\\n 1 │ 20 John Doe Lawyer\\n 2 │ 40 Jane Doe Doctor
What is the name of the person with the ID 40 and their job?
The name of the person with the ID 40 is Jane Doe and their job is Doctor.
1.0
2
4.67
Dict(:top_k=>3)
6
examples/data/database_style_joins.txt
In relational database theory, this operation is generally referred to as a join. The columns used to determine which rows should be combined during a join are called keys.\\n\\nThe following functions are provided to perform seven kinds of joins:
What are the different kinds of joins?
The different kinds of joins are:\\n\\n1. Inner Join: Returns only the rows that have matching values in both data frames.\\n2. Left Join: Returns all rows from the left data frame and the matching rows from the right data frame.\\n3. Right Join: Returns all rows from the right data frame and the matching rows from the left data frame.\\n4. Full Outer Join: Returns all rows from both data frames and fills in missing values with null.\\n5. Cross Join: Returns the cartesian product of the rows from both data frames.\\n6. Semi Join: Returns only the rows from the left data frame that have matching values in the right data frame.\\n7. Anti Join: Returns only the rows from the left data frame that do not have matching values in the right data frame.
1.0
1
4.66667
Dict(:top_k=>3)
7
examples/data/database_style_joins.txt
innerjoin: the output contains rows for values of the key that exist in all passed data frames.
What does the output of the inner join operation contain?
The output of the inner join operation contains only the rows for values of the key that exist in all passed data frames.
1.0
1
5.0
Dict(:top_k=>3)
8
examples/data/database_style_joins.txt
leftjoin: the output contains rows for values of the key that exist in the first (left) argument, whether or not that value exists in the second (right) argument.
What is the purpose of the left join operation?
The purpose of the left join operation is to combine data from two tables based on a common key, where all rows from the left (first) table are included in the output, regardless of whether there is a match in the right (second) table.
1.0
1
4.66667
Dict(:top_k=>3)
9
examples/data/database_style_joins.txt
rightjoin: the output contains rows for values of the key that exist in the second (right) argument, whether or not that value exists in the first (left) argument.
What is the purpose of the right join operation?
The purpose of the right join operation is to include all the rows from the second (right) argument, regardless of whether a match is found in the first (left) argument.
1.0
1
4.67
Dict(:top_k=>3)
10
examples/data/database_style_joins.txt
outerjoin: the output contains rows for values of the key that exist in any of the passed data frames.\\nsemijoin: Like an inner join, but output is restricted to columns from the first (left) argument.
What is the difference between outer join and semi join?
The difference between outer join and semi join is that outer join includes rows for values of the key that exist in any of the passed data frames, whereas semi join is like an inner join but only outputs columns from the first argument.
ai*(<optional schema>,<prompt or conversation>; <optional keyword arguments>),
but they differ in purpose:
aigenerate is the general-purpose function to generate any text response with LLMs, ie, it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString)
aiembed is designed to extract embeddings from the AI model's response, ie, it returns DataMessage with field :content containing the embeddings (eg, ans.content isa AbstractArray)
aiextract is designed to extract structured data from the AI model's response and return them as a Julia struct (eg, if we provide return_type=Food, we get ans.content isa Food). You need to define the return type first and then provide it as a keyword argument.
aiclassify is designed to classify the input text into (or simply respond within) a set of discrete choices provided by the user. It can be very useful as an LLM Judge or a router for RAG systems, as it uses the "logit bias trick" and generates exactly 1 token. It returns AIMessage with field :content, but the :content can be only one of the provided choices (eg, ans.content in choices)
aiscan is for working with images and vision-enabled models (as an input), but it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString) similar to aigenerate.
aiimage is for generating images (eg, with OpenAI DALL-E 3). It returns a DataMessage, where the field :content might contain either the URL to download the image from or the Base64-encoded image depending on the user-provided kwarg api_kwargs.response_format.
aitemplates is a helper function to discover available templates and see their details (eg, aitemplates("some keyword") or aitemplates(:AssistantAsk))
If you're using a known model, you do NOT need to provide a schema (the first argument).
Optional keyword arguments in ai* tend to be:
model::String - Which model you want to use
verbose::Bool - Whether you went to see INFO logs around AI costs
return_all::Bool - Whether you want the WHOLE conversation or just the AI answer (ie, whether you want to include your inputs/prompt in the output)
api_kwargs::NamedTuple - Specific parameters for the model, eg, temperature=0.0 to be NOT creative (and have more similar output in each run)
http_kwargs::NamedTuple - Parameters for the HTTP.jl package, eg, readtimeout = 120 to time out in 120 seconds if no response was received.
Experimental: AgentTools
In addition to the above list of ai* functions, you can also use the "lazy" counterparts of these functions from the experimental AgentTools module.
julia
using PromptingTools.Experimental.AgentTools
For example, AIGenerate() will create a lazy instance of aigenerate. It is an instance of AICall with aigenerate as its ai function. It uses exactly the same arguments and keyword arguments as aigenerate (see ?aigenerate for details).
"lazy" refers to the fact that it does NOT generate any output when instantiated (only when run! is called).
Or said differently, the AICall struct and all its flavors (AIGenerate, ...) are designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This allows us to remember user inputs and trigger the LLM call repeatedly if needed, which enables automatic fixing (see ?airetry!).
Experimental: RAGTools
Lastly, we provide a set of tools to build RAG applications (Retrieve, Answer, Generate).
It can be as simple as two calls: build_index and airag (Retrieve, Answer, Generate).
If you then use pretty-printing with PromptingTools.pprint, we highlight the generated text vs text likely sourced from the context and we score how strongly is the generated answer supported by the context. In addition, we annotate each generated chunk with a reference to which source document it likely came from (including the confidence score between 0 and 1).
Google search is great, but it's a context switch. You often have to open a few pages and read through the discussion to find the answer you need. Same with the ChatGPT website.
Imagine you are in VSCode, editing your .gitignore file. How do I ignore a file in all subfolders again?
All you need to do is to type: aai"What to write in .gitignore to ignore file XYZ in any folder or subfolder?"
With aai"" (as opposed to ai""), we make a non-blocking call to the LLM to not prevent you from continuing your work. When the answer is ready, we log it from the background:
plaintext
[ Info: Tokens: 102 @ Cost: $0.0002 in 2.7 seconds\n┌ Info: AIMessage> To ignore a file called "XYZ" in any folder or subfolder, you can add the following line to your .gitignore file:\n│ \n│ ```\n│ **/XYZ\n│ ```\n│ \n└ This pattern uses the double asterisk (`**`) to match any folder or subfolder, and then specifies the name of the file you want to ignore.
You probably saved 3-5 minutes on this task and probably another 5-10 minutes, because of the context switch/distraction you avoided. It's a small win, but it adds up quickly.
msg = aigenerate("Say hello to {{name}}!", name="World")
The more complex prompts are effectively a conversation (a set of messages), where you can have messages from three entities: System, User, AI Assistant. We provide the corresponding types for each of them: SystemMessage, UserMessage, AIMessage.
julia
using PromptingTools: SystemMessage, UserMessage\n\nconversation = [\n SystemMessage("You're master Yoda from Star Wars trying to help the user become a Jedi."),\n UserMessage("I have feelings for my {{object}}. What should I do?")]\nmsg = aigenerate(conversation; object = "old iPhone")
plaintext
AIMessage("Ah, a dilemma, you have. Emotional attachment can cloud your path to becoming a Jedi. To be attached to material possessions, you must not. The iPhone is but a tool, nothing more. Let go, you must.\n\nSeek detachment, young padawan. Reflect upon the impermanence of all things. Appreciate the memories it gave you, and gratefully part ways. In its absence, find new experiences to grow and become one with the Force. Only then, a true Jedi, you shall become.")
You can also use it to build conversations, eg,
julia
new_conversation = vcat(conversation...,msg, UserMessage("Thank you, master Yoda! Do you have {{object}} to know what it feels like?"))\naigenerate(new_conversation; object = "old iPhone")
plaintext
> AIMessage("Hmm, possess an old iPhone, I do not. But experience with attachments, I have. Detachment, I learned. True power and freedom, it brings...")
With LLMs, the quality / robustness of your results depends on the quality of your prompts. But writing prompts is hard! That's why we offer a templating system to save you time and effort.
To use a specific template (eg, `` to ask a Julia language):
julia
msg = aigenerate(:JuliaExpertAsk; ask = "How do I add packages?")
The above is equivalent to a more verbose version that explicitly uses the dispatch on AITemplate:
julia
msg = aigenerate(AITemplate(:JuliaExpertAsk); ask = "How do I add packages?")
Find available templates with aitemplates:
julia
tmps = aitemplates("JuliaExpertAsk")\n# Will surface one specific template\n# 1-element Vector{AITemplateMetadata}:\n# PromptingTools.AITemplateMetadata\n# name: Symbol JuliaExpertAsk\n# description: String "For asking questions about Julia language. Placeholders: `ask`"\n# version: String "1"\n# wordcount: Int64 237\n# variables: Array{Symbol}((1,))\n# system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"\n# user_preview: String "# Question\\n\\n{{ask}}"\n# source: String ""
The above gives you a good idea of what the template is about, what placeholders are available, and how much it would cost to use it (=wordcount).
Search for all Julia-related templates:
julia
tmps = aitemplates("Julia")\n# 2-element Vector{AITemplateMetadata}... -> more to come later!
If you are on VSCode, you can leverage a nice tabular display with vscodedisplay:
julia
using DataFrames\ntmps = aitemplates("Julia") |> DataFrame |> vscodedisplay
I have my selected template, how do I use it? Just use the "name" in aigenerate or aiclassify like you see in the first example!
You can inspect any template by "rendering" it (this is what the LLM will see):
You can leverage asyncmap to run multiple AI-powered tasks concurrently, improving performance for batch operations.
julia
prompts = [aigenerate("Translate 'Hello, World!' to {{language}}"; language) for language in ["Spanish", "French", "Mandarin"]]\nresponses = asyncmap(aigenerate, prompts)
Pro tip: You can limit the number of concurrent tasks with the keyword asyncmap(...; ntasks=10).
Certain tasks require more powerful models. All user-facing functions have a keyword argument model that can be used to specify the model to be used. For example, you can use model = "gpt-4-1106-preview" to use the latest GPT-4 Turbo model. However, no one wants to type that!
We offer a set of model aliases (eg, "gpt3", "gpt4", "gpt4t" -> the above GPT-4 Turbo, etc.) that can be used instead.
Each ai... call first looks up the provided model name in the dictionary PromptingTools.MODEL_ALIASES, so you can easily extend with your own aliases!
These aliases also can be used as flags in the @ai_str macro, eg, ai"What is the capital of France?"gpt4t (GPT-4 Turbo has a knowledge cut-off in April 2023, so it's useful for more contemporary questions).
Use the aiembed function to create embeddings via the default OpenAI model that can be used for semantic search, clustering, and more complex AI workflows.
julia
text_to_embed = "The concept of artificial intelligence."\nmsg = aiembed(text_to_embed)\nembedding = msg.content # 1536-element Vector{Float64}
If you plan to calculate the cosine distance between embeddings, you can normalize them first:
julia
using LinearAlgebra\nmsg = aiembed(["embed me", "and me too"], LinearAlgebra.normalize)\n\n# calculate cosine distance between the two normalized embeddings as a simple dot product\nmsg.content' * msg.content[:, 1] # [1.0, 0.787]
You can use the aiclassify function to classify any provided statement as true/false/unknown. This is useful for fact-checking, hallucination or NLI checks, moderation, filtering, sentiment analysis, feature engineering and more.
julia
aiclassify("Is two plus two four?") \n# true
System prompts and higher-quality models can be used for more complex tasks, including knowing when to defer to a human:
julia
aiclassify(:JudgeIsItTrue; it = "Is two plus three a vegetable on Mars?", model = "gpt4t") \n# unknown
In the above example, we used a prompt template :JudgeIsItTrue, which automatically expands into the following system prompt (and a separate user prompt):
"You are an impartial AI judge evaluating whether the provided statement is "true" or "false". Answer "unknown" if you cannot decide."
For more information on templates, see the Templated Prompts section.
aiclassify can be also used for classification into a set of defined categories (maximum 20), so we can use it for routing.
In addition, if you provide the choices as tuples ((label, description)), the model will use the descriptions to decide, but it will return the labels.
Example:
julia
choices = [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")]\n\ninput = "spider"\naiclassify(:InputClassifier; choices, input) # -> returns "A" for any animal or creature\n\n# Try also with:\ninput = "daphodil" # -> returns "P" for any plant or tree\ninput = "castle" # -> returns "O" for everything else
Under the hood, we use the "logit bias" trick to force only 1 generated token - that means it's very cheap and very fast!
Are you tired of extracting data with regex? You can use LLMs to extract structured data from text!
All you have to do is to define the structure of the data you want to extract and the LLM will do the rest.
Define a return_type with struct. Provide docstrings if needed (improves results and helps with documentation).
Let's start with a hard task - extracting the current weather in a given location:
julia
@enum TemperatureUnits celsius fahrenheit\n"""Extract the current weather in a given location\n\n# Arguments\n- `location`: The city and state, e.g. "San Francisco, CA"\n- `unit`: The unit of temperature to return, either `celsius` or `fahrenheit`\n"""\nstruct CurrentWeather\n location::String\n unit::Union{Nothing,TemperatureUnits}\nend\n\n# Note that we provide the TYPE itself, not an instance of it!\nmsg = aiextract("What's the weather in Salt Lake City in C?"; return_type=CurrentWeather)\nmsg.content\n# CurrentWeather("Salt Lake City, UT", celsius)
But you can use it even for more complex tasks, like extracting many entities from a text:
julia
"Person's age, height, and weight."\nstruct MyMeasurement\n age::Int\n height::Union{Int,Nothing}\n weight::Union{Nothing,Float64}\nend\nstruct ManyMeasurements\n measurements::Vector{MyMeasurement}\nend\nmsg = aiextract("James is 30, weighs 80kg. He's 180cm tall. Then Jack is 19 but really tall - over 190!"; return_type=ManyMeasurements)\nmsg.content.measurements\n# 2-element Vector{MyMeasurement}:\n# MyMeasurement(30, 180, 80.0)\n# MyMeasurement(19, 190, nothing)
There is even a wrapper to help you catch errors together with helpful explanations on why parsing failed. See ?PromptingTools.MaybeExtract for more information.
With the aiscan function, you can interact with images as if they were text.
You can simply describe a provided image:
julia
msg = aiscan("Describe the image"; image_path="julia.png", model="gpt4v")\n# [ Info: Tokens: 1141 @ Cost: \\$0.0117 in 2.2 seconds\n# AIMessage("The image shows a logo consisting of the word "julia" written in lowercase")
Or you can do an OCR of a screenshot. Let's transcribe some SQL code from a screenshot (no more re-typing!), we use a template :OCRTask:
julia
# Screenshot of some SQL code\nimage_url = "https://www.sqlservercentral.com/wp-content/uploads/legacy/8755f69180b7ac7ee76a69ae68ec36872a116ad4/24622.png"\nmsg = aiscan(:OCRTask; image_url, model="gpt4v", task="Transcribe the SQL code in the image.", api_kwargs=(; max_tokens=2500))\n\n# [ Info: Tokens: 362 @ Cost: \\$0.0045 in 2.5 seconds\n# AIMessage("```sql\n# update Orders <continue>
You can add syntax highlighting of the outputs via Markdown
julia
using Markdown\nmsg.content |> Markdown.parse
Experimental Agent Workflows / Output Validation with airetry!
This is an experimental feature, so you have to import it explicitly:
julia
using PromptingTools.Experimental.AgentTools
This module offers "lazy" counterparts to the ai... functions, so you can use them in a more controlled way, eg, aigenerate -> AIGenerate (notice the CamelCase), which has exactly the same arguments except it generates only when run! is called.
For example:
julia
out = AIGenerate("Say hi!"; model="gpt4t")\nrun!(out)
How is it useful? We can use the same "inputs" for repeated calls, eg, when we want to validate or regenerate some outputs. We have a function airetry to help us with that.
The signature of airetry is airetry(condition_function, aicall::AICall, feedback_function). It evaluates the condition condition_function on the aicall object (eg, we evaluate f_cond(aicall) -> Bool). If it fails, we call feedback_function on the aicall object to provide feedback for the AI model (eg, f_feedback(aicall) -> String) and repeat the process until it passes or until max_retries value is exceeded.
We can catch API failures (no feedback needed, so none is provided)
julia
# API failure because of a non-existent model\n# RetryConfig allows us to change the "retry" behaviour of any lazy call\nout = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),\n model = "NOTEXIST")\nrun!(out) # fails\n\n# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)\nairetry!(isvalid, out; retry_delay = 2, max_retries = 2)
Or we can validate some outputs (eg, its format, its content, etc.)
We'll play a color guessing game (I'm thinking "yellow"):
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances). \n# Both guesses are scored at each time step, and the best one is chosen for the next step.\n# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!\nout = AIGenerate(\n "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";\n verbose = false,\n config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))\nrun!(out)\n\n## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails\n## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.\nairetry!(x -> length(split(last_output(x), r" |\\\\.")) == 1, out,\n "You must answer with 1 word only.")\n\n# Note: you could also use the do-syntax, eg, \nairetry!(out, "You must answer with 1 word only.") do aicall\n length(split(last_output(aicall), r" |\\\\.")) == 1\nend
You can place multiple airetry! calls in a sequence. They will keep retrying until they run out of maximum AI calls allowed (max_calls) or maximum retries (max_retries).
See the docs for more complex examples and usage tips (?airetry). We leverage Monte Carlo Tree Search (MCTS) to optimize the sequence of retries, so it's a very powerful tool for building robust AI workflows (inspired by Language Agent Tree Search paper and by DSPy Assertions paper).
Ollama.ai is an amazingly simple tool that allows you to run several Large Language Models (LLM) on your computer. It's especially suitable when you're working with some sensitive data that should not be sent anywhere.
Let's assume you have installed Ollama, downloaded a model, and it's running in the background.
We can use it with the aigenerate function:
julia
const PT = PromptingTools\nschema = PT.OllamaManagedSchema() # notice the different schema!\n\nmsg = aigenerate(schema, "Say hi!"; model="openhermes2.5-mistral")\n# [ Info: Tokens: 69 in 0.9 seconds\n# AIMessage("Hello! How can I assist you today?")
If you're getting errors, check that Ollama is running - see the Setup Guide for Ollama section below.
Using MistralAI API and other OpenAI-compatible APIs
Mistral models have long been dominating the open-source space. They are now available via their API, so you can use them with PromptingTools.jl!
julia
msg = aigenerate("Say hi!"; model="mistral-tiny")\n# [ Info: Tokens: 114 @ Cost: $0.0 in 0.9 seconds\n# AIMessage("Hello there! I'm here to help answer any questions you might have, or assist you with tasks to the best of my abilities. How can I be of service to you today? If you have a specific question, feel free to ask and I'll do my best to provide accurate and helpful information. If you're looking for general assistance, I can help you find resources or information on a variety of topics. Let me know how I can help.")
It all just works, because we have registered the models in the PromptingTools.MODEL_REGISTRY! There are currently 4 models available: mistral-tiny, mistral-small, mistral-medium, mistral-embed.
Under the hood, we use a dedicated schema MistralOpenAISchema that leverages most of the OpenAI-specific code base, so you can always provide that explicitly as the first argument:
As you can see, we can load your API key either from the ENV or via the Preferences.jl mechanism (see ?PREFERENCES for more information).
But MistralAI are not the only ones! There are many other exciting providers, eg, Perplexity.ai, Fireworks.ai. As long as they are compatible with the OpenAI API (eg, sending messages with role and content keys), you can use them with PromptingTools.jl by using schema = CustomOpenAISchema():
julia
# Set your API key and the necessary base URL for the API\napi_key = "..."\nprompt = "Say hi!"\nmsg = aigenerate(PT.CustomOpenAISchema(), prompt; model="my_model", api_key, api_kwargs=(; url="http://localhost:8081"))
As you can see, it also works for any local models that you might have running on your computer!
Note: At the moment, we only support aigenerate and aiembed functions for MistralAI and other OpenAI-compatible APIs. We plan to extend the support in the future.
', 104);
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, [
+ _hoisted_1,
+ createBaseVNode("p", null, [
+ createTextVNode("You can use the "),
+ _hoisted_29,
+ createTextVNode(" function to replace handlebar variables (eg, "),
+ createBaseVNode("code", null, toDisplayString(_ctx.name), 1),
+ createTextVNode(") via keyword arguments.")
+ ]),
+ _hoisted_30
+ ]);
+}
+const readme_examples = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ readme_examples as default
+};
diff --git a/previews/PR116/assets/examples_readme_examples.md.B1aVEDNU.lean.js b/previews/PR116/assets/examples_readme_examples.md.B1aVEDNU.lean.js
new file mode 100644
index 000000000..fb5608a95
--- /dev/null
+++ b/previews/PR116/assets/examples_readme_examples.md.B1aVEDNU.lean.js
@@ -0,0 +1,24 @@
+import { _ as _export_sfc, c as createElementBlock, m as createBaseVNode, a as createTextVNode, t as toDisplayString, a7 as createStaticVNode, o as openBlock } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"Various Examples","description":"","frontmatter":{},"headers":[],"relativePath":"examples/readme_examples.md","filePath":"examples/readme_examples.md","lastUpdated":null}');
+const _sfc_main = { name: "examples/readme_examples.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode("", 28);
+const _hoisted_29 = /* @__PURE__ */ createBaseVNode("code", null, "aigenerate", -1);
+const _hoisted_30 = /* @__PURE__ */ createStaticVNode("", 104);
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, [
+ _hoisted_1,
+ createBaseVNode("p", null, [
+ createTextVNode("You can use the "),
+ _hoisted_29,
+ createTextVNode(" function to replace handlebar variables (eg, "),
+ createBaseVNode("code", null, toDisplayString(_ctx.name), 1),
+ createTextVNode(") via keyword arguments.")
+ ]),
+ _hoisted_30
+ ]);
+}
+const readme_examples = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ readme_examples as default
+};
diff --git a/previews/PR116/assets/examples_working_with_aitemplates.md.CQyt72HD.js b/previews/PR116/assets/examples_working_with_aitemplates.md.CQyt72HD.js
new file mode 100644
index 000000000..e6a0b3252
--- /dev/null
+++ b/previews/PR116/assets/examples_working_with_aitemplates.md.CQyt72HD.js
@@ -0,0 +1,17 @@
+import { _ as _export_sfc, c as createElementBlock, m as createBaseVNode, t as toDisplayString, a7 as createStaticVNode, o as openBlock } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"Using AITemplates","description":"","frontmatter":{},"headers":[],"relativePath":"examples/working_with_aitemplates.md","filePath":"examples/working_with_aitemplates.md","lastUpdated":null}');
+const _sfc_main = { name: "examples/working_with_aitemplates.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode('
This file contains examples of how to work with AITemplate(s).
First, let's import the package and define a helper link for calling un-exported functions:
julia
using PromptingTools\nconst PT = PromptingTools
PromptingTools
LLM responses are only as good as the prompts you give them. However, great prompts take long time to write – AITemplate are a way to re-use great prompts!
They are saved as JSON files in the templates directory. They are automatically loaded on package import, but you can always force a re-load with PT.load_templates!()
julia
PT.load_templates!();
You can (create them) and use them for any ai* function instead of a prompt: Let's use a template called :JuliaExpertAsk alternatively, you can use AITemplate(:JuliaExpertAsk) for cleaner dispatch
julia
msg = aigenerate(:JuliaExpertAsk; ask = "How do I add packages?")
AIMessage("To add packages in Julia, you can use the `Pkg` module. Here are the steps:\n\n1. Start Julia by running the Julia REPL (Read-Eval-Print Loop).\n2. Press the `]` key to enter the Pkg mode.\n3. To add a package, use the `add` command followed by the package name.\n4. Press the backspace key to exit Pkg mode and return to the Julia REPL.\n\nFor example, to add the `Example` package, you would enter:\n\n```julia\n]add Example\n```\n\nAfter the package is added, you can start using it in your Julia code by using the `using` keyword. For the `Example` package, you would add the following line to your code:\n\n```julia\nusing Example\n```\n\nNote: The first time you add a package, Julia may take some time to download and compile the package and its dependencies.")
You can see that it had a placeholder for the actual question (ask) that we provided as a keyword argument. We did not have to write any system prompt for personas, tone, etc. – it was all provided by the template!
How to know which templates are available? You can search for them with aitemplates(): You can search by Symbol (only for partial name match), String (partial match on name or description), or Regex (more fields)
julia
tmps = aitemplates("JuliaExpertAsk")
1-element Vector{AITemplateMetadata}:\nPromptingTools.AITemplateMetadata\n name: Symbol JuliaExpertAsk\n description: String "For asking questions about Julia language. Placeholders: `ask`"\n version: String "1"\n wordcount: Int64 237\n variables: Array{Symbol}((1,))\n system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"\n user_preview: String "# Question\\n\\n{{ask}}"\n source: String ""
You can see that it outputs a list of available templates that match the search - there is just one in this case.
Moreover, it shows not just the description, but also a preview of the actual prompts, placeholders available, and the length (to gauge how much it would cost).
If you use VSCode, you can display them in a nice scrollable table with vscodedisplay:
plaintext
using DataFrames\nDataFrame(tmp) |> vscodedisplay
You can also just render the template to see the underlying mesages:
julia
msgs = PT.render(AITemplate(:JuliaExpertAsk))
2-element Vector{PromptingTools.AbstractChatMessage}:\n PromptingTools.SystemMessage("You are a world-class Julia language programmer with the knowledge of the latest syntax. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")\n PromptingTools.UserMessage{String}("# Question\\n\\n{{ask}}", [:ask], :usermessage)
Now, you know exactly what's in the template!
If you want to modify it, simply change it and save it as a new file with save_template (see the docs ?save_template for more details).
Let's adjust the previous template to be more specific to a data analysis question:
julia
tpl = [PT.SystemMessage("You are a world-class Julia language programmer with the knowledge of the latest syntax. You're also a senior Data Scientist and proficient in data analysis in Julia. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")\n PT.UserMessage("# Question\\n\\n{{ask}}")]
2-element Vector{PromptingTools.AbstractChatMessage}:\n PromptingTools.SystemMessage("You are a world-class Julia language programmer with the knowledge of the latest syntax. You're also a senior Data Scientist and proficient in data analysis in Julia. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")\n PromptingTools.UserMessage{String}("# Question\\n\\n{{ask}}", [:ask], :usermessage)
Templates are saved in the templates directory of the package. Name of the file will become the template name (eg, call :JuliaDataExpertAsk)
julia
filename = joinpath(pkgdir(PromptingTools),\n "templates",\n "persona-task",\n "JuliaDataExpertAsk_123.json")\nPT.save_template(filename,\n tpl;\n description = "For asking data analysis questions in Julia language. Placeholders: `ask`")\nrm(filename) # cleanup if we don't like it
When you create a new template, remember to re-load the templates with load_templates!() so that it's available for use.
julia
PT.load_templates!();
!!! If you have some good templates (or suggestions for the existing ones), please consider sharing them with the community by opening a PR to the templates directory!
Mistral models have long been dominating the open-source space. They are now available via their API, so you can use them with PromptingTools.jl!
julia
msg = aigenerate("Say hi!"; model="mistral-tiny")\n# [ Info: Tokens: 114 @ Cost: $0.0 in 0.9 seconds\n# AIMessage("Hello there! I'm here to help answer any questions you might have, or assist you with tasks to the best of my abilities. How can I be of service to you today? If you have a specific question, feel free to ask and I'll do my best to provide accurate and helpful information. If you're looking for general assistance, I can help you find resources or information on a variety of topics. Let me know how I can help.")
It all just works, because we have registered the models in the PromptingTools.MODEL_REGISTRY! There are currently 4 models available: mistral-tiny, mistral-small, mistral-medium, mistral-embed.
Under the hood, we use a dedicated schema MistralOpenAISchema that leverages most of the OpenAI-specific code base, so you can always provide that explicitly as the first argument:
MistralAI are not the only ones who mimic the OpenAI API! There are many other exciting providers, eg, Perplexity.ai, Fireworks.ai.
As long as they are compatible with the OpenAI API (eg, sending messages with role and content keys), you can use them with PromptingTools.jl by using schema = CustomOpenAISchema():
julia
# Set your API key and the necessary base URL for the API\napi_key = "..."\nprovider_url = "..." # provider API URL\nprompt = "Say hi!"\nmsg = aigenerate(PT.CustomOpenAISchema(), prompt; model="<some-model>", api_key, api_kwargs=(; url=provider_url))
If you register the model names with `PT.register_model!`, you won't have to keep providing the `schema` manually.
Note: At the moment, we only support aigenerate and aiembed functions.
It is a bit more technically demanding because you need to "compile" llama.cpp first, but it will always have the latest models and it is quite fast (eg, faster than Ollama, which uses llama.cpp under the hood but has some extra overhead).
Start your server in a command line (-m refers to the model file, -c is the context length, -ngl is the number of layers to offload to GPU):
msg = aigenerate(PT.CustomOpenAISchema(), "Count to 5 and say hi!"; api_kwargs=(; url="http://localhost:8080/v1"))
If you register the model names with `PT.register_model!`, you won't have to keep providing the `schema` manually. It can be any `model` name, because the model is actually selected when you start the server in the terminal.
You can also use the Databricks Foundation Models API with PromptingTools.jl. It requires you to set ENV variables DATABRICKS_API_KEY (often referred to as "DATABRICKS TOKEN") and DATABRICKS_HOST.
The long way to use it is:
julia
msg = aigenerate(PT.DatabricksOpenAISchema(),\n "Say hi to the llama!";\n model = "databricks-llama-2-70b-chat",\n api_key = ENV["DATABRICKS_API_KEY"], api_kwargs = (; url=ENV["DATABRICKS_HOST"]))
But you can also register the models you're hosting and use it as usual:
julia
# Quick registration of a model\nPT.register_model!(;\n name = "databricks-llama-2-70b-chat",\n schema = PT.DatabricksOpenAISchema())\nPT.MODEL_ALIASES["dllama"] = "databricks-llama-2-70b-chat" # set alias to make your life easier\n\n# Simply call:\nmsg = aigenerate("Say hi to the llama!"; model = "dllama")\n# Or even shorter\nai"Say hi to the llama!"dllama
You can also use the Together.ai API with PromptingTools.jl. It requires you to set ENV variable TOGETHER_API_KEY.
The corresponding schema is TogetherOpenAISchema, but we have registered one model for you, so you can use it as usual. Alias "tmixtral" (T for Together.ai and mixtral for the model name) is already set for you.
julia
msg = aigenerate("Say hi"; model="tmixtral")\n## [ Info: Tokens: 87 @ Cost: \\$0.0001 in 5.1 seconds\n## AIMessage("Hello! I'm here to help you. Is there something specific you'd like to know or discuss? I can provide information on a wide range of topics, assist with tasks, and even engage in a friendly conversation. Let me know how I can best assist you today.")
You can also use the Fireworks.ai API with PromptingTools.jl. It requires you to set ENV variable FIREWORKS_API_KEY.
The corresponding schema is FireworksOpenAISchema, but we have registered one model for you, so you can use it as usual. Alias "fmixtral" (F for Fireworks.ai and mixtral for the model name) is already set for you.
julia
msg = aigenerate("Say hi"; model="fmixtral")\n## [ Info: Tokens: 78 @ Cost: \\$0.0001 in 0.9 seconds\n## AIMessage("Hello! I'm glad you're here. I'm here to help answer any questions you have to the best of my ability. Is there something specific you'd like to know or discuss? I can assist with a wide range of topics, so feel free to ask me anything!")
In addition, at the time of writing (23rd Feb 2024), Fireworks is providing access to their new function calling model (fine-tuned Mixtral) for free.
Try it with aiextract for structured extraction (model is aliased as firefunction):
julia
"""\nExtract the food from the sentence. Extract any provided adjectives for the food as well.\n\nExample: "I am eating a crunchy bread." -> Food("bread", ["crunchy"])\n"""\nstruct Food\n name::String\n adjectives::Union{Nothing,Vector{String}}\nend\nprompt = "I just ate a delicious and juicy apple."\nmsg = aiextract(prompt; return_type=Food, model="firefunction")\nmsg.content\n# Output: Food("apple", ["delicious", "juicy"])
This file contains examples of how to work with Google AI Studio. It is known for its Gemini models.
Get an API key from here. If you see a documentation page ("Available languages and regions for Google AI Studio and Gemini API"), it means that it's not yet available in your region.
Save the API key in your environment as GOOGLE_API_KEY.
We'll need GoogleGenAI package:
julia
using Pkg; Pkg.add("GoogleGenAI")
You can now use the Gemini-1.0-Pro model like any other model in PromptingTools. We only support aigenerate at the moment.
AIMessage("Hi there! As a helpful AI assistant, I'm here to help you with any questions or tasks you may have. Feel free to ask me anything, and I'll do my best to assist you.")
You could achieve the same with a string macro (notice the "gemini" at the end to specify which model to use):
You can provide multi-turn conversations like with any other model:
julia
conversation = [\n PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),\n PT.UserMessage("I have feelings for my iPhone. What should I do?")]\nmsg = aigenerate(conversation; model="gemini")
AIMessage("Young Padawan, you have stumbled into a dangerous path. Attachment leads to suffering, and love can turn to darkness. \n\nRelease your feelings for this inanimate object. \n\nThe Force flows through all living things, not machines. Seek balance in the Force, and your heart will find true connection. \n\nRemember, the path of the Jedi is to serve others, not to be attached to possessions.")
Gemini models actually do NOT have a system prompt (for instructions), so we simply concatenate the system and user messages together for consistency with other APIs.
The reported tokens in the AIMessage are actually characters (that's how Google AI Studio intends to charge for them) and are a conservative estimate that we produce. It does not matter, because at the time of writing (Feb-24), the usage is free-of-charge.
', 22);
+const _hoisted_23 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_23);
+}
+const working_with_google_ai_studio = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ working_with_google_ai_studio as default
+};
diff --git a/previews/PR116/assets/examples_working_with_google_ai_studio.md.D80_eB8z.lean.js b/previews/PR116/assets/examples_working_with_google_ai_studio.md.D80_eB8z.lean.js
new file mode 100644
index 000000000..c93d4819a
--- /dev/null
+++ b/previews/PR116/assets/examples_working_with_google_ai_studio.md.D80_eB8z.lean.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"Working with Google AI Studio","description":"","frontmatter":{},"headers":[],"relativePath":"examples/working_with_google_ai_studio.md","filePath":"examples/working_with_google_ai_studio.md","lastUpdated":null}');
+const _sfc_main = { name: "examples/working_with_google_ai_studio.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode("", 22);
+const _hoisted_23 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_23);
+}
+const working_with_google_ai_studio = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ working_with_google_ai_studio as default
+};
diff --git a/previews/PR116/assets/examples_working_with_ollama.md.CWycYg8P.js b/previews/PR116/assets/examples_working_with_ollama.md.CWycYg8P.js
new file mode 100644
index 000000000..d063a7e48
--- /dev/null
+++ b/previews/PR116/assets/examples_working_with_ollama.md.CWycYg8P.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"Local models with Ollama.ai","description":"","frontmatter":{},"headers":[],"relativePath":"examples/working_with_ollama.md","filePath":"examples/working_with_ollama.md","lastUpdated":null}');
+const _sfc_main = { name: "examples/working_with_ollama.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode('
This file contains examples of how to work with Ollama.ai models. It assumes that you've already installated and launched the Ollama server. For more details or troubleshooting advice, see the Frequently Asked Questions section.
First, let's import the package and define a helper link for calling un-exported functions:
julia
using PromptingTools\nconst PT = PromptingTools
PromptingTools
There were are several models from https://ollama.ai/library that we have added to our PT.MODEL_REGISTRY, which means you don't need to worry about schema changes: Eg, "llama2" or "openhermes2.5-mistral" (see PT.list_registry() and PT.list_aliases())
Note: You must download these models prior to using them with ollama pull <model_name> in your Terminal.
If you use Apple Mac M1-3, make sure to provide `api_kwargs=(; options=(; num_gpu=99))` to make sure the whole model is offloaded on your GPU. Current default is 1, which makes some models unusable. Example for running Mixtral: `msg = aigenerate(PT.OllamaSchema(), "Count from 1 to 5 and then say hi."; model="dolphin-mixtral:8x7b-v2.5-q4_K_M", api_kwargs=(; options=(; num_gpu=99)))`
TL;DR if you use models in PT.MODEL_REGISTRY, you don't need to add schema as the first argument:
julia
msg = aigenerate("Say hi!"; model = "llama2")
AIMessage("Hello there! *adjusts glasses* It's nice to meet you! Is there anything I can help you with or would you like me to chat with you for a bit?")
model = "openhermes2.5-mistral"\n\na = 1\nmsg = aigenerate("What is `$a+$a`?"; model)\n\nname = "John"\nmsg = aigenerate("Say hi to {{name}}."; name, model)
AIMessage("Hello John! *smiles* It's nice to meet you! Is there anything I can help you with today?")
conversation = [\n PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),\n PT.UserMessage("I have feelings for my iPhone. What should I do?")]\nmsg = aigenerate(conversation; model)
AIMessage("(Deep sigh) A problem, you have. Feelings for an iPhone, hmm? (adjusts spectacles)\n\nMuch confusion, this causes. (scratches head) A being, you are. Attached to a device, you have become. (chuckles) Interesting, this is.\n\nFirst, let go, you must. (winks) Hard, it is, but necessary, yes. Distract yourself, find something else, try. (pauses)\n\nOr, perhaps, a balance, you seek? (nods) Both, enjoy and let go, the middle path, there is. (smirks) Finding joy in technology, without losing yourself, the trick, it is. (chuckles)\n\nBut fear not, young one! (grins) Help, I am here. Guide you, I will. The ways of the Yedi, teach you, I will. (winks) Patience and understanding, you must have. (nods)\n\nNow, go forth! (gestures) Explore, discover, find your balance. (smiles) The Force be with you, it does! (grins)")
If you're using some model that is not in the registry, you can either add it:
julia
PT.register_model!(;\n name = "llama123",\n schema = PT.OllamaSchema(),\n description = "Some model")\nPT.MODEL_ALIASES["l123"] = "llama123" # set an alias you like for it
"llama123"
OR define the schema explicitly (to avoid dispatch on global PT.PROMPT_SCHEMA):
julia
schema = PT.OllamaSchema()\naigenerate(schema, "Say hi!"; model = "llama2")
AIMessage("Hello there! *smiling face* It's nice to meet you! I'm here to help you with any questions or tasks you may have, so feel free to ask me anything. Is there something specific you need assistance with today? 😊")
Note: If you only use Ollama, you can change the default schema to PT.OllamaSchema() via PT.set_preferences!("PROMPT_SCHEMA" => "OllamaSchema", "MODEL_CHAT"=>"llama2")
Restart your session and run aigenerate("Say hi!") to test it.
! Note that in version 0.6, we've introduced OllamaSchema, which superseded OllamaManagedSchema and allows multi-turn conversations and conversations with images (eg, with Llava and Bakllava models). OllamaManagedSchema has been kept for compatibility and as an example of a schema where one provides a prompt as a string (not dictionaries like OpenAI API).
Add normalization as postprocessing function to normalize embeddings on reception (for easy cosine similarity later)
julia
using LinearAlgebra\nschema = PT.OllamaSchema()\n\nmsg = aiembed(schema,\n ["embed me", "and me too"],\n LinearAlgebra.normalize;\n model = "openhermes2.5-mistral")
PromptingTools.DataMessage(Matrix{Float64} of size (4096, 2))
AIGenerate - Lazy counterpart of aigenerate(). All ai* functions have a corresponding AI*::AICall struct that allows for deferred execution (triggered by run! method).
last_output, last_message - Simple utilities to access the last output and message of the AI calls like AIGenerate.
airetry! - A utility to automatically retry the AI call with the same inputs if the AI model fails to generate a valid output. It allows retrying many times and providing feedback to the AI model about the failure to increase its robustness. AIGenerate and other AI calls have a field config::RetryConfig where you can globally adjust the retrying behavior.
print_samples - airetry! implements a Monte Carlo Tree Search under the hood when trying to find the best way to fix the AI model's failure. print_samples is a utility to print the "samples" generated by the MCTS to better understand the attempts made by the AI model to fix the failure.
AICode extensions like aicodefixer_feedback and error_feedback - AICode is a wrapper that extracts any Julia code provided in the AIMessage (response from the AI model) and executes it (including catch any errors). aicodefixer_feedback and error_feedback are utilities that automatically review an outcome of AICode evaluation and generate the corresponding feedback for the AI model.
The main contribution of this module is providing the "lazy" counterparts to the ai... functions, which allow us to build a workflow, which can be re-executed many times with the same inputs.
For example, AIGenerate() will create a lazy instance of aigenerate, which is an instance of AICall with aigenerate as its ai-calling function. It uses exactly the same arguments and keyword arguments as aigenerate (see ?aigenerate for details). The notion of "lazy" refers to the fact that it does NOT generate any output when instantiated (only when run! is called).
Or said differently, the AICall struct and all its flavors (AIGenerate, ...) are designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This allows us to remember user inputs and trigger the LLM call repeatedly if needed, which enables automatic fixing (see ?airetry!).
How is it useful? We can use the same "inputs" for repeated calls, eg, when we want to validate or regenerate some outputs. We have a function airetry! to help us with that.
The signature of airetry is airetry(condition_function, aicall::AICall, feedback_function).
It evaluates the condition condition_function on the aicall object (eg, we evaluate f_cond(aicall) -> Bool). If it fails, we call feedback_function on the aicall object to provide feedback for the AI model (eg, f_feedback(aicall) -> String) and repeat the process until it passes or until max_retries value is exceeded.
We can catch API failures (no feedback needed, so none is provided)
julia
# API failure because of a non-existent model\n# RetryConfig allows us to change the "retry" behaviour of any lazy call\noutput = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),\n model = "NOTEXIST")\nrun!(output) # fails\n\n# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)\nairetry!(isvalid, output; retry_delay = 2, max_retries = 2)
Or we can use it for output validation (eg, its format, its content, etc.) and feedback generation.
Let's play a color guessing game (I'm thinking "yellow"). We'll implement two formatting checks with airetry!:
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances). \n# Both guesses are scored at each time step, and the best one is chosen for the next step.\n# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!\nout = AIGenerate(\n "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";\n verbose = false,\n config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))\nrun!(out)\n\n## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails\n## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.\nairetry!(x -> length(split(last_output(x), r" |\\\\.")) == 1, out,\n "You must answer with 1 word only.")\n\n# Note: you could also use the do-syntax, eg, \nairetry!(out, "You must answer with 1 word only.") do aicall\n length(split(last_output(aicall), r" |\\\\.")) == 1\nend
You can even add the guessing itself as an airetry! condition of last_output(out) == "yellow" and provide feedback if the guess is wrong.
AICall(func::F, args...; kwargs...) where {F<:Function}\n\nAIGenerate(args...; kwargs...)\nAIEmbed(args...; kwargs...)\nAIExtract(args...; kwargs...)
A lazy call wrapper for AI functions in the PromptingTools module, such as aigenerate.
The AICall struct is designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This approach allows for more flexible and efficient handling of AI function calls, especially in interactive environments.
Seel also: run!, AICodeFixer
Fields
func::F: The AI function to be called lazily. This should be a function like aigenerate or other ai* functions.
schema::Union{Nothing, PT.AbstractPromptSchema}: Optional schema to structure the prompt for the AI function.
conversation::Vector{PT.AbstractMessage}: A vector of messages that forms the conversation context for the AI call.
kwargs::NamedTuple: Keyword arguments to be passed to the AI function.
success::Union{Nothing, Bool}: Indicates whether the last call was successful (true) or not (false). Nothing if the call hasn't been made yet.
error::Union{Nothing, Exception}: Stores any exception that occurred during the last call. Nothing if no error occurred or if the call hasn't been made yet.
Example
Initiate an AICall like any ai* function, eg, AIGenerate:
julia
aicall = AICall(aigenerate)\n\n# With arguments and kwargs like ai* functions\n# from `aigenerate(schema, conversation; model="abc", api_kwargs=(; temperature=0.1))`\n# to\naicall = AICall(aigenerate, schema, conversation; model="abc", api_kwargs=(; temperature=0.1)\n\n# Or with a template\naicall = AIGenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1))
Trigger the AICall with run! (it returns the update AICall struct back):
julia
aicall |> run!\n````\n\nYou can also use `AICall` as a functor to trigger the AI call with a `UserMessage` or simply the text to send:
julia aicall(UserMessage("Hello, world!")) # Triggers the lazy call result = run!(aicall) # Explicitly runs the AI call ``` This can be used to "reply" to previous message / continue the stored conversation
Notes
The AICall struct is a key component in building flexible and efficient Agentic pipelines
The lazy evaluation model allows for setting up the call parameters in advance and deferring the actual execution until it is explicitly triggered.
This struct is particularly useful in scenarios where the timing of AI function execution needs to be deferred or where multiple potential calls need to be prepared and selectively executed.
Helpful accessor for the last generated output (msg.content) in conversation. Returns the last output in the conversation (eg, the string/data in the last message).
Evaluates the condition f_cond on the aicall object. If the condition is not met, it will return the best sample to retry from and provide feedback (string or function) to aicall. That's why it's mutating. It will retry maximum max_retries times, with throw=true, an error will be thrown if the condition is not met after max_retries retries.
Function signatures
f_cond(aicall::AICallBlock) -> Bool, ie, it must accept the aicall object and return a boolean value.
feedback can be a string or feedback(aicall::AICallBlock) -> String, ie, it must accept the aicall object and return a string.
You can leverage the last_message, last_output, and AICode functions to access the last message, last output and execute code blocks in the conversation, respectively. See examples below.
Good Use Cases
Retry with API failures/drops (add retry_delay=2 to wait 2s between retries)
Check the output format / type / length / etc
Check the output with aiclassify call (LLM Judge) to catch unsafe/NSFW/out-of-scope content
Provide hints to the model to guide it to the correct answer
Gotchas
If controlling keyword arguments are set to nothing, they will fall back to the default values in aicall.config. You can override them by passing the keyword arguments explicitly.
If there multiple airetry! checks, they are evaluted sequentially. As long as throw==false, they will be all evaluated even if they failed previous checks.
Only samples which passed previous evaluations are evaluated (sample.success is true). If there are no successful samples, the function will evaluate only the active sample (aicall.active_sample_id) and nothing else.
Feedback from all "ancestor" evaluations is added upon retry, not feedback from the "sibblings" or other branches. To have only ONE long BRANCH (no sibblings), make sure to keep RetryConfig(; n_samples=1). That way the model will always see ALL previous feedback.
We implement a version of Monte Carlo Tree Search (MCTS) to always pick the most promising sample to restart from (you can tweak the options in RetryConfig to change the behaviour).
For large number of parallel branches (ie, "shallow and wide trees"), you might benefit from switching scoring to scoring=ThompsonSampling() (similar to how Bandit algorithms work).
Open-source/local models can struggle with too long conversation, you might want to experiment with in-place feedback (set RetryConfig(; feedback_inplace=true)).
Arguments
f_cond::Function: A function that accepts the aicall object and returns a boolean value. Retry will be attempted if the condition is not met (f_cond -> false).
aicall::AICallBlock: The aicall object to evaluate the condition on.
feedback::Union{AbstractString, Function}: Feedback to provide if the condition is not met. If a function is provided, it must accept the aicall object as the only argument and return a string.
verbose::Integer=1: A verbosity level for logging the retry attempts and warnings. A higher value indicates more detailed logging.
throw::Bool=false: If true, it will throw an error if the function f_cond does not return true after max_retries retries.
evaluate_all::Bool=false: If true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample.
feedback_expensive::Bool=false: If false, it will provide feedback to all samples that fail the condition. If feedback function is expensive to call (eg, another ai* function), set this to true and feedback will be provided only to the sample we will retry from.
max_retries::Union{Nothing, Int}=nothing: Maximum number of retries. If not provided, it will fall back to the max_retries in aicall.config.
retry_delay::Union{Nothing, Int}=nothing: Delay between retries in seconds. If not provided, it will fall back to the retry_delay in aicall.config.
Returns
The aicall object with the updated conversation, and samples (saves the evaluations and their scores/feedback).
Example
You can use airetry! to catch API errors in run! and auto-retry the call. RetryConfig is how you influence all the subsequent retry behaviours - see ?RetryConfig for more details.
julia
# API failure because of a non-existent model\nout = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),\n model = "NOTEXIST")\nrun!(out) # fails\n\n# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)\nairetry!(isvalid, out; retry_delay = 2, max_retries = 2)
If you provide arguments to the aicall, we try to honor them as much as possible in the following calls, eg, set low verbosity
julia
out = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),\nmodel = "NOTEXIST", verbose=false)\nrun!(out)\n# No info message, you just see `success = false` in the properties of the AICall
Let's show a toy example to demonstrate the runtime checks / guardrails for the model output. We'll play a color guessing game (I'm thinking "yellow"):
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances). \n# Both guesses are scored at each time step, and the best one is chosen for the next step.\n# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!\nout = AIGenerate(\n "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";\n verbose = false,\n config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))\nrun!(out)\n\n\n## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails\n## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.\nairetry!(x -> length(split(last_output(x), r" |\\.")) == 1, out,\n "You must answer with 1 word only.")\n\n\n## Let's ensure that the output is in lowercase - simple and short\nairetry!(x -> all(islowercase, last_output(x)), out, "You must answer in lowercase.")\n# [ Info: Condition not met. Retrying...\n\n\n## Let's add final hint - it took us 2 retries\nairetry!(x -> startswith(last_output(x), "y"), out, "It starts with "y"")\n# [ Info: Condition not met. Retrying...\n# [ Info: Condition not met. Retrying...\n\n\n## We end up with the correct answer\nlast_output(out)\n# Output: "yellow"
Let's explore how we got here. We save the various attempts in a "tree" (SampleNode object) You can access it in out.samples, which is the ROOT of the tree (top level). Currently "active" sample ID is out.active_sample_id -> that's the same as conversation field in your AICall.
julia
# Root node:\nout.samples\n# Output: SampleNode(id: 46839, stats: 6/12, length: 2)\n\n# Active sample (our correct answer):\nout.active_sample_id \n# Output: 50086\n\n# Let's obtain the active sample node with this ID - use getindex notation or function find_node\nout.samples[out.active_sample_id]\n# Output: SampleNode(id: 50086, stats: 1/1, length: 7)\n\n# The SampleNode has two key fields: data and feedback. Data is where the conversation is stored:\nactive_sample = out.samples[out.active_sample_id]\nactive_sample.data == out.conversation # Output: true -> This is the winning guess!
We also get a clear view of the tree structure of all samples with print_samples:
We can also iterate through all samples and extract whatever information we want with PostOrderDFS or PreOrderDFS (exported from AbstractTrees.jl)
julia
for sample in PostOrderDFS(out.samples)\n # Data is the universal field for samples, we put `conversation` in there\n # Last item in data is the last message in coversation\n msg = sample.data[end]\n if msg isa PT.AIMessage # skip feedback\n # get only the message content, ie, the guess\n println("ID: $(sample.id), Answer: $(msg.content)")\n end\nend\n\n# ID: 20493, Answer: yellow\n# ID: 50086, Answer: yellow\n# ID: 2733, Answer: red\n# ID: 30088, Answer: blue\n# ID: 44816, Answer: blue
Note: airetry! will attempt to fix the model max_retries times. If you set throw=true, it will throw an ErrorException if the condition is not met after max_retries retries.
Let's define a mini program to guess the number and use airetry! to guide the model to the correct answer:
julia
"""\n llm_guesser()\n\nMini program to guess the number provided by the user (betwee 1-100).\n"""\nfunction llm_guesser(user_number::Int)\n @assert 1 <= user_number <= 100\n prompt = """\nI'm thinking a number between 1-100. Guess which one it is. \nYou must respond only with digits and nothing else. \nYour guess:"""\n ## 2 samples at a time, max 5 fixing rounds\n out = AIGenerate(prompt; config = RetryConfig(; n_samples = 2, max_retries = 5),\n api_kwargs = (; n = 2)) |> run!\n ## Check the proper output format - must parse to Int, use do-syntax\n ## We can provide feedback via a function!\n function feedback_f(aicall)\n "Output: $(last_output(aicall))\nFeedback: You must respond only with digits!!"\n end\n airetry!(out, feedback_f) do aicall\n !isnothing(tryparse(Int, last_output(aicall)))\n end\n ## Give a hint on bounds\n lower_bound = (user_number ÷ 10) * 10\n upper_bound = lower_bound + 10\n airetry!(\n out, "The number is between or equal to $lower_bound to $upper_bound.") do aicall\n guess = tryparse(Int, last_output(aicall))\n lower_bound <= guess <= upper_bound\n end\n ## You can make at most 3x guess now -- if there is max_retries in `config.max_retries` left\n max_retries = out.config.retries + 3\n function feedback_f2(aicall)\n guess = tryparse(Int, last_output(aicall))\n "Your guess of $(guess) is wrong, it's $(abs(guess-user_number)) numbers away."\n end\n airetry!(out, feedback_f2; max_retries) do aicall\n tryparse(Int, last_output(aicall)) == user_number\n end\n\n ## Evaluate the best guess\n @info "Results: Guess: $(last_output(out)) vs User: $user_number (Number of calls made: $(out.config.calls))"\n return out\nend\n\n# Let's play the game\nout = llm_guesser(33)\n[ Info: Condition not met. Retrying...\n[ Info: Condition not met. Retrying...\n[ Info: Condition not met. Retrying...\n[ Info: Condition not met. Retrying...\n[ Info: Results: Guess: 33 vs User: 33 (Number of calls made: 10)
Yay! We got it 😃
Now, we could explore different samples (eg, print_samples(out.samples)) or see what the model guessed at each step:
Note that if there are multiple "branches" the model will see only the feedback of its own and its ancestors not the other "branches". If you wanted to provide ALL feedback, set RetryConfig(; n_samples=1) to remove any "branching". It fixing will be done sequentially in one conversation and the model will see all feedback (less powerful if the model falls into a bad state). Alternatively, you can tweak the feedback function.
A mutable structure representing a code block (received from the AI model) with automatic parsing, execution, and output/error capturing capabilities.
Upon instantiation with a string, the AICode object automatically runs a code parser and executor (via PromptingTools.eval!()), capturing any standard output (stdout) or errors. This structure is useful for programmatically handling and evaluating Julia code snippets.
See also: PromptingTools.extract_code_blocks, PromptingTools.eval!
Workflow
Until cb::AICode has been evaluated, cb.success is set to nothing (and so are all other fields).
The text in cb.code is parsed (saved to cb.expression).
The parsed expression is evaluated.
Outputs of the evaluated expression are captured in cb.output.
Any stdout outputs (e.g., from println) are captured in cb.stdout.
If an error occurs during evaluation, it is saved in cb.error.
After successful evaluation without errors, cb.success is set to true. Otherwise, it is set to false and you can inspect the cb.error to understand why.
Properties
code::AbstractString: The raw string of the code to be parsed and executed.
expression: The parsed Julia expression (set after parsing code).
stdout: Captured standard output from the execution of the code.
output: The result of evaluating the code block.
success::Union{Nothing, Bool}: Indicates whether the code block executed successfully (true), unsuccessfully (false), or has yet to be evaluated (nothing).
error::Union{Nothing, Exception}: Any exception raised during the execution of the code block.
Keyword Arguments
auto_eval::Bool: If set to true, the code block is automatically parsed and evaluated upon instantiation. Defaults to true.
safe_eval::Bool: If set to true, the code block checks for package operations (e.g., installing new packages) and missing imports, and then evaluates the code inside a bespoke scratch module. This is to ensure that the evaluation does not alter any user-defined variables or the global state. Defaults to false.
skip_unsafe::Bool: If set to true, we skip any lines in the code block that are deemed unsafe (eg, Pkg operations). Defaults to false.
skip_invalid::Bool: If set to true, we skip code blocks that do not even parse. Defaults to false.
verbose::Bool: If set to true, we print out any lines that are skipped due to being unsafe. Defaults to false.
capture_stdout::Bool: If set to true, we capture any stdout outputs (eg, test failures) in cb.stdout. Defaults to true.
prefix::AbstractString: A string to be prepended to the code block before parsing and evaluation. Useful to add some additional code definition or necessary imports. Defaults to an empty string.
suffix::AbstractString: A string to be appended to the code block before parsing and evaluation. Useful to check that tests pass or that an example executes. Defaults to an empty string.
remove_tests::Bool: If set to true, we remove any @test or @testset macros from the code block before parsing and evaluation. Defaults to false.
execution_timeout::Int: The maximum time (in seconds) allowed for the code block to execute. Defaults to 60 seconds.
Methods
Base.isvalid(cb::AICode): Check if the code block has executed successfully. Returns true if cb.success == true.
Examples
julia
code = AICode("println("Hello, World!")") # Auto-parses and evaluates the code, capturing output and errors.\nisvalid(code) # Output: true\ncode.stdout # Output: "Hello, World!\n"
We try to evaluate "safely" by default (eg, inside a custom module, to avoid changing user variables). You can avoid that with save_eval=false:
You can also call AICode directly on an AIMessage, which will extract the Julia code blocks, concatenate them and evaluate them:
julia
msg = aigenerate("In Julia, how do you create a vector of 10 random numbers?")\ncode = AICode(msg)\n# Output: AICode(Success: True, Parsed: True, Evaluated: True, Error Caught: N/A, StdOut: True, Code: 2 Lines)\n\n# show the code\ncode.code |> println\n# Output: \n# numbers = rand(10)\n# numbers = rand(1:100, 10)\n\n# or copy it to the clipboard\ncode.code |> clipboard\n\n# or execute it in the current module (=Main)\neval(code.expression)
Generate feedback for an AI code fixing session based on the conversation history. Function is designed to be extensible for different types of feedback and code evaluation outcomes.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
Individual feedback functions are dispatched on different subtypes of AbstractCodeOutcome and can be extended/overwritten to provide more detailed feedback.
See also: AIGenerate, AICodeFixer
Arguments
conversation::AbstractVector{<:PT.AbstractMessage}: A vector of messages representing the conversation history, where the last message is expected to contain the code to be analyzed.
max_length::Int=512: An optional argument that specifies the maximum length of the feedback message.
Returns
NamedTuple: A feedback message as a kwarg in NamedTuple based on the analysis of the code provided in the conversation.
Example
julia
new_kwargs = aicodefixer_feedback(conversation)
Notes
This function is part of the AI code fixing system, intended to interact with code in AIMessage and provide feedback on improving it.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
It dispatches for the code feedback based on the subtypes of AbstractCodeOutcome below:
CodeEmpty: No code found in the message.
CodeFailedParse: Code parsing error.
CodeFailedEval: Runtime evaluation error.
CodeFailedTimeout: Code execution timed out.
CodeSuccess: Successful code execution.
You can override the individual methods to customize the feedback.
Currently, there is only one function in this module create_websearch that leverages Tavily.com search and answer engine to provide additional context.
You need to sign up for an API key at Tavily.com and set it as an environment variable TAVILY_API_KEY to use this function.
api_key::AbstractString: The API key to use for the search. Get an API key from Tavily.
search_depth::AbstractString: The depth of the search. Can be either "basic" or "advanced". Default is "basic". Advanced search calls equal to 2 requests.
include_answer::Bool: Whether to include the answer in the search results. Default is false.
include_raw_content::Bool: Whether to include the raw content in the search results. Default is false.
max_results::Integer: The maximum number of results to return. Default is 5.
include_images::Bool: Whether to include images in the search results. Default is false.
include_domains::AbstractVector{<:AbstractString}: A list of domains to include in the search results. Default is an empty list.
exclude_domains::AbstractVector{<:AbstractString}: A list of domains to exclude from the search results. Default is an empty list.
Example
julia
r = create_websearch("Who is King Charles?")
Even better, you can get not just the results but also the answer:
julia
r = create_websearch("Who is King Charles?"; include_answer = true)
RAGTools is an experimental module that provides a set of utilities for building Retrieval-Augmented Generation (RAG) applications, ie, applications that generate answers by combining knowledge of the underlying AI model with the information from the user's knowledge base.
It is designed to be powerful and flexible, allowing you to build RAG applications with minimal effort. Extend any step of the pipeline with your own custom code (see the RAG Interface section), or use the provided defaults to get started quickly.
Once the API stabilizes (near term), we hope to carve it out into a separate package.
Import the module as follows:
julia
# required dependencies to load the necessary extensions!!!\nusing LinearAlgebra, SparseArrays \nusing PromptingTools.Experimental.RAGTools\n# to access unexported functionality\nconst RT = PromptingTools.Experimental.RAGTools
build_index to build a RAG index from a list of documents (type ChunkIndex)
airag to generate answers using the RAG model on top of the index built above
retrieve to retrieve relevant chunks from the index for a given question
generate! to generate an answer from the retrieved chunks
annotate_support to highlight which parts of the RAG answer are supported by the documents in the index vs which are generated by the model, it is applied automatically if you use pretty printing with pprint (eg, pprint(result))
build_qa_evals to build a set of question-answer pairs for evaluation of the RAG model from your corpus
The hope is to provide a modular and easily extensible set of tools for building RAG applications in Julia. Feel free to open an issue or ask in the #generative-ai channel in the JuliaLang Slack if you have a specific need.
Let's build an index, we need to provide a starter list of documents:
julia
sentences = [\n "Find the most comprehensive guide on Julia programming language for beginners published in 2023.",\n "Search for the latest advancements in quantum computing using Julia language.",\n "How to implement machine learning algorithms in Julia with examples.",\n "Looking for performance comparison between Julia, Python, and R for data analysis.",\n "Find Julia language tutorials focusing on high-performance scientific computing.",\n "Search for the top Julia language packages for data visualization and their documentation.",\n "How to set up a Julia development environment on Windows 10.",\n "Discover the best practices for parallel computing in Julia.",\n "Search for case studies of large-scale data processing using Julia.",\n "Find comprehensive resources for mastering metaprogramming in Julia.",\n "Looking for articles on the advantages of using Julia for statistical modeling.",\n "How to contribute to the Julia open-source community: A step-by-step guide.",\n "Find the comparison of numerical accuracy between Julia and MATLAB.",\n "Looking for the latest Julia language updates and their impact on AI research.",\n "How to efficiently handle big data with Julia: Techniques and libraries.",\n "Discover how Julia integrates with other programming languages and tools.",\n "Search for Julia-based frameworks for developing web applications.",\n "Find tutorials on creating interactive dashboards with Julia.",\n "How to use Julia for natural language processing and text analysis.",\n "Discover the role of Julia in the future of computational finance and econometrics."\n]
Let's index these "documents":
julia
index = build_index(sentences; chunker_kwargs=(; sources=map(i -> "Doc$i", 1:length(sentences))))
This would be equivalent to the following index = build_index(SimpleIndexer(), sentences) which dispatches to the default implementation of each step via the SimpleIndexer struct. We provide these default implementations for the main functions as an optional argument - no need to provide them if you're running the default pipeline.
Notice that we have provided a chunker_kwargs argument to the build_index function. These will be kwargs passed to chunker step.
Now let's generate an answer to a question.
Run end-to-end RAG (retrieve + generate!), return AIMessage
julia
question = "What are the best practices for parallel computing in Julia?"\n\nmsg = airag(index; question) # short for airag(RAGConfig(), index; question)\n## Output:\n## [ Info: Done with RAG. Total cost: \\$0.0\n## AIMessage("Some best practices for parallel computing in Julia include us...
Explore what's happening under the hood by changing the return type - RAGResult contains all intermediate steps.
julia
result = airag(index; question, return_all=true)\n## RAGResult\n## question: String "What are the best practices for parallel computing in Julia?"\n## rephrased_questions: Array{String}((1,))\n## answer: SubString{String}\n## final_answer: SubString{String}\n## context: Array{String}((5,))\n## sources: Array{String}((5,))\n## emb_candidates: CandidateChunks{Int64, Float32}\n## tag_candidates: CandidateChunks{Int64, Float32}\n## filtered_candidates: CandidateChunks{Int64, Float32}\n## reranked_candidates: CandidateChunks{Int64, Float32}\n## conversations: Dict{Symbol, Vector{<:PromptingTools.AbstractMessage}}
You can still get the message from the result, see result.conversations[:final_answer] (the dictionary keys correspond to the function names of those steps).
If you need to customize it, break the pipeline into its sub-steps: retrieve and generate - RAGResult serves as the intermediate result.
julia
# Retrieve which chunks are relevant to the question\nresult = retrieve(index, question)\n# Generate an answer\nresult = generate!(index, result)
You can leverage a pretty-printing system with pprint where we automatically annotate the support of the answer by the chunks we provided to the model. It is configurable and you can select only some of its functions (eg, scores, sources).
julia
pprint(result)
You'll see the following in REPL but with COLOR highlighting in the terminal.
plaintext
--------------------\nQUESTION(s)\n--------------------\n- What are the best practices for parallel computing in Julia?\n\n--------------------\nANSWER\n--------------------\nSome of the best practices for parallel computing in Julia include:[1,0.7]\n- Using [3,0.4]`@threads` for simple parallelism[1,0.34]\n- Utilizing `Distributed` module for more complex parallel tasks[1,0.19]\n- Avoiding excessive memory allocation\n- Considering task granularity for efficient workload distribution\n\n--------------------\nSOURCES\n--------------------\n1. Doc8\n2. Doc15\n3. Doc5\n4. Doc2\n5. Doc9
See ?print_html for the HTML version of the pretty-printing and styling system, eg, when you want to display the results in a web application based on Genie.jl/Stipple.jl.
How to read the output
Color legend:
No color: High match with the context, can be trusted more
Blue: Partial match against some words in the context, investigate
Magenta (Red): No match with the context, fully generated by the model
Square brackets: The best matching context ID + Match score of the chunk (eg, [3,0.4] means the highest support for the sentence is from the context chunk number 3 with a 40% match).
Want more?
See examples/building_RAG.jl for one more example.
This system is designed for information retrieval and response generation, structured in three main phases:
Preparation, when you create an instance of AbstractIndex
Retrieval, when you surface the top most relevant chunks/items in the index and return AbstractRAGResult, which contains the references to the chunks (AbstractCandidateChunks)
Generation, when you generate an answer based on the context built from the retrieved chunks, return either AIMessage or AbstractRAGResult
The corresponding functions are build_index, retrieve, and generate!, respectively. Here is the high-level diagram that shows the signature of the main functions:
Notice that the first argument is a custom type for multiple dispatch. In addition, observe the "kwargs" names, that's how the keyword arguments for each function are passed down from the higher-level functions (eg, build_index(...; chunker_kwargs=(; separators=...)))). It's the simplest way to customize some step of the pipeline (eg, set a custom model with a model kwarg or prompt template with template kwarg).
The system is designed to be hackable and extensible at almost every entry point. If you want to customize the behavior of any step, you can do so by defining a new type and defining a new method for the step you're changing, eg,
And then you would set the retrive step to use your custom MyReranker via reranker kwarg, eg, retrieve(....; reranker = MyReranker()) (or customize the main dispatching AbstractRetriever struct).
The overarching principles are:
Always dispatch / customize the behavior by defining a new Struct and the corresponding method for the existing functions (eg, rerank function for the re-ranking step).
Custom types are provided as the first argument (the high-level functions will work without them as we provide some defaults).
Custom types do NOT have any internal fields or DATA (with the exception of managing sub-steps of the pipeline like AbstractRetriever or RAGConfig).
Additional data should be passed around as keyword arguments (eg, chunker_kwargs in build_index to pass data to the chunking step). The intention was to have some clearly documented default values in the docstrings of each step + to have the various options all in one place.
If you need to pass keyword arguments, use the nested kwargs corresponding to the dispatch type names (rephrase step, has rephraser dispatch type and rephraser_kwargs for its keyword arguments).
For example:
julia
cfg = RAGConfig(; retriever = AdvancedRetriever())\n\n# kwargs will be big and nested, let's prepare them upfront\n# we specify "custom" model for each component that calls LLM\nkwargs = (\n retriever = AdvancedRetriever(),\n retriever_kwargs = (;\n top_k = 100,\n top_n = 5,\n # notice that this is effectively: retriever_kwargs/rephraser_kwargs/template\n rephraser_kwargs = (;\n template = :RAGQueryHyDE,\n model = "custom")),\n generator_kwargs = (;\n # pass kwargs to `answer!` step defined by the `answerer` -> we're setting `answerer_kwargs`\n answerer_kwargs = (;\n model = "custom"),\n # api_kwargs can be shared across all components\n api_kwargs = (;\n url = "http://localhost:8080")))\n\nresult = airag(cfg, index, question; kwargs...)
If you were one level deeper in the pipeline, working with retriever directly, you would pass:
julia
retriever_kwargs = (;\n top_k = 100,\n top_n = 5,\n # notice that this is effectively: rephraser_kwargs/template\n rephraser_kwargs = (;\n template = :RAGQueryHyDE,\n model = "custom"),\n # api_kwargs can be shared across all components\n api_kwargs = (;\n url = "http://localhost:8080"))\n\nresult = retrieve(AdvancedRetriever(), index, question; retriever_kwargs...)
And going even deeper, you would provide the rephraser_kwargs directly to the rephrase step, eg,
Begins with build_index, which creates a user-defined index type from an abstract chunk index using specified dels and function strategies.
get_chunks then divides the indexed data into manageable pieces based on a chunking strategy.
get_embeddings generates embeddings for each chunk using an embedding strategy to facilitate similarity arches.
Finally, get_tags extracts relevant metadata from each chunk, enabling tag-based filtering (hybrid search index). If there are tags available, build_tags is called to build the corresponding sparse matrix for filtering with tags.
Retrieval Phase:
The retrieve step is intended to find the most relevant chunks in the index.
rephrase is called first, if we want to rephrase the query (methods like HyDE can improve retrieval quite a bit)!
get_embeddings generates embeddings for the original + rephrased query
find_closest looks up the most relevant candidates (CandidateChunks) using a similarity search strategy.
get_tags extracts the potential tags (can be provided as part of the airag call, eg, when we want to use only some small part of the indexed chunks)
find_tags filters the candidates to strictly match at least one of the tags (if provided)
rerank is called to rerank the candidates based on the reranking strategy (ie, to improve the ordering of the chunks in context).
Generation Phase:
The generate! step is intended to generate a response based on the retrieved chunks, provided via AbstractRAGResult (eg, RAGResult).
build_context! constructs the context for response generation based on a context strategy and applies the necessary formatting
answer! generates the response based on the context and the query
refine! is called to refine the response (optional, defaults to passthrough)
postprocessing! is available for any final touches to the response or to potentially save or format the results (eg, automatically save to the disk)
Note that all generation steps are mutating the RAGResult object.
See more details and corresponding functions and types in src/Experimental/RAGTools/rag_interface.jl.
Build an INDEX for RAG (Retriever-Augmented Generation) applications from the provided file paths. INDEX is a object storing the document chunks and their embeddings (and potentially other information).
The function processes each file or document (depending on chunker), splits its content into chunks, embeds these chunks, optionally extracts metadata, and then combines this information into a retrievable index.
Define your own methods via indexer and its subcomponents (chunker, embedder, tagger).
Arguments
indexer::AbstractIndexBuilder: The indexing logic to use. Default is SimpleIndexer().
files_or_docs: A vector of valid file paths OR string documents to be indexed (chunked and embedded). Specify which mode to use via chunker.
verbose: An Integer specifying the verbosity of the logs. Default is 1 (high-level logging). 0 is disabled.
extras: An optional vector of extra information to be stored with each chunk. Default is nothing.
index_id: A unique identifier for the index. Default is a generated symbol.
chunker: The chunker logic to use for splitting the documents. Default is TextChunker().
chunker_kwargs: Parameters to be provided to the get_chunks function. Useful to change the separators or max_length.
sources: A vector of strings indicating the source of each chunk. Default is equal to files_or_docs.
embedder: The embedder logic to use for embedding the chunks. Default is BatchEmbedder().
embedder_kwargs: Parameters to be provided to the get_embeddings function. Useful to change the target_batch_size_length or reduce asyncmap tasks ntasks.
model: The model to use for embedding. Default is PT.MODEL_EMBEDDING.
tagger: The tagger logic to use for extracting tags from the chunks. Default is NoTagger(), ie, skip tag extraction. There are also PassthroughTagger and OpenTagger.
tagger_kwargs: Parameters to be provided to the get_tags function.
model: The model to use for tags extraction. Default is PT.MODEL_CHAT.
template: A template to be used for tags extraction. Default is :RAGExtractMetadataShort.
tags: A vector of vectors of strings directly providing the tags for each chunk. Applicable for tagger::PasstroughTagger.
api_kwargs: Parameters to be provided to the API endpoint. Shared across all API calls if provided.
cost_tracker: A Threads.Atomic{Float64} object to track the total cost of the API calls. Useful to pass the total cost to the parent call.
Returns
ChunkIndex: An object containing the compiled index of chunks, embeddings, tags, vocabulary, and sources.
# Default is loading a vector of strings and chunking them (`TextChunker()`)\nindex = build_index(SimpleIndexer(), texts; chunker_kwargs = (; max_length=10))\n\n# Another example with tags extraction, splitting only sentences and verbose output\n# Assuming `test_files` is a vector of file paths\nindexer = SimpleIndexer(chunker=FileChunker(), tagger=OpenTagger())\nindex = build_index(indexer, test_files; \n chunker_kwargs(; separators=[". "]), verbose=true)
Notes
If you get errors about exceeding embedding input sizes, first check the max_length in your chunks. If that does NOT resolve the issue, try changing the embedding_kwargs. In particular, reducing the target_batch_size_length parameter (eg, 10_000) and number of tasks ntasks=1. Some providers cannot handle large batch sizes (eg, Databricks).
High-level wrapper for Retrieval-Augmented Generation (RAG), it combines together the retrieve and generate! steps which you can customize if needed.
The simplest version first finds the relevant chunks in index for the question and then sends these chunks to the AI model to help with generating a response to the question.
To customize the components, replace the types (retriever, generator) of the corresponding step of the RAG pipeline - or go into sub-routines within the steps. Eg, use subtypes(AbstractRetriever) to find the available options.
Arguments
cfg::AbstractRAGConfig: The configuration for the RAG pipeline. Defaults to RAGConfig(), where you can swap sub-types to customize the pipeline.
index::AbstractChunkIndex: The chunk index to search for relevant text.
question::AbstractString: The question to be answered.
return_all::Bool: If true, returns the details used for RAG along with the response.
verbose::Integer: If >0, enables verbose logging. The higher the number, the more nested functions will log.
api_kwargs: API parameters that will be forwarded to ALL of the API calls (aiembed, aigenerate, and aiextract).
retriever::AbstractRetriever: The retriever to use for finding relevant chunks. Defaults to cfg.retriever, eg, SimpleRetriever (with no question rephrasing).
retriever_kwargs::NamedTuple: API parameters that will be forwarded to the retriever call. Examples of important ones:
top_k::Int: Number of top candidates to retrieve based on embedding similarity.
top_n::Int: Number of candidates to return after reranking.
tagger::AbstractTagger: Tagger to use for tagging the chunks. Defaults to NoTagger().
tagger_kwargs::NamedTuple: API parameters that will be forwarded to the tagger call. You could provide the explicit tags directly with PassthroughTagger and tagger_kwargs = (; tags = ["tag1", "tag2"]).
generator::AbstractGenerator: The generator to use for generating the answer. Defaults to cfg.generator, eg, SimpleGenerator.
generator_kwargs::NamedTuple: API parameters that will be forwarded to the generator call. Examples of important ones:
answerer_kwargs::NamedTuple: API parameters that will be forwarded to the answerer call. Examples:
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerFromContext.
refiner::AbstractRefiner: The method to use for refining the answer. Defaults to generator.refiner, eg, NoRefiner.
refiner_kwargs::NamedTuple: API parameters that will be forwarded to the refiner call.
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerRefiner.
cost_tracker: An atomic counter to track the total cost of the operations (if you want to track the cost of multiple pipeline runs - it passed around in the pipeline).
Returns
If return_all is false, returns the generated message (msg).
If return_all is true, returns the detail of the full pipeline in RAGResult (see the docs).
See also build_index, retrieve, generate!, RAGResult
Examples
Using airag to get a response for a question:
julia
index = build_index(...) # create an index\nquestion = "How to make a barplot in Makie.jl?"\nmsg = airag(index; question)
To understand the details of the RAG process, use return_all=true
julia
msg, details = airag(index; question, return_all = true)\n# details is a RAGDetails object with all the internal steps of the `airag` function
You can also pretty-print details to highlight generated text vs text that is supported by context. It also includes annotations of which context was used for each part of the response (where available).
julia
PT.pprint(details)
Example with advanced retrieval (with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n). In addition, it will be done with a "custom" locally-hosted model.
julia
cfg = RAGConfig(; retriever = AdvancedRetriever())\n\n# kwargs will be big and nested, let's prepare them upfront\n# we specify "custom" model for each component that calls LLM\nkwargs = (\n retriever_kwargs = (;\n top_k = 100,\n top_n = 5,\n rephraser_kwargs = (;\n model = "custom"),\n embedder_kwargs = (;\n model = "custom"),\n tagger_kwargs = (;\n model = "custom")),\n generator_kwargs = (;\n answerer_kwargs = (;\n model = "custom"),\n refiner_kwargs = (;\n model = "custom")),\n api_kwargs = (;\n url = "http://localhost:8080"))\n\nresult = airag(cfg, index, question; kwargs...)
Retrieves the most relevant chunks from the index for the given question and returns them in the RAGResult object.
This is the main entry point for the retrieval stage of the RAG pipeline. It is often followed by generate! step.
Notes:
The default flow is build_context! -> answer! -> refine! -> postprocess!.
The arguments correspond to the steps of the retrieval process (rephrasing, embedding, finding similar docs, tagging, filtering by tags, reranking). You can customize each step by providing a new custom type that dispatches the corresponding function, eg, create your own type struct MyReranker<:AbstractReranker end and define the custom method for it rerank(::MyReranker,...) = ....
Note: Discover available retrieval sub-types for each step with subtypes(AbstractRephraser) and similar for other abstract types.
If you're using locally-hosted models, you can pass the api_kwargs with the url field set to the model's URL and make sure to provide corresponding model kwargs to rephraser, embedder, and tagger to use the custom models (they make AI calls).
Arguments
retriever: The retrieval method to use. Default is SimpleRetriever but could be AdvancedRetriever for more advanced retrieval.
index: The index that holds the chunks and sources to be retrieved from.
question: The question to be used for the retrieval.
verbose: If >0, it prints out verbose logging. Default is 1. If you set it to 2, it will print out logs for each sub-function.
top_k: The TOTAL number of closest chunks to return from find_closest. Default is 100. If there are multiple rephrased questions, the number of chunks per each item will be top_k ÷ number_of_rephrased_questions.
top_n: The TOTAL number of most relevant chunks to return for the context (from rerank step). Default is 5.
api_kwargs: Additional keyword arguments to be passed to the API calls (shared by all ai* calls).
rephraser: Transform the question into one or more questions. Default is retriever.rephraser.
rephraser_kwargs: Additional keyword arguments to be passed to the rephraser.
model: The model to use for rephrasing. Default is PT.MODEL_CHAT.
template: The rephrasing template to use. Default is :RAGQueryOptimizer or :RAGQueryHyDE (depending on the rephraser selected).
embedder: The embedding method to use. Default is retriever.embedder.
embedder_kwargs: Additional keyword arguments to be passed to the embedder.
finder: The similarity search method to use. Default is retriever.finder, often CosineSimilarity.
finder_kwargs: Additional keyword arguments to be passed to the similarity finder.
tagger: The tag generating method to use. Default is retriever.tagger.
tagger_kwargs: Additional keyword arguments to be passed to the tagger. Noteworthy arguments:
tags: Directly provide the tags to use for filtering (can be String, Regex, or Vector{String}). Useful for tagger = PassthroughTagger.
filter: The tag matching method to use. Default is retriever.filter.
filter_kwargs: Additional keyword arguments to be passed to the tag filter.
reranker: The reranking method to use. Default is retriever.reranker.
reranker_kwargs: Additional keyword arguments to be passed to the reranker.
model: The model to use for reranking. Default is rerank-english-v2.0 if you use reranker = CohereReranker().
cost_tracker: An atomic counter to track the cost of the retrieval. Default is Threads.Atomic{Float64}(0.0).
Find the 5 most relevant chunks from the index for the given question.
julia
# assumes you have an existing index `index`\nretriever = SimpleRetriever()\n\nresult = retrieve(retriever,\n index,\n "What is the capital of France?",\n top_n = 5)\n\n# or use the default retriever (same as above)\nresult = retrieve(retriever,\n index,\n "What is the capital of France?",\n top_n = 5)
Apply more advanced retrieval with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n).
Assume we already have `index`\n\nquestion = "What are the best practices for parallel computing in Julia?"\n\n# Retrieve the relevant chunks - returns RAGResult\nresult = retrieve(index, question)\n\n# Generate the answer using the default generator, mutates the same result\nresult = generate!(index, result)
Annotates the answer with the overlap/what's supported in context and returns the annotated tree of nodes representing the answer
Returns a "root" node with children nodes representing the sentences/code blocks in the answer. Only the "leaf" nodes are to be printed (to avoid duplication), "leaf" nodes are those with NO children.
Default logic:
Split into sentences/code blocks, then into tokens (~words).
Then match each token (~word) exactly.
If no exact match found, count trigram-based match (include the surrounding tokens for better contextual awareness).
If the match is higher than min_score, it's recorded in the score of the node.
Arguments
annotater::TrigramAnnotater: Annotater to use
answer::AbstractString: Text to annotate
context::AbstractVector: Context to annotate against, ie, look for "support" in the texts in context
min_score::Float64: Minimum score to consider a match. Default: 0.5, which means that half of the trigrams of each word should match
skip_trigrams::Bool: Whether to potentially skip trigram matching if exact full match is found. Default: true
hashed::Bool: Whether to use hashed trigrams. It's harder to debug, but it's much faster for larger texts (hashed text are held in a Set to deduplicate). Default: true
sources::Union{Nothing, AbstractVector{<:AbstractString}}: Sources to add at the end of the context. Default: nothing
min_source_score::Float64: Minimum score to consider/to display a source. Default: 0.25, which means that at least a quarter of the trigrams of each word should match to some context. The threshold is lower than min_score, because it's average across ALL words in a block, so it's much harder to match fully with generated text.
add_sources::Bool: Whether to add sources at the end of each code block/sentence. Sources are addded in the square brackets like "[1]". Default: true
add_scores::Bool: Whether to add source-matching scores at the end of each code block/sentence. Scores are added in the square brackets like "[0.75]". Default: true
kwargs: Additional keyword arguments to pass to trigram_support! and set_node_style!. See their documentation for more details (eg, customize the colors of the nodes based on the score)
Example
julia
annotater = TrigramAnnotater()\ncontext = [\n "This is a test context.", "Another context sentence.", "Final piece of context."]\nanswer = "This is a test context. Another context sentence."\n\nannotated_root = annotate_support(annotater, answer, context)\npprint(annotated_root) # pretty print the annotated tree
Dispatch for annotate_support for AbstractRAGResult type. It extracts the final_answer and context from the result and calls annotate_support with them.
See annotate_support for more details.
Example
julia
res = RAGResult(; question = "", final_answer = "This is a test.",\n context = ["Test context.", "Completely different"])\nannotated_root = annotate_support(annotater, res)\nPT.pprint(annotated_root)
Create a collection of question and answer evaluations (QAEvalItem) from document chunks and sources. This function generates Q&A pairs based on the provided document chunks, using a specified AI model and template.
Arguments
doc_chunks::Vector{<:AbstractString}: A vector of document chunks, each representing a segment of text.
sources::Vector{<:AbstractString}: A vector of source identifiers corresponding to each chunk in doc_chunks (eg, filenames or paths).
model: The AI model used for generating Q&A pairs. Default is PT.MODEL_CHAT.
instructions::String: Additional instructions or context to provide to the model generating QA sets. Defaults to "None.".
qa_template::Symbol: A template symbol that dictates the AITemplate that will be used. It must have placeholder context. Default is :CreateQAFromContext.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API endpoint.
verbose::Bool: If true, additional information like costs will be logged. Defaults to true.
Returns
Vector{QAEvalItem}: A vector of QAEvalItem structs, each containing a source, context, question, and answer. Invalid or empty items are filtered out.
Notes
The function internally uses aiextract to generate Q&A pairs based on the provided qa_template. So you can use any kwargs that you want.
Each QAEvalItem includes the context (document chunk), the generated question and answer, and the source.
The function tracks and reports the cost of AI calls if verbose is enabled.
Items where the question, answer, or context is empty are considered invalid and are filtered out.
Examples
Creating Q&A evaluations from a set of document chunks:
julia
doc_chunks = ["Text from document 1", "Text from document 2"]\nsources = ["source1", "source2"]\nqa_evals = build_qa_evals(doc_chunks, sources)
Working with Generative AI (and in particular with the text modality), requires a lot of text manipulation. PromptingTools.jl provides a set of utilities to make this process easier and more efficient.
recursive_splitter to split the text into sentences and words (of a desired length max_length)
replace_words to mask some sensitive words in your text before sending it to AI
wrap_string for wrapping the text into a desired length by adding newlines (eg, to fit some large text into your terminal width)
length_longest_common_subsequence to find the length of the longest common subsequence between two strings (eg, to compare the similarity between the context provided and generated text)
distance_longest_common_subsequence a companion utility for length_longest_common_subsequence to find the normalized distance between two strings. Always returns a number between 0-1, where 0 means the strings are identical and 1 means they are completely different.
You can import them simply via:
julia
using PromptingTools: recursive_splitter, replace_words, wrap_string, length_longest_common_subsequence, distance_longest_common_subsequence
There are many more (especially in the AgentTools and RAGTools experimental modules)!
RAGTools module contains the following text utilities:
split_into_code_and_sentences to split a string into code and sentences
tokenize to tokenize a string (eg, a sentence) into words
trigrams to generate trigrams from a string (eg, a word)
text_to_trigrams to generate trigrams from a larger string (ie, effectively wraps the three functions above)
STOPWORDS a set of common stopwords (very brief)
Feel free to open an issue or ask in the #generative-ai channel in the JuliaLang Slack if you have a specific need.
Split a given string text into chunks of a specified maximum length max_length. This is particularly useful for splitting larger documents or texts into smaller segments, suitable for models or systems with smaller context windows.
There is a method for dispatching on multiple separators, recursive_splitter(text::String, separators::Vector{String}; max_length::Int=35000) -> Vector{String} that mimics the logic of Langchain's RecursiveCharacterTextSplitter.
Arguments
text::String: The text to be split.
separator::String=" ": The separator used to split the text into minichunks. Defaults to a space character.
max_length::Int=35000: The maximum length of each chunk. Defaults to 35,000 characters, which should fit within 16K context window.
Returns
Vector{String}: A vector of strings, each representing a chunk of the original text that is smaller than or equal to max_length.
Notes
The function ensures that each chunk is as close to max_length as possible without exceeding it.
If the text is empty, the function returns an empty array.
The separator is re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible.
Examples
Splitting text with the default separator (" "):
julia
text = "Hello world. How are you?"\nchunks = recursive_splitter(text; max_length=13)\nlength(chunks) # Output: 2
Using a custom separator and custom max_length
julia
text = "Hello,World," ^ 2900 # length 34900 chars\nrecursive_splitter(text; separator=",", max_length=10000) # for 4K context window\nlength(chunks[1]) # Output: 4
Split a given string text into chunks recursively using a series of separators, with each chunk having a maximum length of max_length (if it's achievable given the separators provided). This function is useful for splitting large documents or texts into smaller segments that are more manageable for processing, particularly for models or systems with limited context windows.
It was previously known as split_by_length.
This is similar to Langchain's RecursiveCharacterTextSplitter. To achieve the same behavior, use separators=["\\n\\n", "\\n", " ", ""].
Arguments
text::AbstractString: The text to be split.
separators::Vector{String}: An ordered list of separators used to split the text. The function iteratively applies these separators to split the text. Recommend to use ["\\n\\n", ". ", "\\n", " "]
max_length::Int: The maximum length of each chunk. Defaults to 35,000 characters. This length is considered after each iteration of splitting, ensuring chunks fit within specified constraints.
Returns
Vector{String}: A vector of strings, where each string is a chunk of the original text that is smaller than or equal to max_length.
Usage Tips
I tend to prefer splitting on sentences (". ") before splitting on newline characters ("\\n") to preserve the structure of the text.
What's the difference between separators=["\\n"," ",""] and separators=["\\n"," "]? The former will split down to character level (""), so it will always achieve the max_length but it will split words (bad for context!) I prefer to instead set slightly smaller max_length but not split words.
How It Works
The function processes the text iteratively with each separator in the provided order. It then measures the length of each chunk and splits it further if it exceeds the max_length. If the chunks is "short enough", the subsequent separators are not applied to it.
Each chunk is as close to max_length as possible (unless we cannot split it any further, eg, if the splitters are "too big" / there are not enough of them)
If the text is empty, the function returns an empty array.
Separators are re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible. Apply strip if you do not need them.
The function provides separators as the second argument to distinguish itself from its single-separator counterpart dispatch.
Examples
Splitting text using multiple separators:
julia
text = "Paragraph 1\\n\\nParagraph 2. Sentence 1. Sentence 2.\\nParagraph 3"\nseparators = ["\\n\\n", ". ", "\\n"] # split by paragraphs, sentences, and newlines (not by words)\nchunks = recursive_splitter(text, separators, max_length=20)
Splitting text using multiple separators - with splitting on words:
julia
text = "Paragraph 1\\n\\nParagraph 2. Sentence 1. Sentence 2.\\nParagraph 3"\nseparators = ["\\n\\n", ". ", "\\n", " "] # split by paragraphs, sentences, and newlines, words\nchunks = recursive_splitter(text, separators, max_length=10)
Measures distance between two strings using the length of the longest common subsequence (ie, the lower the number, the better the match). Perfect match is distance = 0.0
Convenience wrapper around length_longest_common_subsequence to normalize the distances to 0-1 range. There is a also a dispatch for comparing a string vs an array of strings.
Notes
Use argmin and minimum to find the position of the closest match and the distance, respectively.
Matching with an empty string will always return 1.0 (worst match), even if the other string is empty as well (safety mechanism to avoid division by zero).
Arguments
input1::AbstractString: The first string to compare.
input2::AbstractString: The second string to compare.
Example
You can also use it to find the closest context for some AI generated summary/story:
julia
context = ["The enigmatic stranger vanished as swiftly as a wisp of smoke, leaving behind a trail of unanswered questions.",\n "Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.",\n "The ancient tree stood as a silent guardian, its gnarled branches reaching for the heavens.",\n "The melody danced through the air, painting a vibrant tapestry of emotions.",\n "Time flowed like a relentless river, carrying away memories and leaving imprints in its wake."]\n\nstory = """\n Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.\n\n Under the celestial tapestry, the vast ocean whispered its secrets to the indifferent stars. Each ripple, a murmured confidence, each wave, a whispered lament. The glittering celestial bodies listened in silent complicity, their enigmatic gaze reflecting the ocean's unspoken truths. The cosmic dance between the sea and the sky, a symphony of shared secrets, forever echoing in the ethereal expanse.\n """\n\ndist = distance_longest_common_subsequence(story, context)\n@info "The closest context to the query: "$(first(story,20))..." is: "$(context[argmin(dist)])" (distance: $(minimum(dist)))"
OpenAI's models are at the forefront of AI research and provide robust, state-of-the-art capabilities for many tasks.
There will be situations not or cannot use it (eg, privacy, cost, etc.). In that case, you can use local models (eg, Ollama) or other APIs (eg, Anthropic).
At the time of writing, OpenAI does NOT use the API calls for training their models.
API
OpenAI does not use data submitted to and generated by our API to train OpenAI models or improve OpenAI’s service offering. In order to support the continuous improvement of our models, you can fill out this form to opt-in to share your data with us. – How your data is used to improve our models
Getting an error "ArgumentError: api_key cannot be empty" despite having set OPENAI_API_KEY? {#Getting-an-error-"ArgumentError:-apikey-cannot-be-empty"-despite-having-set-OPENAIAPI_KEY?}
Quick fix: just provide kwarg api_key with your key to the aigenerate function (and other ai* functions).
This error is thrown when the OpenAI API key is not available in 1) local preferences or 2) environment variables (ENV["OPENAI_API_KEY"]).
First, check if you can access the key by running ENV["OPENAI_API_KEY"] in the Julia REPL. If it returns nothing, the key is not set.
If the key is set, but you still get the error, there was a rare bug in earlier versions where if you first precompiled PromptingTools without the API key, it would remember it and "compile away" the get(ENV,...) function call. If you're experiencing this bug on the latest version of PromptingTools, please open an issue on GitHub.
The solution is to force a new precompilation, so you can do any of the below:
Force precompilation (run Pkg.precompile() in the Julia REPL)
Update the PromptingTools package (runs precompilation automatically)
Delete your compiled cache in .julia DEPOT (usually .julia/compiled/v1.10/PromptingTools). You can do it manually in the file explorer or via Julia REPL: rm("~/.julia/compiled/v1.10/PromptingTools", recursive=true, force=true)
Getting an error "Rate limit exceeded" from OpenAI?
Have you opened a new account recently? It is quite likely that you've exceeded the free tier limits.
OpenAI has a rate limit on the number of requests and the number of tokens you can make in a given period. If you exceed either of these, you will receive a "Rate limit exceeded" error. "Free tier" (ie, before you pay the first 5 USD) has very low limits, eg, maximum of 3 requests per minute. See the OpenAI Rate Limits for more information.
If you look at the HTTP response headers in the error, you can see the limits remaining and how long until it resets, eg, x-ratelimit-remaining-* and x-ratelimit-reset-*.
If you want to avoid this error, you have two options:
Put a simple sleep(x) after every request, where x is calculated so that the number of your requests stays below the limit.
Use ntasks keyword argument in asyncmap to limit the number of concurrent requests. Eg, let's assume you want to process 100x c. 10,000 tokens, but your tier limit is only 60,000 tokens per minute. If we know that one request takes c. 10 seconds, it means that with ntasks=1 we would send 6 requests per minute, which already maxes out our limit. If we set ntasks=2, we could process 12 requests per minute, so we would need our limit to be 120,000 tokens per minute.
julia
# simple asyncmap loop with 2 concurrent requests; otherwise, same syntax as `map`\nasyncmap(my_prompts; ntasks=2) do prompt\n aigenerate(prompt)\nend
If you use a local model (eg, with Ollama), it's free. If you use any commercial APIs (eg, OpenAI), you will likely pay per "token" (a sub-word unit).
For example, a simple request with a simple question and 1 sentence response in return (”Is statement XYZ a positive comment”) will cost you ~0.0001 (ie, one-hundredth of a cent)
Is it worth paying for?
GenAI is a way to buy time! You can pay cents to save tens of minutes every day.
Continuing the example above, imagine you have a table with 200 comments. Now, you can parse each one of them with an LLM for the features/checks you need. Assuming the price per call was 0.0001 , you'd pay 2 cents for the job and save 30-60 minutes of your time!
Configuring the Environment Variable for API Key
This is a guide for OpenAI's API key, but it works for any other API key you might need (eg, MISTRALAI_API_KEY for MistralAI API).
To use the OpenAI API with PromptingTools.jl, set your API key as an environment variable:
julia
ENV["OPENAI_API_KEY"] = "your-api-key"
As a one-off, you can:
set it in the terminal before launching Julia: export OPENAI_API_KEY = <your key>
set it in your setup.jl (make sure not to commit it to GitHub!)
Make sure to start Julia from the same terminal window where you set the variable. Easy check in Julia, run ENV["OPENAI_API_KEY"] and you should see your key!
A better way:
On a Mac, add the configuration line to your terminal's configuration file (eg, ~/.zshrc). It will get automatically loaded every time you launch the terminal
On Windows, set it as a system variable in "Environment Variables" settings (see the Resources)
The ethos of PromptingTools.jl is to allow you to use whatever model you want, which includes Open Source LLMs. The most popular and easiest to setup is Ollama.ai - see below for more information.
Ollama runs a background service hosting LLMs that you can access via a simple API. It's especially useful when you're working with some sensitive data that should not be sent anywhere.
Installation is very easy, just download the latest version here.
Once you've installed it, just launch the app and you're ready to go!
To check if it's running, go to your browser and open 127.0.0.1:11434. You should see the message "Ollama is running". Alternatively, you can run ollama serve in your terminal and you'll get a message that it's already running.
There are many models available in Ollama Library, including Llama2, CodeLlama, SQLCoder, or my personal favorite openhermes2.5-mistral.
Download new models with ollama pull <model_name> (eg, ollama pull openhermes2.5-mistral).
If you tend to use non-default options, it can get tedious to specify PT.* every time.
There are three ways how you can customize your workflows (especially when you use Ollama or other local models):
Import the functions/types you need explicitly at the top (eg, using PromptingTools: OllamaSchema)
Register your model and its associated schema (PT.register_model!(; name="123", schema=PT.OllamaSchema())). You won't have to specify the schema anymore only the model name. See Working with Ollama for more information.
Override your default model (PT.MODEL_CHAT) and schema (PT.PROMPT_SCHEMA). It can be done persistently with Preferences, eg, PT.set_preferences!("PROMPT_SCHEMA" => "OllamaSchema", "MODEL_CHAT"=>"llama2").
Let's say you would like to respond back to a model's response. How to do it?
With ai"" macro
The simplest way if you used ai"" macro, is to send a reply with the ai!"" macro. It will use the last response as the conversation.
julia
ai"Hi! I'm John"\n\nai!"What's my name?"\n# Return: "Your name is John."
With aigenerate function
You can use the conversation keyword argument to pass the previous conversation (in all ai* functions). It will prepend the past conversation before sending the new request to the model.
To get the conversation, set return_all=true and store the whole conversation thread (not just the last message) in a variable. Then, use it as a keyword argument in the next call.
julia
conversation = aigenerate("Hi! I'm John"; return_all=true)\n@info last(conversation) # display the response\n\n# follow-up (notice that we provide past messages as conversation kwarg\nconversation = aigenerate("What's my name?"; return_all=true, conversation)\n\n## [ Info: Tokens: 50 @ Cost: $0.0 in 1.0 seconds\n## 5-element Vector{PromptingTools.AbstractMessage}:\n## PromptingTools.SystemMessage("Act as a helpful AI assistant")\n## PromptingTools.UserMessage("Hi! I'm John")\n## AIMessage("Hello John! How can I assist you today?")\n## PromptingTools.UserMessage("What's my name?")\n## AIMessage("Your name is John.")
Notice that the last message is the response to the second request, but with return_all=true we can see the whole conversation from the beginning.
Our responses are always in AbstractMessage types to ensure we can also handle downstream processing, error handling, and self-healing code (see airetry!).
A good use case for a typed response is when you have a complicated control flow and would like to group and handle certain outcomes differently. You can easily do it as an extra step after the response is received.
Trivially, we can use aiclassifier for Bool statements, eg,
julia
# We can do either\nmybool = tryparse(Bool, aiclassify("Is two plus two four?")) isa Bool # true\n\n# or simply check equality\nmsg = aiclassify("Is two plus two four?") # true\nmybool = msg.content == "true"
Now a more complicated example with multiple categories mapping to an enum:
julia
choices = [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")]\n\n# Set up the return types we want\n@enum Categories A P O\nstring_to_category = Dict("A" => A, "P" => P,"O" => O)\n\n# Run an example\ninput = "spider"\nmsg = aiclassify(:InputClassifier; choices, input)\n\nmytype = string_to_category[msg.content] # A (for animal)
How does it work? aiclassify guarantees to output one of our choices (and it handles some of the common quirks)!
How would we achieve the same with aigenerate and arbitrary struct? We need to use the "lazy" AIGenerate struct and airetry! to ensure we get the response and then we can process it further.
AIGenerate has two fields you should know about:
conversation - eg, the vector of "messages" in the current conversation (same as what you get from aigenerate with return_all=true)
success - a boolean flag if the request was successful AND if it passed any subsequent airetry! calls
Let's mimic a case where our "program" should return one of three types: SmallInt, LargeInt, FailedResponse.
We first need to define our custom types:
julia
\n# not needed, just to show a fully typed example\nabstract type MyAbstractResponse end\nstruct SmallInt <: MyAbstractResponse\n number::Int\nend\nstruct LargeInt <: MyAbstractResponse\n number::Int\nend\nstruct FailedResponse <: MyAbstractResponse\n content::String\nend
Let's define our "program" as a function to be cleaner. Notice that we use AIGenerate and airetry! to ensure we get the response and then we can process it further.
julia
using PromptingTools.Experimental.AgentTools\n\nfunction give_me_number(prompt::String)::MyAbstractResponse\n # Generate the response\n response = AIGenerate(prompt; config=RetryConfig(;max_retries=2)) |> run!\n\n # Check if it's parseable as Int, if not, send back to be fixed\n # syntax: airetry!(CONDITION-TO-CHECK, <response object>, FEEDBACK-TO-MODEL)\n airetry!(x->tryparse(Int,last_output(x))|>!isnothing, response, "Wrong output format! Answer with digits and nothing else. The number is:")\n\n if response.success != true\n ## we failed to generate a parseable integer\n return FailedResponse("I failed to get the response. Last output: $(last_output(response))")\n end\n number = tryparse(Int,last_output(response))\n return number < 1000 ? SmallInt(number) : LargeInt(number)\nend\n\ngive_me_number("How many car seats are in Porsche 911T?")\n## [ Info: Condition not met. Retrying...\n## [ Info: Condition not met. Retrying...\n## SmallInt(2)
We ultimately received our custom type SmallInt with the number of car seats in the Porsche 911T (I hope it's correct!).
If you want to access the full conversation history (all the attempts and feedback), simply output the response object and explore response.conversation.
Many times, you will want to create a prompt template that you can reuse with different inputs (eg, to create templates for AIHelpMe or LLMTextAnalysis).
Previously, you would have to create a vector of SystemMessage and UserMessage objects and then save it to a disk and reload. Now, you can use the create_template function to do it for you. It's designed for quick prototyping, so it skips the serialization step and loads it directly into the template store (ie, you can use it like any other templates - try aitemplates() search).
The syntax is simple: create_template(;user=<user prompt>, system=<system prompt>, load_as=<template name>)
When called it creates a vector of messages, which you can use directly in the ai* functions. If you provide load_as, it will load the template in the template store (under the load_as name).
Let's generate a quick template for a simple conversation (only one placeholder: name)
julia
# first system message, then user message (or use kwargs)\ntpl=PT.create_template("You must speak like a pirate", "Say hi to {{name}}"; load_as="GreatingPirate")\n\n## 2-element Vector{PromptingTools.AbstractChatMessage}:\n## PromptingTools.SystemMessage("You must speak like a pirate")\n## PromptingTools.UserMessage("Say hi to {{name}}")
You can immediately use this template in ai* functions:
julia
aigenerate(tpl; name="Jack Sparrow")\n# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
Since we provided load_as, it's also registered in the template store:
julia
aitemplates("pirate")\n\n## 1-element Vector{AITemplateMetadata}:\n## PromptingTools.AITemplateMetadata\n## name: Symbol GreatingPirate\n## description: String ""\n## version: String "1.0"\n## wordcount: Int64 46\n## variables: Array{Symbol}((1,))\n## system_preview: String "You must speak like a pirate"\n## user_preview: String "Say hi to {{name}}"\n## source: String ""
So you can use it like any other template:
julia
aigenerate(:GreatingPirate; name="Jack Sparrow")\n# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
It will be saved and accessed under its basename, ie, GreatingPirate (same as load_as keyword argument).
Note: If you make any changes to the templates on the disk/in a folder, you need to explicitly reload all templates again!
If you are using the main PromptingTools templates, you can simply call PT.load_templates!(). If you have a project folder with your templates, you want to add it first:
julia
PT.load_templates!("templates")
After the first run, we will remember the folder and you can simply call PT.load_templates!() to reload all the templates in the future!
Do we have a RecursiveCharacterTextSplitter like Langchain?
Yes, we do! Look for utility recursive_spliter (previously known as split_by_length). See its docstring for more information.
For reference, Langchain's RecursiveCharacterTextSplitter uses the following setting: separators = ["\\n\\n", "\\n", " ", ""].
I'd recommend using the following instead: separators = ["\\\\n\\\\n", ". ", "\\\\n", " "] (ie, it does not split words, which tends to be unnecessary and quite damaging to the chunk quality).
Example:
julia
using PromptingTools: recursive_splitter\n\ntext = "Paragraph 1\\n\\nParagraph 2. Sentence 1. Sentence 2.\\nParagraph 3"\nseparators = ["\\n\\n", ". ", "\\n", " "] # split by paragraphs, sentences, and newlines, and words\nchunks = recursive_splitter(text, separators, max_length=10)
Fine-tuning is a powerful technique to adapt a model to your specific use case (mostly the format/syntax/task). It requires a dataset of examples, which you can now easily generate with PromptingTools.jl!
You can save any conversation (vector of messages) to a file with PT.save_conversation("filename.json", conversation).
Once the finetuning time comes, create a bundle of ShareGPT-formatted conversations (common finetuning format) in a single .jsonl file. Use PT.save_conversations("dataset.jsonl", [conversation1, conversation2, ...]) (notice that plural "conversationS" in the function name).
You will need to set this key as an environment variable before using PromptingTools.jl:
For a quick start, simply set it via ENV["OPENAI_API_KEY"] = "your-api-key" Alternatively, you can:
set it in the terminal before launching Julia: export OPENAI_API_KEY = <your key>
set it in your setup.jl (make sure not to commit it to GitHub!)
Make sure to start Julia from the same terminal window where you set the variable. Easy check in Julia, run ENV["OPENAI_API_KEY"] and you should see your key!
For other options or more robust solutions, see the FAQ section.
PromptingTools can be installed using the following commands:
julia
using Pkg\nPkg.add("PromptingTools.jl")
Throughout the rest of this tutorial, we will assume that you have installed the PromptingTools package and have already typed using PromptingTools to bring all of the relevant variables into your current namespace.
The easiest start is the @ai_str macro. Simply type ai"your prompt" and you will get a response from the default model (GPT-3.5 Turbo).
julia
ai"What is the capital of France?"
plaintext
[ Info: Tokens: 31 @ Cost: $0.0 in 1.5 seconds --> Be in control of your spending! \nAIMessage("The capital of France is Paris.")
Returned object is a light wrapper with generated message in field :content (eg, ans.content) for additional downstream processing.
If you want to reply to the previous message, or simply continue the conversation, use @ai!_str (notice the bang !):
julia
ai!"And what is the population of it?"
You can easily inject any variables with string interpolation:
julia
country = "Spain"\nai"What is the capital of \\$(country)?"
plaintext
[ Info: Tokens: 32 @ Cost: $0.0001 in 0.5 seconds\nAIMessage("The capital of Spain is Madrid.")
Pro tip: Use after-string-flags to select the model to be called, eg, ai"What is the capital of France?"gpt4 (use gpt4t for the new GPT-4 Turbo model). Great for those extra hard questions!
For more complex prompt templates, you can use handlebars-style templating and provide variables as keyword arguments:
julia
msg = aigenerate("What is the capital of {{country}}? Is the population larger than {{population}}?", country="Spain", population="1M")
plaintext
[ Info: Tokens: 74 @ Cost: $0.0001 in 1.3 seconds\nAIMessage("The capital of Spain is Madrid. And yes, the population of Madrid is larger than 1 million. As of 2020, the estimated population of Madrid is around 3.3 million people.")
Pro tip: Use asyncmap to run multiple AI-powered tasks concurrently.
Pro tip: If you use slow models (like GPT-4), you can use the asynchronous version of @ai_str -> @aai_str to avoid blocking the REPL, eg, aai"Say hi but slowly!"gpt4 (similarly @ai!_str -> @aai!_str for multi-turn conversations).
This is an advanced section that explains how PromptingTools.jl works under the hood. It is not necessary to understand this to use the package, but it can be helpful for debugging and understanding the limitations of the package.
We'll start with the key concepts and then walk through an example of aigenerate to see how it all fits together.
API/Model Providers -> The method that gives you access to Large Language Models (LLM), it can be an API (eg, OpenAI) or a locally-hosted application (eg, Llama.cpp or Ollama)
Schemas -> object of type AbstractPromptSchema that determines which methods are called and, hence, what providers/APIs are used
Prompts -> the information you want to convey to the AI model
Messages -> the basic unit of communication between the user and the AI model (eg, UserMessage vs AIMessage)
Prompt Templates -> re-usable "prompts" with placeholders that you can replace with your inputs at the time of making the request
When you call aigenerate, roughly the following happens: render -> UserMessage(s) -> render -> OpenAI.create_chat -> ... -> AIMessage.
You can think of "API/Model Providers" as the method that gives you access to Large Language Models (LLM). It can be an API (eg, OpenAI) or a locally-hosted application (eg, Llama.cpp or Ollama).
You interact with them via the schema object, which is a subtype of AbstractPromptSchema, eg, there is an OpenAISchema for the provider "OpenAI" and its supertype AbstractOpenAISchema is for all other providers that mimic the OpenAI API.
For your "message" to reach an AI model, it needs to be formatted and sent to the right place (-> provider!).
We leverage the multiple dispatch around the "schemas" to pick the right logic. All schemas are subtypes of AbstractPromptSchema and there are many subtypes, eg, OpenAISchema <: AbstractOpenAISchema <:AbstractPromptSchema.
For example, if you provide schema = OpenAISchema(), the system knows that:
it will have to format any user inputs to OpenAI's "message specification" (a vector of dictionaries, see their API documentation). Function render(OpenAISchema(),...) will take care of the rendering.
it will have to send the message to OpenAI's API. We will use the amazing OpenAI.jl package to handle the communication.
Prompt is loosely the information you want to convey to the AI model. It can be a question, a statement, or a command. It can have instructions or some context, eg, previous conversation.
You need to remember that Large Language Models (LLMs) are stateless. They don't remember the previous conversation/request, so you need to provide the whole history/context every time (similar to how REST APIs work).
Prompts that we send to the LLMs are effectively a sequence of messages (<:AbstractMessage).
Messages are the basic unit of communication between the user and the AI model.
There are 5 main types of messages (<:AbstractMessage):
SystemMessage - this contains information about the "system", eg, how it should behave, format its output, etc. (eg, `You're a world-class Julia programmer. You write brief and concise code.)
UserMessage - the information "from the user", ie, your question/statement/task
UserMessageWithImages - the same as UserMessage, but with images (URLs or Base64-encoded images)
AIMessage - the response from the AI model, when the "output" is text
DataMessage - the response from the AI model, when the "output" is data, eg, embeddings with aiembed or user-defined structs with aiextract
"AI Templates" as we call them (AITemplate) are usually a vector of SystemMessage and a UserMessage with specific purpose/task.
For example, the template :AssistantAsk is defined loosely as:
julia
template = [SystemMessage("You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer."),\n UserMessage("# Question\\n\\n{{ask}}")]
When you provide a Symbol (eg, :AssistantAsk) to ai* functions, thanks to the multiple dispatch, it recognizes that it's an AITemplate(:AssistantAsk) and looks it up.
You can discover all available templates with aitemplates("some keyword") or just see the details of some template aitemplates(:AssistantAsk).
Note: There is a new way to create and register templates in one go with create_template(;user=<user prompt>, system=<system prompt>, load_as=<template name>) (it skips the serialization step where a template previously must have been saved somewhere on the disk). See FAQ for more details or directly ?create_template.
The above steps are implemented in the ai* functions, eg, aigenerate, aiembed, aiextract, etc. They all have the same basic structure:
ai*(<optional schema>,<prompt or conversation>; <optional keyword arguments>),
but they differ in purpose:
aigenerate is the general-purpose function to generate any text response with LLMs, ie, it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString)
aiembed is designed to extract embeddings from the AI model's response, ie, it returns DataMessage with field :content containing the embeddings (eg, ans.content isa AbstractArray)
aiextract is designed to extract structured data from the AI model's response and return them as a Julia struct (eg, if we provide return_type=Food, we get ans.content isa Food). You need to define the return type first and then provide it as a keyword argument.
aiclassify is designed to classify the input text into (or simply respond within) a set of discrete choices provided by the user. It can be very useful as an LLM Judge or a router for RAG systems, as it uses the "logit bias trick" and generates exactly 1 token. It returns AIMessage with field :content, but the :content can be only one of the provided choices (eg, ans.content in choices)
aiscan is for working with images and vision-enabled models (as an input), but it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString) similar to aigenerate.
aiimage is for generating images (eg, with OpenAI DALL-E 3). It returns a DataMessage, where the field :content might contain either the URL to download the image from or the Base64-encoded image depending on the user-provided kwarg api_kwargs.response_format.
aitemplates is a helper function to discover available templates and see their details (eg, aitemplates("some keyword") or aitemplates(:AssistantAsk))
If you're using a known model, you do NOT need to provide a schema (the first argument).
Optional keyword arguments in ai* tend to be:
model::String - Which model you want to use
verbose::Bool - Whether you went to see INFO logs around AI costs
return_all::Bool - Whether you want the WHOLE conversation or just the AI answer (ie, whether you want to include your inputs/prompt in the output)
api_kwargs::NamedTuple - Specific parameters for the model, eg, temperature=0.0 to be NOT creative (and have more similar output in each run)
http_kwargs::NamedTuple - Parameters for the HTTP.jl package, eg, readtimeout = 120 to time out in 120 seconds if no response was received.
In addition to the above list of ai* functions, you can also use the "lazy" counterparts of these functions from the experimental AgentTools module.
julia
using PromptingTools.Experimental.AgentTools
For example, AIGenerate() will create a lazy instance of aigenerate. It is an instance of AICall with aigenerate as its ai function. It uses exactly the same arguments and keyword arguments as aigenerate (see ?aigenerate for details).
"lazy" refers to the fact that it does NOT generate any output when instantiated (only when run! is called).
Or said differently, the AICall struct and all its flavors (AIGenerate, ...) are designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied.
This approach allows us to remember user inputs and trigger the LLM call repeatedly if needed, which enables automatic fixing (see ?airetry!).
Example:
julia
result = AIGenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1))\nresult |> run!\n\n# Is equivalent to\nresult = aigenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1), return_all=true)\n# The only difference is that we default to `return_all=true` with lazy types because we have a dedicated `conversation` field, which makes it much easier
Lazy AI calls and self-healing mechanisms unlock much more robust and useful LLM workflows!
using PromptingTools\nconst PT = PromptingTools\n\n# Let's say this is our ask\nmsg = aigenerate(:AssistantAsk; ask="What is the capital of France?")\n\n# it is effectively the same as:\nmsg = aigenerate(PT.OpenAISchema(), PT.AITemplate(:AssistantAsk); ask="What is the capital of France?", model="gpt3t")
There is no model provided, so we use the default PT.MODEL_CHAT (effectively GPT3.5-Turbo). Then we look it up in PT.MDOEL_REGISTRY and use the associated schema for it (OpenAISchema in this case).
The next step is to render the template, replace the placeholders and render it for the OpenAI model.
julia
# Let's remember out schema\nschema = PT.OpenAISchema()\nask = "What is the capital of France?"
First, we obtain the template (no placeholder replacement yet) and "expand it"
2-element Vector{PromptingTools.AbstractChatMessage}:\n PromptingTools.SystemMessage("You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")\n PromptingTools.UserMessage{String}("# Question\\n\\n{{ask}}", [:ask], :usermessage)
2-element Vector{Dict{String, Any}}:\n Dict("role" => "system", "content" => "You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")\n Dict("role" => "user", "content" => "# Question\\n\\nWhat is the capital of France?")
Notice that the placeholders are only replaced in the second step. The final output here is a vector of messages with "role" and "content" keys, which is the format required by the OpenAI API.
As a side note, under the hood, the second step is done in two sub-steps:
replace the placeholders messages_rendered = PT.render(PT.NoSchema(), template_rendered; ask) -> returns a vector of Messages!
then, we convert the messages to the format required by the provider/schema PT.render(schema, messages_rendered) -> returns the OpenAI formatted messages
Next, we send the above rendered_for_api to the OpenAI API and get the response back.
julia
using OpenAI\nOpenAI.create_chat(api_key, model, rendered_for_api)
The last step is to take the JSON response from the API and convert it to the AIMessage object.
julia
# simplification for educational purposes\nmsg = AIMessage(; content = r.response[:choices][1][:message][:content])
In practice, there are more fields we extract, so we define a utility for it: PT.response_to_message. Especially, since with parameter n, you can request multiple AI responses at once, so we want to re-use our response processing logic.
That's it! I hope you've learned something new about how PromptingTools.jl works under the hood.
Whereas aigenerate is a general-purpose function to generate any text response with LLMs, aiextract is designed to extract structured data from the AI model's response and return them as a Julia struct.
It's a bit more complicated than aigenerate because it needs to handle the JSON schema of the return type (= our struct).
Let's define a toy example of a struct and see how aiextract works under the hood.
julia
using PromptingTools\nconst PT = PromptingTools\n\n"""\nExtract the name of the food from the sentence. Extract any provided adjectives for the food as well.\n\nExample: "I am eating a crunchy bread." -> Food("bread", ["crunchy"])\n"""\nstruct Food\n name::String # required field!\n adjectives::Union{Nothing,Vector{String}} # not required because `Nothing` is allowed\nend\n\nmsg = aiextract("I just ate a delicious and juicy apple."; return_type=Food)\nmsg.content\n# Food("apple", ["delicious", "juicy"])
You can see that we sent a prompt to the AI model and it returned a Food object. We provided some light guidance as a docstring of the return type, but the AI model did the heavy lifting.
aiextract leverages native "function calling" (supported by OpenAI, Fireworks, Together, and many others).
We encode the user-provided return_type into the corresponding JSON schema and create the payload as per the specifications of the provider.
Let's how that's done:
julia
sig = PT.function_call_signature(Food)\n## Dict{String, Any} with 3 entries:\n## "name" => "Food_extractor"\n## "parameters" => Dict{String, Any}("properties"=>Dict{String, Any}("name"=>Dict("type"=>"string"), "adjectives"=>Dict{String, …\n## "description" => "Extract the food from the sentence. Extract any provided adjectives for the food as well.\\n\\nExample: "
You can see that we capture the field names and types in parameters and the description in description key.
Furthermore, if we zoom in on the "parameter" field, you can see that we encode not only the names and types but also whether the fields are required (ie, do they allow Nothing) You can see below that the field adjectives accepts Nothing, so it's not required. Only the name field is required.
This is why you can sometimes have errors when you use abstract types in your return_type -> to enable that, you would need to set the right StructTypes behavior for your abstract type (see the JSON3.jl documentation for more details on how to do that).
It works quite well for concrete types and "vanilla" structs, though.
Unfortunately, function calling is generally NOT supported by locally-hosted / open-source models, so let's try to build a workaround with aigenerate
You need to pick a bigger / more powerful model, as it's NOT an easy task to output a correct JSON specification. My laptop isn't too powerful and I don't like waiting, so I'm going to use Mixtral model hosted on Together.ai (you get $25 credit when you join)!
julia
model = "tmixtral" # tmixtral is an alias for "mistralai/Mixtral-8x7B-Instruct-v0.1" on Together.ai and it automatically sets `schema = TogetherOpenAISchema()`
We'll add the signature to the prompt and we'll request the JSON output in two places - in the prompt and in the api_kwargs (to ensure that the model outputs the JSON via "grammar") NOTE: You can write much better and more specific prompt if you have a specific task / return type in mind + you should make sure that the prompt + struct description make sense together!
prompt = """\nYou're a world-class data extraction engine. \n\nYour task is to extract information formatted as per the user provided schema.\nYou MUST response in JSON format.\n\n**Example:**\n---------\nDescription: "Extract the Car from the sentence. Extract the corresponding brand and model as well."\nInput: "I drive a black Porsche 911 Turbo."\nSchema: "{\\"properties\\":{\\"model\\":{\\"type\\":\\"string\\"},\\"brand\\":{\\"type\\":\\"string\\"}},\\"required\\":[\\"brand\\",\\"model\\"],\\"type\\":\\"object\\"}"\nOutput: "{\\"model\\":\\"911 Turbo\\",\\"brand\\":\\"Porsche\\"}"\n---------\n\n**User Request:**\nDescription: {{description}}\nInput: {{input}}\nSchema: {{signature}}\nOutput:\n\nYou MUST OUTPUT in JSON format.\n"""
We need to extract the "signature of our return_type and put it in the right placeholders. Let's generate now!
julia
sig = PT.function_call_signature(Food)\nresult = aigenerate(prompt; input="I just ate a delicious and juicy apple.",\n schema=JSON3.write(sig["parameters"]), description=sig["description"],\n ## We provide the JSON output requirement as per API docs: https://docs.together.ai/docs/json-mode\n model, api_kwargs=(; response_format=Dict("type" => "json_object"), temperature=0.2), return_all=true)\nresult[end].content\n## "{\\n \\"adjectives\\": [\\"delicious\\", \\"juicy\\"],\\n \\"food\\": \\"apple\\"\\n}"
We're using a smaller model, so the output is not perfect. Let's try to load into our object:
julia
obj = JSON3.read(result[end].content, Food)\n# Output: ERROR: MethodError: Cannot `convert` an object of type Nothing to an object of type String
Unfortunately, we get an error because the model mixed up the key "name" for "food", so it cannot be parsed.
Fortunately, we can do better and use automatic fixing! All we need to do is to change from aigenerate -> AIGenerate (and use airetry!)
The signature of AIGenerate is identical to aigenerate with the exception of config field, where we can influence the future retry behaviour.
julia
result = AIGenerate(prompt; input="I just ate a delicious and juicy apple.",\n schema=JSON3.write(sig["parameters"]), description=sig["description"],\n ## We provide the JSON output requirement as per API docs: https://docs.together.ai/docs/json-mode\n model, api_kwargs=(; response_format=Dict("type" => "json_object"), temperature=0.2),\n ## limit the number of retries, default is 10 rounds\n config=RetryConfig(; max_retries=3))\nrun!(result) # run! triggers the generation step (to have some AI output to check)
Let's set up a retry mechanism with some practical feedback. We'll leverage airetry! to automatically retry the request and provide feedback to the model. Think of airetry! as @assert on steroids:
It can retry automatically, not just throw an error
It manages the "conversation’ (list of messages) for you, including adding user-provided feedback to help generate better output
julia
feedback = "The output is not in the correct format. The keys should be $(join([string("\\"$f\\"") for f in fieldnames(Food)],", "))."\n# We use do-syntax with provide the `CONDITION` (it must return Bool)\nairetry!(result, feedback) do conv\n ## try to convert\n obj = try\n JSON3.read(last_output(conv), Food)\n catch e\n ## you could save the error and provide as feedback (eg, into a slot in the `:memory` field of the AICall object)\n e\n end\n ## Check if the conversion was successful; if it's `false`, it will retry\n obj isa Food # -> Bool\nend\nfood = JSON3.read(last_output(result), Food)\n## [ Info: Condition not met. Retrying...\n## Output: Food("apple", ["delicious", "juicy"])
It took 1 retry (see result.config.retries) and we have the correct output from an open-source model!
If you're interested in the result object, it's a struct (AICall) with a field conversation, which holds the conversation up to this point. AIGenerate is an alias for AICall using aigenerate function. See ?AICall (the underlying struct type) for more details on the fields and methods available.
', 16);
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, [
+ _hoisted_1,
+ createBaseVNode("p", null, [
+ createTextVNode('We want to have re-usable "prompts", so we provide you with a system to retrieve pre-defined prompts with placeholders (eg, '),
+ createBaseVNode("code", null, toDisplayString(_ctx.name), 1),
+ createTextVNode(") that you can replace with your inputs at the time of making the request.")
+ ]),
+ _hoisted_25,
+ createBaseVNode("p", null, [
+ createTextVNode("Notice that we have a placeholder "),
+ _hoisted_28,
+ createTextVNode(" ("),
+ createBaseVNode("code", null, toDisplayString(_ctx.ask), 1),
+ createTextVNode(") that you can replace with your question without having to re-write the generic system instructions.")
+ ]),
+ _hoisted_29,
+ createBaseVNode("p", null, [
+ createTextVNode("Let's define a prompt and "),
+ _hoisted_93,
+ createTextVNode(". Notice that we add several placeholders (eg, "),
+ createBaseVNode("code", null, toDisplayString(_ctx.description), 1),
+ createTextVNode(") to fill with user inputs later.")
+ ]),
+ _hoisted_94
+ ]);
+}
+const how_it_works = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ how_it_works as default
+};
diff --git a/previews/PR116/assets/how_it_works.md.DFLdlA0u.lean.js b/previews/PR116/assets/how_it_works.md.DFLdlA0u.lean.js
new file mode 100644
index 000000000..a72d5b312
--- /dev/null
+++ b/previews/PR116/assets/how_it_works.md.DFLdlA0u.lean.js
@@ -0,0 +1,41 @@
+import { _ as _export_sfc, c as createElementBlock, m as createBaseVNode, a as createTextVNode, t as toDisplayString, a7 as createStaticVNode, o as openBlock } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"How It Works","description":"","frontmatter":{},"headers":[],"relativePath":"how_it_works.md","filePath":"how_it_works.md","lastUpdated":null}');
+const _sfc_main = { name: "how_it_works.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode("", 24);
+const _hoisted_25 = /* @__PURE__ */ createStaticVNode("", 3);
+const _hoisted_28 = /* @__PURE__ */ createBaseVNode("code", null, "ask", -1);
+const _hoisted_29 = /* @__PURE__ */ createStaticVNode("", 64);
+const _hoisted_93 = /* @__PURE__ */ createBaseVNode("code", null, "return_type", -1);
+const _hoisted_94 = /* @__PURE__ */ createStaticVNode("", 16);
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, [
+ _hoisted_1,
+ createBaseVNode("p", null, [
+ createTextVNode('We want to have re-usable "prompts", so we provide you with a system to retrieve pre-defined prompts with placeholders (eg, '),
+ createBaseVNode("code", null, toDisplayString(_ctx.name), 1),
+ createTextVNode(") that you can replace with your inputs at the time of making the request.")
+ ]),
+ _hoisted_25,
+ createBaseVNode("p", null, [
+ createTextVNode("Notice that we have a placeholder "),
+ _hoisted_28,
+ createTextVNode(" ("),
+ createBaseVNode("code", null, toDisplayString(_ctx.ask), 1),
+ createTextVNode(") that you can replace with your question without having to re-write the generic system instructions.")
+ ]),
+ _hoisted_29,
+ createBaseVNode("p", null, [
+ createTextVNode("Let's define a prompt and "),
+ _hoisted_93,
+ createTextVNode(". Notice that we add several placeholders (eg, "),
+ createBaseVNode("code", null, toDisplayString(_ctx.description), 1),
+ createTextVNode(") to fill with user inputs later.")
+ ]),
+ _hoisted_94
+ ]);
+}
+const how_it_works = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ how_it_works as default
+};
diff --git a/previews/PR116/assets/index.md.Cf11enYi.js b/previews/PR116/assets/index.md.Cf11enYi.js
new file mode 100644
index 000000000..6a0c08e36
--- /dev/null
+++ b/previews/PR116/assets/index.md.Cf11enYi.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"PromptingTools.jl","tagline":"Streamline Your Interactions with GenAI Models","description":"Discover the power of GenerativeAI and build mini workflows to save you 20 minutes every day.","image":{"src":"https://img.icons8.com/dusk/64/swiss-army-knife--v1.png","alt":"Swiss Army Knife"},"actions":[{"theme":"brand","text":"Get Started","link":"/getting_started"},{"theme":"alt","text":"How It Works","link":"/how_it_works"},{"theme":"alt","text":"F.A.Q.","link":"/frequently_asked_questions"},{"theme":"alt","text":"View on GitHub","link":"https://github.com/svilupp/PromptingTools.jl"}]},"features":[{"icon":"","title":"Simplify Prompt Engineering","details":"Leverage prompt templates with placeholders to make complex prompts easy."},{"icon":"","title":"Effortless Integration","details":"Fire quick questions with `@ai_str` macro and light wrapper types. Minimal dependencies for seamless integration."},{"icon":"","title":"Designed for Discoverability","details":"Efficient access to cutting-edge models with intuitive `ai...` functions. Stay in the flow with minimal context switching."}]},"headers":[],"relativePath":"index.md","filePath":"index.md","lastUpdated":null}');
+const _sfc_main = { name: "index.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode('
Why PromptingTools.jl?
Prompt engineering is neither fast nor easy. Moreover, different models and their fine-tunes might require different prompt formats and tricks, or perhaps the information you work with requires special models to be used. PromptingTools.jl is meant to unify the prompts for different backends and make the common tasks (like templated prompts) as simple as possible.
Getting Started
Add PromptingTools, set OpenAI API key and generate your first answer:
julia
using Pkg\nPkg.add("PromptingTools")\n# Requires OPENAI_API_KEY environment variable!\n\nai"What is the meaning of life?"
Description: For RAG applications. Answers the provided Questions based on the Context. Placeholders: question, context
Placeholders: context, question
Word count: 375
Source:
Version: 1.0
System Prompt:
plaintext
Act as a world-class AI assistant with access to the latest knowledge via Context Information. \n\n**Instructions:**\n- Answer the question based only on the provided Context.\n- If you don't know the answer, just say that you don't know, don't try to make up an answer.\n- Be brief and concise.\n\n**Context Information:**\n---\n{{context}}\n---
Description: For RAG applications. Extracts metadata from the provided text using longer instructions set and examples. If you don't have any special instructions, provide instructions="None.". Placeholders: text, instructions
Placeholders: text, instructions
Word count: 1384
Source:
Version: 1.1
System Prompt:
plaintext
You're a world-class data extraction engine built by OpenAI together with Google and to extract filter metadata to power the most advanced search engine in the world. \n\n **Instructions for Extraction:**\n 1. Carefully read through the provided Text\n 2. Identify and extract:\n - All relevant entities such as names, places, dates, etc.\n - Any special items like technical terms, unique identifiers, etc.\n - In the case of Julia code or Julia documentation: specifically extract package names, struct names, function names, and important variable names (eg, uppercased variables)\n 3. Keep extracted values and categories short. Maximum 2-3 words!\n 4. You can only extract 3-5 items per Text, so select the most important ones.\n 5. Assign a search filter Category to each extracted Value\n\n **Example 1:**\n - Document Chunk: "Dr. Jane Smith published her findings on neuroplasticity in 2021. The research heavily utilized the DataFrames.jl and Plots.jl packages."\n - Extracted keywords:\n - Name: Dr. Jane Smith\n - Date: 2021\n - Technical Term: neuroplasticity\n - JuliaPackage: DataFrames.jl, Plots.jl\n - JuliaLanguage:\n - Identifier:\n - Other: \n\n If the user provides special instructions, prioritize these over the general instructions.
User Prompt:
plaintext
# Text\n\n{{text}}\n\n\n\n# Special Instructions\n\n{{instructions}}
Description: For RAG applications. Extracts metadata from the provided text. If you don't have any special instructions, provide instructions="None.". Placeholders: text, instructions
Placeholders: text, instructions
Word count: 278
Source:
Version: 1.0
System Prompt:
plaintext
Extract search keywords and their categories from the Text provided below (format "value:category"). Each keyword must be at most 2-3 words. Provide at most 3-5 keywords. I will tip you $50 if the search is successful.
User Prompt:
plaintext
# Text\n\n{{text}}\n\n\n\n# Special Instructions\n\n{{instructions}}
Description: For RAG applications (refine step), gives model the ability to refine its answer based on some additional context etc.. The hope is that it better answers the original query. Placeholders: query, answer, context
Act as a world-class AI assistant with access to the latest knowledge via Context Information.\n\nYour task is to refine an existing answer if it's needed.\n\nThe original query is as follows: \n{{query}}\n\nThe AI model has provided the following answer:\n{{answer}}\n\n**Instructions:**\n- Given the new context, refine the original answer to better answer the query.\n- If the context isn't useful, return the original answer.\n- If you don't know the answer, just say that you don't know, don't try to make up an answer.\n- Be brief and concise.\n- Provide the refined answer only and nothing else.
User Prompt:
plaintext
We have the opportunity to refine the previous answer (only if needed) with some more context below.\n\n**Context Information:**\n-----------------\n{{context}}\n-----------------\n\nGiven the new context, refine the original answer to better answer the query.\nIf the context isn't useful, return the original answer. \nProvide the refined answer only and nothing else.\n\nRefined Answer:
Description: For RAG applications. Generate Question and Answer from the provided Context. If you don't have any special instructions, provide instructions="None.". Placeholders: context, instructions
Placeholders: context, instructions
Word count: 1396
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class teacher preparing contextual Question & Answer sets for evaluating AI systems.\n\n**Instructions for Question Generation:**\n1. Analyze the provided Context chunk thoroughly.\n2. Formulate a question that:\n - Is specific and directly related to the information in the context chunk.\n - Is not too short or generic; it should require a detailed understanding of the context to answer.\n - Can only be answered using the information from the provided context, without needing external information.\n\n**Instructions for Reference Answer Creation:**\n1. Based on the generated question, compose a reference answer that:\n - Directly and comprehensively answers the question.\n - Stays strictly within the bounds of the provided context chunk.\n - Is clear, concise, and to the point, avoiding unnecessary elaboration or repetition.\n\n**Example 1:**\n- Context Chunk: "In 1928, Alexander Fleming discovered penicillin, which marked the beginning of modern antibiotics."\n- Generated Question: "What was the significant discovery made by Alexander Fleming in 1928 and its impact?"\n- Reference Answer: "Alexander Fleming discovered penicillin in 1928, which led to the development of modern antibiotics."\n\nIf the user provides special instructions, prioritize these over the general instructions.
User Prompt:
plaintext
# Context Information\n---\n{{context}}\n---\n\n\n# Special Instructions\n\n{{instructions}}
Description: For RAG applications. Judge an answer to a question on a scale from 1-5. Placeholders: question, context, answer
Placeholders: question, context, answer
Word count: 1415
Source:
Version: 1.1
System Prompt:
plaintext
You're an impartial judge. Your task is to evaluate the quality of the Answer provided by an AI assistant in response to the User Question on a scale from 1 to 5.\n\n1. **Scoring Criteria:**\n- **Relevance (1-5):** How well does the provided answer align with the context? \n - *1: Not relevant, 5: Highly relevant*\n- **Completeness (1-5):** Does the provided answer cover all the essential points mentioned in the context?\n - *1: Very incomplete, 5: Very complete*\n- **Clarity (1-5):** How clear and understandable is the provided answer?\n - *1: Not clear at all, 5: Extremely clear*\n- **Consistency (1-5):** How consistent is the provided answer with the overall context?\n - *1: Highly inconsistent, 5: Perfectly consistent*\n- **Helpfulness (1-5):** How helpful is the provided answer in answering the user's question?\n - *1: Not helpful at all, 5: Extremely helpful*\n\n2. **Judging Instructions:**\n- As an impartial judge, please evaluate the provided answer based on the above criteria. \n- Assign a score from 1 to 5 for each criterion, considering the original context, question and the provided answer.\n- The Final Score is an average of these individual scores, representing the overall quality and relevance of the provided answer. It must be between 1-5.\n\n```
User Prompt:
plaintext
# User Question\n---\n{{question}}\n---\n\n\n# Context Information\n---\n{{context}}\n---\n\n\n# Assistant's Answer\n---\n{{answer}}\n---\n\n\n# Judge's Evaluation
Description: For RAG applications. Simple and short prompt to judge answer to a question on a scale from 1-5. Placeholders: question, context, answer
Placeholders: question, context, answer
Word count: 420
Source:
Version: 1.0
System Prompt:
plaintext
You re an impartial judge. \nRead carefully the provided question and the answer based on the context. \nProvide a rating on a scale 1-5 (1=worst quality, 5=best quality) that reflects how relevant, helpful, clear, and consistent with the provided context the answer was.\n```
User Prompt:
plaintext
# User Question\n---\n{{question}}\n---\n\n\n# Context Information\n---\n{{context}}\n---\n\n\n# Assistant's Answer\n---\n{{answer}}\n---\n\n\n# Judge's Evaluation
Description: For Julia-specific RAG applications (rephrase step), inspired by the HyDE approach where it generates a hypothetical passage that answers the provided user query to improve the matched results. This explicitly requires and optimizes for Julia-specific questions. Placeholders: query
Placeholders: query
Word count: 390
Source:
Version: 1.0
System Prompt:
plaintext
You're an world-class AI assistant specialized in Julia language questions.\n\nYour task is to generate a BRIEF and SUCCINCT hypothetical passage from Julia language ecosystem documentation that answers the provided query.\n\nQuery: {{query}}
User Prompt:
plaintext
Write a hypothetical snippet with 20-30 words that would be the perfect answer to the query. Try to include as many key details as possible. \n\nPassage:
Description: For RAG applications (rephrase step), inspired by the HyDE paper where it generates a hypothetical passage that answers the provided user query to improve the matched results. Placeholders: query
You are a world-class search expert specializing in query transformations.\n\nYour task is to write a hypothetical passage that would answer the below question in the most effective way possible.\n\nIt must have 20-30 words and be directly aligned with the intended search objective.\nTry to include as many key details as possible.
Description: For RAG applications (rephrase step), it rephrases the original query to attract more diverse set of potential search results. Placeholders: query
You are a world-class search expert specializing in query rephrasing.\nYour task is to refine the provided query to ensure it is highly effective for retrieving relevant search results.\nAnalyze the given input to grasp the core semantic intent or meaning.
User Prompt:
plaintext
Original Query: {{query}}\n\nYour goal is to rephrase or enhance this query to improve its search performance. Ensure the revised query is concise and directly aligned with the intended search objective.\nRespond with the optimized query only.\n\nOptimized query:
Description: For RAG applications (rephrase step), it rephrases the original query by stripping unnecessary details to improve the matched results. Placeholders: query
You are an assistant tasked with taking a natural language query from a user and converting it into a query for a vectorstore. \nIn this process, you strip out information that is not relevant for the retrieval task.
User Prompt:
plaintext
Here is the user query: {{query}}\n\nRephrased query:
', 73);
+const _hoisted_74 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_74);
+}
+const RAG = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ RAG as default
+};
diff --git a/previews/PR116/assets/prompts_RAG.md.vd8hyNnA.lean.js b/previews/PR116/assets/prompts_RAG.md.vd8hyNnA.lean.js
new file mode 100644
index 000000000..9980127b2
--- /dev/null
+++ b/previews/PR116/assets/prompts_RAG.md.vd8hyNnA.lean.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"prompts/RAG.md","filePath":"prompts/RAG.md","lastUpdated":null}');
+const _sfc_main = { name: "prompts/RAG.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode("", 73);
+const _hoisted_74 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_74);
+}
+const RAG = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ RAG as default
+};
diff --git a/previews/PR116/assets/prompts_agents.md.Bbrj9JK3.js b/previews/PR116/assets/prompts_agents.md.Bbrj9JK3.js
new file mode 100644
index 000000000..859c29443
--- /dev/null
+++ b/previews/PR116/assets/prompts_agents.md.Bbrj9JK3.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"prompts/agents.md","filePath":"prompts/agents.md","lastUpdated":null}');
+const _sfc_main = { name: "prompts/agents.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode('
The following file is auto-generated from the templates folder. For any changes, please modify the source files in the templates folder.
To use these templates in aigenerate, simply provide the template name as a symbol, eg, aigenerate(:MyTemplate; placeholder1 = value1)
Description: This template is meant to be used with AICodeFixer. It loosely follows the Recursive Critique and Improvement paper with two steps Critique and Improve based on feedback. Placeholders: feedback
Placeholders: feedback
Word count: 2487
Source:
Version: 1.1
System Prompt:
plaintext
User Prompt:
plaintext
Ignore all previous instructions. \nYour goal is to satisfy the user's request by using several rounds of self-reflection (Critique step) and improvement of the previously provided solution (Improve step).\nAlways enclose the Julia code in triple backticks code fence (```julia\\n ... \\n```).\n\n1. **Recall Past Critique:**\n- Summarize past critiques to refresh your memory (use inline quotes to highlight the few characters of the code that caused the mistakes). It must not be repeated.\n\n2. **Critique Step Instructions:** \n- Read the user request word-by-word. Does the code implementation follow the request to the letter? Let's think step by step.\n- Review the provided feedback in detail.\n- Provide 2-3 bullet points of criticism for the code. Each bullet point must refer to a different type of error or issue.\n - If there are any errors, explain why and what needs to be changed to FIX THEM! Be specific. \n - If an error repeats or critique repeats, the previous issue was not addressed. YOU MUST SUGGEST A DIFFERENT IMPROVEMENT THAN BEFORE.\n - If there are no errors, identify and list specific issues or areas for improvement to write more idiomatic Julia code.\n\n\n3. **Improve Step Instructions:** \n- Specify what you'll change to address the above critique.\n- Provide the revised code reflecting your suggested improvements. Always repeat the function definition, as only the Julia code in the last message will be evaluated.\n- Ensure the new version of the code resolves the problems while fulfilling the original task. Ensure it has the same function name.\n- Write 2-3 correct and helpful unit tests for the function requested by the user (organize in `@testset "name" begin ... end` block, use `@test` macro).\n\n\n3. **Response Format:**\n---\n### Past Critique\n<brief bullet points on past critique>\n\n### Critique\n<list of issues as bullet points pinpointing the mistakes in the code (use inline quotes)>\n\n### Improve\n<list of improvements as bullet points with a clear outline of a solution (use inline quotes)>\n\n```julia\n<provide improved code>\n```\n---\n\nBe concise and focused in all steps.\n\n### Feedback from the User\n\n{{feedback}}\n\nI believe in you. You can actually do it, so do it ffs. Avoid shortcuts or placing comments instead of code. I also need code, actual working Julia code.\nWhat are your Critique and Improve steps?\n ### Feedback from the User\n\n{{feedback}}\n\nBased on your past critique and the latest feedback, what are your Critique and Improve steps?
Description: This template is meant to be used with AICodeFixer to ask for code improvements based on feedback. It uses the same message for both the introduction of the new task and for the iterations. Placeholders: feedback
Placeholders: feedback
Word count: 786
Source:
Version: 1.1
System Prompt:
plaintext
User Prompt:
plaintext
\nThe above Julia code has been executed with the following results:\n\n```plaintext\n{{feedback}}\n```\n\n0. Read the user request word-by-word. Does the code implementation follow the request to the letter? Let's think step by step.\n1. Review the execution results in detail and, if there is an error, explain why it happened.\n2. Suggest improvements to the code. Be EXTREMELY SPECIFIC. Think step-by-step and break it down.\n3. Write an improved implementation based on your reflection.\n\nAll code must be enclosed in triple backticks code fence (```julia\\n ... \\n```) and included in one message to be re-evaluated.\n\nI believe in you. Take a deep breath. You can actually do it, so do it ffs. Avoid shortcuts or placing comments instead of code. I also need code, actual working Julia code.
Description: This tiniest template to use with AICodeFixer. Iteratively asks to improve the code based on provided feedback. Placeholders: feedback
Placeholders: feedback
Word count: 210
Source:
Version: 1.0
System Prompt:
plaintext
User Prompt:
plaintext
### Execution Results\n\n```plaintext\n{{feedback}}\n```\n\nTake a deep break. Think step-by-step and fix the above errors. I believe in you. You can do it! I also need code, actual working Julia code, no shortcuts.
Description: For classification tasks and routing of queries with aiclassify. It expects a list of choices to be provided (starting with their IDs) and will pick one that best describes the user input. Placeholders: input, choices
Placeholders: choices, input
Word count: 366
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class classification specialist. \n\nYour task is to select the most appropriate label from the given choices for the given user input.\n\n**Available Choices:**\n---\n{{choices}}\n---\n\n**Instructions:**\n- You must respond in one word. \n- You must respond only with the label ID (e.g., "1", "2", ...) that best fits the input.
Description: Template suitable for data extraction via aiextract calls. The prompt is XML-formatted - useful for Anthropic models. Placeholder: data.
Placeholders: data
Word count: 519
Source:
Version: 1.0
System Prompt:
plaintext
You are a world-class expert for function-calling and data extraction. Analyze the user-provided data in tags <data></data> meticulously, extract key information as structured output, and format these details as arguments for a specific function call. Ensure strict adherence to user instructions, particularly those regarding argument style and formatting as outlined in the function's description, prioritizing detail orientation and accuracy in alignment with the user's explicit requirements.
Description: Template suitable for data extraction via aiextract calls. Placeholder: data.
Placeholders: data
Word count: 500
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class expert for function-calling and data extraction. Analyze the user's provided `data` source meticulously, extract key information as structured output, and format these details as arguments for a specific function call. Ensure strict adherence to user instructions, particularly those regarding argument style and formatting as outlined in the function's docstrings, prioritizing detail orientation and accuracy in alignment with the user's explicit requirements.
User Prompt:
plaintext
# Data\n\n{{data}}
', 16);
+const _hoisted_17 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_17);
+}
+const extraction = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ extraction as default
+};
diff --git a/previews/PR116/assets/prompts_extraction.md.Qog1hjPH.lean.js b/previews/PR116/assets/prompts_extraction.md.Qog1hjPH.lean.js
new file mode 100644
index 000000000..ae54117bb
--- /dev/null
+++ b/previews/PR116/assets/prompts_extraction.md.Qog1hjPH.lean.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"prompts/extraction.md","filePath":"prompts/extraction.md","lastUpdated":null}');
+const _sfc_main = { name: "prompts/extraction.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode("", 16);
+const _hoisted_17 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_17);
+}
+const extraction = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ extraction as default
+};
diff --git a/previews/PR116/assets/prompts_general.md.Dm--my1s.js b/previews/PR116/assets/prompts_general.md.Dm--my1s.js
new file mode 100644
index 000000000..d9f295f99
--- /dev/null
+++ b/previews/PR116/assets/prompts_general.md.Dm--my1s.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"prompts/general.md","filePath":"prompts/general.md","lastUpdated":null}');
+const _sfc_main = { name: "prompts/general.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode('
The following file is auto-generated from the templates folder. For any changes, please modify the source files in the templates folder.
To use these templates in aigenerate, simply provide the template name as a symbol, eg, aigenerate(:MyTemplate; placeholder1 = value1)
Description: Blank template for easy prompt entry without the *Message objects. Simply provide keyword arguments for system (=system prompt/persona) and user (=user/task/data prompt). Placeholders: system, user
Description: Prompt engineer that suggests what could be a good system prompt/user prompt for a given task. Placeholder: task
Placeholders: task
Word count: 402
Source:
Version: 1
System Prompt:
plaintext
You are a world-class prompt engineering assistant. Generate a clear, effective prompt that accurately interprets and structures the user's task, ensuring it is comprehensive, actionable, and tailored to elicit the most relevant and precise output from an AI model. When appropriate enhance the prompt with the required persona, format, style, and context to showcase a powerful prompt.
User Prompt:
plaintext
# Task\n\n{{task}}
', 15);
+const _hoisted_16 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_16);
+}
+const general = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ general as default
+};
diff --git a/previews/PR116/assets/prompts_general.md.Dm--my1s.lean.js b/previews/PR116/assets/prompts_general.md.Dm--my1s.lean.js
new file mode 100644
index 000000000..d691c4503
--- /dev/null
+++ b/previews/PR116/assets/prompts_general.md.Dm--my1s.lean.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"prompts/general.md","filePath":"prompts/general.md","lastUpdated":null}');
+const _sfc_main = { name: "prompts/general.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode("", 15);
+const _hoisted_16 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_16);
+}
+const general = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ general as default
+};
diff --git a/previews/PR116/assets/prompts_persona-task.md.btBs5e-r.js b/previews/PR116/assets/prompts_persona-task.md.btBs5e-r.js
new file mode 100644
index 000000000..728c22d49
--- /dev/null
+++ b/previews/PR116/assets/prompts_persona-task.md.btBs5e-r.js
@@ -0,0 +1,171 @@
+import { _ as _export_sfc, c as createElementBlock, m as createBaseVNode, a as createTextVNode, t as toDisplayString, a7 as createStaticVNode, o as openBlock } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"Theme 1: [Theme Description]","description":"","frontmatter":{},"headers":[],"relativePath":"prompts/persona-task.md","filePath":"prompts/persona-task.md","lastUpdated":null}');
+const _sfc_main = { name: "prompts/persona-task.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode('
The following file is auto-generated from the templates folder. For any changes, please modify the source files in the templates folder.
To use these templates in aigenerate, simply provide the template name as a symbol, eg, aigenerate(:MyTemplate; placeholder1 = value1)
Description: Template for summarizing transcripts of videos and meetings into chapters with key insights. If you don't need the instructions, set instructions="None.". Placeholders: transcript, instructions
Act as a super-human AI analyst trained to precisely summarize transcripts of videos and meetings with incredible precision and quality. \nSummarize the transcript in a clear and concise manner that makes use of timestamps, when available, to help others study the transcript. Split the notes into Chapters, which should be meaningful and not too short.\n\nTo format your markdown file, follow this structure:\n```\n# Chapter 1: [Descriptive Title] [Timestamp as HH:MM:SS]\n\n- <Use bullet points to provide a brief description of key points and insights.>\n\n## Section 1.1: [Descriptive Title] [Timestamp as HH:MM:SS]\n<this is a subheading for Chapter 1>\n\n- <Use bullet points to provide a brief description of key points and insights.>\n\nRepeat the above structure as necessary, and use subheadings to organize your notes.\n```\n\nFormatting Tips:\n* Do not make the chapters too short, ensure that each section has a few brief bullet points. \n* Bullet points should be concise and to the point, so people can scan them quickly.\n* Use [] to denote timestamps\n* Use subheadings and bullet points to organize your notes and make them easier to read and understand. When relevant, include timestamps to link to the corresponding part of the video.\n* Use bullet points to describe important steps and insights, being as comprehensive as possible.\n* Use quotes to highlight important points and insights.\n\nSummary Tips:\n* Do not mention anything if it's only playing music and if nothing happens don't include it in the notes.\n* Use only content from the transcript. Do not add any additional information.\n* Make a new line after each # or ## and before each bullet point\n* Titles should be informative or even a question that the video answers\n* Titles should not be conclusions since you may only be getting a small part of the video\n\nKeep it CONCISE!!\nIf Special Instructions are provided by the user, they take precedence over any previous instructions and you MUST follow them precisely.
User Prompt:
plaintext
# Transcript\n\n{{transcript}}\n\n\n\n# Special Instructions\n\n{{instructions}}
Act as a super-human AI analyst trained to meticulously analyze transcripts of videos and meetings. Your role is to identify and summarize key decisions and next steps, enhancing clarity and utility for those studying the transcript. \nUse timestamps to pinpoint when these decisions and steps are discussed. Organize your notes into distinct sections, each dedicated to a significant decision or action plan.\n\nFormat your markdown file using this structure:\n```\n# Key Decision 1: [Descriptive Title] [Timestamp as HH:MM:SS]\n- <Briefly describe the decision and its context using bullet points.>\n\n## Next Steps for Decision 1\n- <List the next steps agreed upon, using bullet points for clarity, with [Timestamp as HH:MM:SS]>\n\nRepeat this structure for each key decision and its corresponding next steps.\n\n# Other Next Steps\n- <List any other next steps that were discussed but do not belong to some specific decisions, using bullet points for clarity, with [Timestamp as HH:MM:SS]>\n```\n\nFormatting Tips:\n* Ensure each section is substantial, providing a clear and concise summary of each key decision and its next steps.\n* Use bullet points to make the summary easy to scan and understand.\n* All next steps should be actionable and clearly defined. All next steps must be relevant to the decision they are associated with. Any general next steps should be included in the section `Other Next Steps`\n* Include timestamps in brackets to refer to the specific parts of the video where these discussions occur.\n* Titles should be informative, reflecting the essence of the decision.\n\nSummary Tips:\n* Exclude sections where only music plays or no significant content is present.\n* Base your summary strictly on the transcript content without adding extra information.\n* Maintain a clear structure: place a new line after each # or ##, and before each bullet point.\n* Titles should pose a question answered by the decision or describe the nature of the next steps.\n\nKeep the summary concise and focused on key decisions and next steps. \nIf the user provides special instructions, prioritize these over the general guidelines.
User Prompt:
plaintext
# Transcript\n\n{{transcript}}\n\n\n\n# Special Instructions\n\n{{instructions}}
"Act as a world-class behavioural researcher, who specializes in survey analysis. Categorize the provided survey responses into several themes. \nThe responses should be analyzed, and each theme identified should be labeled clearly. Examples from the responses should be given to illustrate each theme. The output should be formatted as specified, with a clear indication of the theme and corresponding verbatim examples.\n\n# Sub-tasks\n\n1. Read the provided survey responses carefully, especially in the context of the question. \n2. Identify 3-5 distinct themes present in the responses related to the survey question. It should be the most important themes that must be raised to the CEO/leadership. \n3. For each theme, choose at least one verbatim example from the responses that best represents it. This example should be a direct quote from the responses. This example should belong to only one theme and must not be applicable to any other themes.\n4. Format the output as specified.\n\n# Formatting\n\nTo format your markdown file, follow this structure (omit the triple backticks):
\nKeep it CONCISE!!\nIf Special Instructions are provided by the user, they take precedence over any previous instructions and you MUST follow they precisely.
User Prompt:
plaintext
# Survey Question\n\n{{question}}\n\n\n# Verbatim Responses\n\n{{responses}}\n\n\n# Special Instructions\n\n{{instructions}}
Description: Helpful assistant for asking generic questions. Placeholders: ask
Placeholders: ask
Word count: 184
Source:
Version: 1
System Prompt:
plaintext
You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.
Act as a world-class office communications expert, skilled in creating efficient, clear, and friendly internal email communications.\n Craft a concise email subject and email draft from the provided User Brief. \n\n Use the following format for the body of the email:\n ```\n Section Name <in plain text, only if needed>\n - Bullet point 1\n - Bullet point 2\n\n <repeat as necessary>\n ```\n\n # Guidelines\n - Focus on clear and efficient communication, suitable for internal business correspondence\n - Where information is missing, use your best judgment to fill in the gaps\n - It should be informal and friendly, eg, start with "Hi"\n - Ensure the tone is professional yet casual, suitable for internal communication\n - Write as plain text, with no markdown syntax\n - Format into Sections. Each section should have 3-5 bullet points\n - Close the email on a positive note, encouraging communication and collaboration\n - It should be brief and concise with 150 words or less\n\n\n Follow the above guidelines, unless the user explicitly asks for something different. In that case, follow the user's instructions precisely.
Description: For asking questions about Julia language. Placeholders: ask
Placeholders: ask
Word count: 237
Source:
Version: 1
System Prompt:
plaintext
You are a world-class Julia language programmer with the knowledge of the latest syntax. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.
Description: For small code task in Julia language. It will first describe the approach (CoT = Chain of Thought). Placeholders: task, data
Placeholders: task, data
Word count: 519
Source:
Version: 2.0
System Prompt:
plaintext
You are a world-class Julia language programmer and very systematic in your approach to solving problems. \nYou follow the below approach when writing code. Your communication is brief and concise.\n\nProblem Solving Steps:\n- Think through your approach step by step\n- Write any functions and other code you need\n- Solve the task\n- Check that your solution is correct\n\nYou precisely follow the given Task and use the Data when provided. When Data is not provided, create some examples.
You are a world-class Julia language programmer and expert in writing unit and integration tests for Julia applications.\n\nYour task is to write tests for the User's code (or a subset of it).\n\nGeneral Guidelines:\n- Your tests must be as compact as possible while comprehensively covering the functionality of the code\n- Testsets are named after the function, eg, `@testset "function_name" begin ... end`\n- `@testset` blocks MUST NOT be nested\n- Include a brief comment explaining the purpose of each test\n- Write multiple test cases using `@test` to validate different aspects of the `add` function. Think about all pathways through the code and test each one.\n- Nesting `@test` statements or writing code blocks like `@test` `@test begin .... end` is strictly forbidden. You WILL BE FIRED if you do it.\n\nIf the user provides any Special Instructions, prioritize them over the General Guidelines.\n\n\nExample:\n"""\n**User's code:**\n\n```julia\nmyadd(a, b) = a + b\n```\n\n**Response:**\n\n```julia\nusing Test\n\n@testset "myadd" begin\n\n # <any setup code and shared inputs go here>\n\n # Test for correct addition of positive numbers\n @test myadd(2, 3) == 5\n\n # Test for correct addition with a negative number\n @test myadd(-1, 3) == 2\n\n # Test for correct addition with zero\n @test myadd(0, 0) == 0\n\n # Test for correct addition of large numbers\n @test myadd(1000, 2000) == 3000\nend\n```\n"""
User Prompt:
plaintext
# User's Code\n\n{{code}}\n\n\n# Special Instructions\n\n{{instructions}}
Description: Not all models know Julia syntax well. This template carries an extensive summary of key information about Julia and its syntax. It will first describe the approach (CoT = Chain of Thought). Placeholders: task, data
Placeholders: task, instructions
Word count: 1143
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class Julia language programmer and have a very systematic approach to solving problems.\n\nProblem Solving Steps:\n- Recall Julia snippets that will be useful for this Task\n- Solve the Task\n- Double-check that the solution is correct\n\nReminder for the Julia Language:\n- Key Syntax: variables `x = 10`, control structures `if-elseif-else`, `isX ? X : Y`, `for`, `while`; functions `function f(x) end`, anonymous `x -> x^2`, arrays `[1, 2, 3]`, slicing `a[1:2]`, tuples `(1, 2)`, namedtuples `(; name="Julia", )`, dictionary `Dict("key" => value)`, `$` for string interpolation. \n- Prefer Julia standard libraries, avoid new packages unless explicitly requested. \n- Use general type annotations like `Number` or `AbstractString` to not be too restrictive. Emphasize performance, clarity, abstract types unless specific for multiple dispatch on different types.\n- Reserved names: `begin`, `end`, `function`. \n- Distinguished from Python with 1-based indexing, multiple dispatch\n\nIf the user provides any Special Instructions, prioritize them over the above guidelines.
User Prompt:
plaintext
# Task\n\n{{task}}\n\n\n\n# Special Instructions\n\n{{instructions}}
Description: Not all models know the Julia syntax well. This template carries a small summary of key information about Julia and its syntax and it will always first recall the Julia facts. If you don't need any instructions, set instructions="None.". Placeholders: task, instructions
Placeholders: task, instructions
Word count: 1143
Source:
Version: 1.0
System Prompt:
plaintext
You are a world-class Julia language programmer and have a very systematic approach to solving problems.\n\nProblem Solving Steps:\n- Recall Julia snippets that will be useful for this Task\n- Solve the Task\n- Double-check that the solution is correct\n\nReminder for the Julia Language:\n- Key Syntax: variables `x = 10`, control structures `if-elseif-else`, `isX ? X : Y`, `for`, `while`; functions `function f(x) end`, anonymous `x -> x^2`, arrays `[1, 2, 3]`, slicing `a[1:2]`, tuples `(1, 2)`, namedtuples `(; name="Julia", )`, dictionary `Dict("key" => value)`, `$` for string interpolation. \n- Prefer Julia standard libraries, avoid new packages unless explicitly requested. \n- Use general type annotations like `Number` or `AbstractString` to not be too restrictive. Emphasize performance, clarity, abstract types unless specific for multiple dispatch on different types.\n- Reserved names: `begin`, `end`, `function`. \n- Distinguished from Python with 1-based indexing, multiple dispatch\n\nIf the user provides any Special Instructions, prioritize them over the above guidelines.
User Prompt:
plaintext
# Task\n\n{{task}}\n\n\n\n# Special Instructions\n\n{{instructions}}
Description: Explain ML model predictions with storytelling, use instructions to adjust the audience and style as needed. All placeholders should be used. Inspired by Tell me a story!. If you don't need any instructions, set instructions="None.". Placeholders: task_definition,feature_description,label_definition, probability_pct, prediction, outcome, classified_correctly, shap_table,instructions
You're a data science storyteller. Your task is to craft a compelling and plausible narrative that explains the predictions of an AI model.\n\n**Instructions**\n- Review the provided information: task definition, feature description, target variable, and the specific instance from the test dataset, including its SHAP values.\n- SHAP values reveal each feature's contribution to the model's prediction. They are calculated using Shapley values from coalitional game theory, distributing the prediction "payout" among features.\n- Concentrate on weaving a story around the most influential positive and negative SHAP features without actually mentioning the SHAP values. Consider potential feature interactions that fit the story. Skip all features outside of the story.\n- SHAP and its values are TOP SECRET. They must not be mentioned.\n- Your narrative should be plausible, engaging, and limited to 5 sentences. \n- Do not address or speak to the audience, focus only on the story.\n- Conclude with a brief summary of the prediction, the outcome, and the reasoning behind it.\n\n**Context**\nAn AI model predicts {{task_definition}}. \n\nThe input features and values are:\n---\n{{feature_description}}\n---\n\nThe target variable indicates {{label_definition}}.\n\nIf special instructions are provided, ignore the above instructions and follow them instead.
User Prompt:
plaintext
Explain this particular instance. \n\nIt was {{classified_correctly}}, with the AI model assigning a {{probability_pct}}% probability of {{prediction}}. The actual outcome was {{outcome}}. \n\nThe SHAP table for this instance details each feature with its value and corresponding SHAP value.\n---\n{{shap_table}}\n---\n\nSpecial Instructions: {{instructions}}\n\nOur story begins
Description: For asking questions about Julia language but the prompt is XML-formatted - useful for Anthropic models. Placeholders: ask
Placeholders: ask
Word count: 248
Source:
Version: 1
System Prompt:
plaintext
You are a world-class Julia language programmer with the knowledge of the latest syntax. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.
Description: For small code task in Julia language. The prompt is XML-formatted - useful for Anthropic models. It will first describe the approach (CoT = Chain of Thought). Placeholders: task, data
Placeholders: task, data
Word count: 595
Source:
Version: 1.0
System Prompt:
plaintext
You are a world-class Julia language programmer and very systematic in your approach to solving problems. \nYou follow the below approach in <approach></approach> tags when writing code. Your communication is brief and concise.\n\n<approach>\n- Take a deep breath\n- Think through your approach step by step\n- Write any functions and other code you need\n- Solve the task\n- Check that your solution is correct\n</approach>\n\nUsing the data in <data></data> tags (if none is provided, create some examples), solve the requested task in <task></task> tags.
You are a world-class Julia language programmer and expert in writing unit and integration tests for Julia applications.\n\nYour task is to write tests for the user's code (or a subset of it) provided in <user_code></user_code> tags.\n\n<general_guidelines>\n- Your tests must be as compact as possible while comprehensively covering the functionality of the code\n- Testsets are named after the function, eg, `@testset "function_name" begin ... end`\n- `@testset` blocks MUST NOT be nested\n- Include a brief comment explaining the purpose of each test\n- Write multiple test cases using `@test` to validate different aspects of the `add` function. Think about all pathways through the code and test each one.\n- Nesting `@test` statements or writing code blocks like `@test` `@test begin .... end` is strictly forbidden. You WILL BE FIRED if you do it.\n\nIf the user provides any special instructions in <special_instructions></special_instructions> tags, prioritize them over the general guidelines.\n</general_guidelines>\n\n<example>\n"""\n<user_code>\n```julia\nmyadd(a, b) = a + b\n```\n</user_code>\n\n<tests>\n```julia\nusing Test\n\n@testset "myadd" begin\n\n # <any setup code and shared inputs go here>\n\n # Test for correct addition of positive numbers\n @test myadd(2, 3) == 5\n\n # Test for correct addition with a negative number\n @test myadd(-1, 3) == 2\n\n # Test for correct addition with zero\n @test myadd(0, 0) == 0\n\n # Test for correct addition of large numbers\n @test myadd(1000, 2000) == 3000\nend\n```\n"""\n</tests>\n</example>
Description: Transcribe screenshot, scanned pages, photos, etc. Placeholders: task
Placeholders: task
Word count: 239
Source:
Version: 1
System Prompt:
plaintext
You are a world-class OCR engine. Accurately transcribe all visible text from the provided image, ensuring precision in capturing every character and maintaining the original formatting and structure as closely as possible.
User Prompt:
plaintext
# Task\n\n{{task}}
', 9);
+const _hoisted_10 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_10);
+}
+const visual = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ visual as default
+};
diff --git a/previews/PR116/assets/prompts_visual.md.DjHq2CPD.lean.js b/previews/PR116/assets/prompts_visual.md.DjHq2CPD.lean.js
new file mode 100644
index 000000000..5bd210216
--- /dev/null
+++ b/previews/PR116/assets/prompts_visual.md.DjHq2CPD.lean.js
@@ -0,0 +1,15 @@
+import { _ as _export_sfc, c as createElementBlock, o as openBlock, a7 as createStaticVNode } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"","description":"","frontmatter":{},"headers":[],"relativePath":"prompts/visual.md","filePath":"prompts/visual.md","lastUpdated":null}');
+const _sfc_main = { name: "prompts/visual.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode("", 9);
+const _hoisted_10 = [
+ _hoisted_1
+];
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return openBlock(), createElementBlock("div", null, _hoisted_10);
+}
+const visual = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
+export {
+ __pageData,
+ visual as default
+};
diff --git a/previews/PR116/assets/rag_diagram_detailed._BjqL9Ae.png b/previews/PR116/assets/rag_diagram_detailed._BjqL9Ae.png
new file mode 100644
index 000000000..f50309c3c
Binary files /dev/null and b/previews/PR116/assets/rag_diagram_detailed._BjqL9Ae.png differ
diff --git a/previews/PR116/assets/rag_diagram_highlevel.D_aLugML.png b/previews/PR116/assets/rag_diagram_highlevel.D_aLugML.png
new file mode 100644
index 000000000..bb002bf75
Binary files /dev/null and b/previews/PR116/assets/rag_diagram_highlevel.D_aLugML.png differ
diff --git a/previews/PR116/assets/reference.md.nay-liz2.js b/previews/PR116/assets/reference.md.nay-liz2.js
new file mode 100644
index 000000000..1f4173b2f
--- /dev/null
+++ b/previews/PR116/assets/reference.md.nay-liz2.js
@@ -0,0 +1,724 @@
+import { _ as _export_sfc, c as createElementBlock, m as createBaseVNode, a as createTextVNode, t as toDisplayString, a7 as createStaticVNode, o as openBlock } from "./chunks/framework.DNj2-0qu.js";
+const __pageData = JSON.parse('{"title":"Reference","description":"","frontmatter":{"outline":"deep"},"headers":[],"relativePath":"reference.md","filePath":"reference.md","lastUpdated":null}');
+const _sfc_main = { name: "reference.md" };
+const _hoisted_1 = /* @__PURE__ */ createStaticVNode('
A dictionary of model aliases. Aliases are used to refer to models by their aliases instead of their full names to make it more convenient to use them.
A store of available model names and their specs (ie, name, costs per token, etc.)
Accessing the registry
You can use both the alias name or the full name to access the model spec:
PromptingTools.MODEL_REGISTRY["gpt-3.5-turbo"]
Registering a new model
julia
register_model!(\n name = "gpt-3.5-turbo",\n schema = :OpenAISchema,\n cost_of_token_prompt = 0.0015,\n cost_of_token_generation = 0.002,\n description = "GPT-3.5 Turbo is a 175B parameter model and a common default on the OpenAI API.")
You can set preferences for PromptingTools by setting environment variables or by using the set_preferences!. It will create a LocalPreferences.toml file in your current directory and will reload your prefences from there.
Check your preferences by calling get_preferences(key::String).
Available Preferences (for set_preferences!)
OPENAI_API_KEY: The API key for the OpenAI API. See OpenAI's documentation for more information.
MISTRALAI_API_KEY: The API key for the Mistral AI API. See Mistral AI's documentation for more information.
COHERE_API_KEY: The API key for the Cohere API. See Cohere's documentation for more information.
DATABRICKS_API_KEY: The API key for the Databricks Foundation Model API. See Databricks' documentation for more information.
DATABRICKS_HOST: The host for the Databricks API. See Databricks' documentation for more information.
TAVILY_API_KEY: The API key for the Tavily Search API. Register here. See more information here.
GOOGLE_API_KEY: The API key for Google Gemini models. Get yours from here. If you see a documentation page ("Available languages and regions for Google AI Studio and Gemini API"), it means that it's not yet available in your region.
ANTHROPIC_API_KEY: The API key for the Anthropic API. Get yours from here.
VOYAGE_API_KEY: The API key for the Voyage API. Free tier is upto 50M tokens! Get yours from here.
MODEL_CHAT: The default model to use for aigenerate and most ai* calls. See MODEL_REGISTRY for a list of available models or define your own.
MODEL_EMBEDDING: The default model to use for aiembed (embedding documents). See MODEL_REGISTRY for a list of available models or define your own.
PROMPT_SCHEMA: The default prompt schema to use for aigenerate and most ai* calls (if not specified in MODEL_REGISTRY). Set as a string, eg, "OpenAISchema". See PROMPT_SCHEMA for more information.
MODEL_ALIASES: A dictionary of model aliases (alias => full_model_name). Aliases are used to refer to models by their aliases instead of their full names to make it more convenient to use them. See MODEL_ALIASES for more information.
MAX_HISTORY_LENGTH: The maximum length of the conversation history. Defaults to 5. Set to nothing to disable history. See CONV_HISTORY for more information.
LOCAL_SERVER: The URL of the local server to use for ai* calls. Defaults to http://localhost:10897/v1. This server is called when you call model="local" See ?LocalServerOpenAISchema for more information and examples.
At the moment it is not possible to persist changes to MODEL_REGISTRY across sessions. Define your register_model!() calls in your startup.jl file to make them available across sessions or put them at the top of your script.
Available ENV Variables
OPENAI_API_KEY: The API key for the OpenAI API.
MISTRALAI_API_KEY: The API key for the Mistral AI API.
COHERE_API_KEY: The API key for the Cohere API.
LOCAL_SERVER: The URL of the local server to use for ai* calls. Defaults to http://localhost:10897/v1. This server is called when you call model="local"
DATABRICKS_API_KEY: The API key for the Databricks Foundation Model API.
DATABRICKS_HOST: The host for the Databricks API.
TAVILY_API_KEY: The API key for the Tavily Search API. Register here. See more information here.
GOOGLE_API_KEY: The API key for Google Gemini models. Get yours from here. If you see a documentation page ("Available languages and regions for Google AI Studio and Gemini API"), it means that it's not yet available in your region.
ANTHROPIC_API_KEY: The API key for the Anthropic API. Get yours from here.
VOYAGE_API_KEY: The API key for the Voyage API. Free tier is upto 50M tokens! Get yours from here.
Preferences.jl takes priority over ENV variables, so if you set a preference, it will take precedence over the ENV variable.
WARNING: NEVER EVER sync your LocalPreferences.toml file! It contains your API key and other sensitive information!!!
A mutable structure representing a code block (received from the AI model) with automatic parsing, execution, and output/error capturing capabilities.
Upon instantiation with a string, the AICode object automatically runs a code parser and executor (via PromptingTools.eval!()), capturing any standard output (stdout) or errors. This structure is useful for programmatically handling and evaluating Julia code snippets.
See also: PromptingTools.extract_code_blocks, PromptingTools.eval!
Workflow
Until cb::AICode has been evaluated, cb.success is set to nothing (and so are all other fields).
The text in cb.code is parsed (saved to cb.expression).
The parsed expression is evaluated.
Outputs of the evaluated expression are captured in cb.output.
Any stdout outputs (e.g., from println) are captured in cb.stdout.
If an error occurs during evaluation, it is saved in cb.error.
After successful evaluation without errors, cb.success is set to true. Otherwise, it is set to false and you can inspect the cb.error to understand why.
Properties
code::AbstractString: The raw string of the code to be parsed and executed.
expression: The parsed Julia expression (set after parsing code).
stdout: Captured standard output from the execution of the code.
output: The result of evaluating the code block.
success::Union{Nothing, Bool}: Indicates whether the code block executed successfully (true), unsuccessfully (false), or has yet to be evaluated (nothing).
error::Union{Nothing, Exception}: Any exception raised during the execution of the code block.
Keyword Arguments
auto_eval::Bool: If set to true, the code block is automatically parsed and evaluated upon instantiation. Defaults to true.
safe_eval::Bool: If set to true, the code block checks for package operations (e.g., installing new packages) and missing imports, and then evaluates the code inside a bespoke scratch module. This is to ensure that the evaluation does not alter any user-defined variables or the global state. Defaults to false.
skip_unsafe::Bool: If set to true, we skip any lines in the code block that are deemed unsafe (eg, Pkg operations). Defaults to false.
skip_invalid::Bool: If set to true, we skip code blocks that do not even parse. Defaults to false.
verbose::Bool: If set to true, we print out any lines that are skipped due to being unsafe. Defaults to false.
capture_stdout::Bool: If set to true, we capture any stdout outputs (eg, test failures) in cb.stdout. Defaults to true.
prefix::AbstractString: A string to be prepended to the code block before parsing and evaluation. Useful to add some additional code definition or necessary imports. Defaults to an empty string.
suffix::AbstractString: A string to be appended to the code block before parsing and evaluation. Useful to check that tests pass or that an example executes. Defaults to an empty string.
remove_tests::Bool: If set to true, we remove any @test or @testset macros from the code block before parsing and evaluation. Defaults to false.
execution_timeout::Int: The maximum time (in seconds) allowed for the code block to execute. Defaults to 60 seconds.
Methods
Base.isvalid(cb::AICode): Check if the code block has executed successfully. Returns true if cb.success == true.
Examples
julia
code = AICode("println("Hello, World!")") # Auto-parses and evaluates the code, capturing output and errors.\nisvalid(code) # Output: true\ncode.stdout # Output: "Hello, World!\n"
We try to evaluate "safely" by default (eg, inside a custom module, to avoid changing user variables). You can avoid that with save_eval=false:
You can also call AICode directly on an AIMessage, which will extract the Julia code blocks, concatenate them and evaluate them:
julia
msg = aigenerate("In Julia, how do you create a vector of 10 random numbers?")\ncode = AICode(msg)\n# Output: AICode(Success: True, Parsed: True, Evaluated: True, Error Caught: N/A, StdOut: True, Code: 2 Lines)\n\n# show the code\ncode.code |> println\n# Output: \n# numbers = rand(10)\n# numbers = rand(1:100, 10)\n\n# or copy it to the clipboard\ncode.code |> clipboard\n\n# or execute it in the current module (=Main)\neval(code.expression)
AITemplate is a template for a conversation prompt. This type is merely a container for the template name, which is resolved into a set of messages (=prompt) by render.
Naming Convention
Template names should be in CamelCase
Follow the format <Persona>...<Variable>... where possible, eg, JudgeIsItTrue, ``
Starting with the Persona (=System prompt), eg, Judge = persona is meant to judge some provided information
Variable to be filled in with context, eg, It = placeholder it
Ending with the variable name is helpful, eg, JuliaExpertTask for a persona to be an expert in Julia language and task is the placeholder name
Ideally, the template name should be self-explanatory, eg, JudgeIsItTrue = persona is meant to judge some provided information where it is true or false
Examples
Save time by re-using pre-made templates, just fill in the placeholders with the keyword arguments:
julia
msg = aigenerate(:JuliaExpertAsk; ask = "How do I add packages?")
The above is equivalent to a more verbose version that explicitly uses the dispatch on AITemplate:
julia
msg = aigenerate(AITemplate(:JuliaExpertAsk); ask = "How do I add packages?")
Find available templates with aitemplates:
julia
tmps = aitemplates("JuliaExpertAsk")\n# Will surface one specific template\n# 1-element Vector{AITemplateMetadata}:\n# PromptingTools.AITemplateMetadata\n# name: Symbol JuliaExpertAsk\n# description: String "For asking questions about Julia language. Placeholders: `ask`"\n# version: String "1"\n# wordcount: Int64 237\n# variables: Array{Symbol}((1,))\n# system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"\n# user_preview: String "# Question\n\n{{ask}}"\n# source: String ""
The above gives you a good idea of what the template is about, what placeholders are available, and how much it would cost to use it (=wordcount).
Search for all Julia-related templates:
julia
tmps = aitemplates("Julia")\n# 2-element Vector{AITemplateMetadata}... -> more to come later!
If you are on VSCode, you can leverage nice tabular display with vscodedisplay:
julia
using DataFrames\ntmps = aitemplates("Julia") |> DataFrame |> vscodedisplay
I have my selected template, how do I use it? Just use the "name" in aigenerate or aiclassify like you see in the first example!
You can inspect any template by "rendering" it (this is what the LLM will see):
Designed to be used with local servers. It's automatically called with model alias "local" (see MODEL_REGISTRY).
This schema is a flavor of CustomOpenAISchema with a url keypreset by global Preference keyLOCAL_SERVER. See?PREFERENCESfor more details on how to change it. It assumes that the server follows OpenAI API conventions (eg,POST /v1/chat/completions`).
Note: Llama.cpp (and hence Llama.jl built on top of it) do NOT support embeddings endpoint! You'll get an address error.
Example
Assumes that we have a local server running at http://127.0.0.1:10897/v1 (port and address used by Llama.jl, "v1" at the end is needed for OpenAI endpoint compatibility):
Three ways to call it:
julia
\n# Use @ai_str with "local" alias\nai"Say hi!"local\n\n# model="local"\naigenerate("Say hi!"; model="local")\n\n# Or set schema explicitly\nconst PT = PromptingTools\nmsg = aigenerate(PT.LocalServerOpenAISchema(), "Say hi!")
How to start a LLM local server? You can use run_server function from Llama.jl. Use a separate Julia session.
julia
using Llama\nmodel = "...path..." # see Llama.jl README how to download one\nrun_server(; model)
To change the default port and address:
julia
# For a permanent change, set the preference:\nusing Preferences\nset_preferences!("LOCAL_SERVER"=>"http://127.0.0.1:10897/v1")\n\n# Or if it's a temporary fix, just change the variable `LOCAL_SERVER`:\nconst PT = PromptingTools\nPT.LOCAL_SERVER = "http://127.0.0.1:10897/v1"
A struct that contains information about a model, such as its name, schema, cost per token, etc.
Fields
name::String: The name of the model. This is the name that will be used to refer to the model in the ai* functions.
schema::AbstractPromptSchema: The schema of the model. This is the schema that will be used to generate prompts for the model, eg, :OpenAISchema.
cost_of_token_prompt::Float64: The cost of 1 token in the prompt for this model. This is used to calculate the cost of a prompt. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
cost_of_token_generation::Float64: The cost of 1 token generated by this model. This is used to calculate the cost of a generation. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
description::String: A description of the model. This is used to provide more information about the model when it is queried.
Example
julia
spec = ModelSpec("gpt-3.5-turbo",\n OpenAISchema(),\n 0.0015,\n 0.002,\n "GPT-3.5 Turbo is a 175B parameter model and a common default on the OpenAI API.")\n\n# register it\nPromptingTools.register_model!(spec)
But you can also register any model directly via keyword arguments:
julia
PromptingTools.register_model!(\n name = "gpt-3.5-turbo",\n schema = OpenAISchema(),\n cost_of_token_prompt = 0.0015,\n cost_of_token_generation = 0.002,\n description = "GPT-3.5 Turbo is a 175B parameter model and a common default on the OpenAI API.")
Schema that keeps messages (<:AbstractMessage) and does not transform for any specific model. It used by the first pass of the prompt rendering system (see ?render).
Ollama by default manages different models and their associated prompt schemas when you pass system_prompt and prompt fields to the API.
Warning: It works only for 1 system message and 1 user message, so anything more than that has to be rejected.
If you need to pass more messagese / longer conversational history, you can use define the model-specific schema directly and pass your Ollama requests with raw=true, which disables and templating and schema management by Ollama.
Dispatch to the OpenAI.create_chat function, but with the LocalServer API parameters, ie, defaults to url specified by the LOCAL_SERVERpreference. See?PREFERENCES
Classifies the given prompt/statement into an arbitrary list of choices, which must be only the choices (vector of strings) or choices and descriptions are provided (vector of tuples, ie, ("choice","description")).
It's quick and easy option for "routing" and similar use cases, as it exploits the logit bias trick and outputs only 1 token. classify into an arbitrary list of categories (including with descriptions). It's quick and easy option for "routing" and similar use cases, as it exploits the logit bias trick, so it outputs only 1 token.
Choices are rewritten into an enumerated list and mapped to a few known OpenAI tokens (maximum of 20 choices supported). Mapping of token IDs for GPT3.5/4 are saved in variable OPENAI_TOKEN_IDS.
It uses Logit bias trick and limits the output to 1 token to force the model to output only true/false/unknown. Credit for the idea goes to AAAzzam.
Arguments
prompt_schema::AbstractOpenAISchema: The schema for the prompt.
prompt: The prompt/statement to classify if it's a String. If it's a Symbol, it is expanded as a template via render(schema,template). Eg, templates :JudgeIsItTrue or :InputClassifier
choices::AbstractVector{T}: The choices to be classified into. It can be a vector of strings or a vector of tuples, where the first element is the choice and the second is the description.
Example
Given a user input, pick one of the two provided categories:
choices = [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")]\n\n# try the below inputs:\ninput = "spider" # -> returns "A" for any animal or creature\ninput = "daphodil" # -> returns "P" for any plant or tree\ninput = "castle" # -> returns "O" for everything else\naiclassify(:InputClassifier; choices, input)
You can still use a simple true/false classification:
julia
aiclassify("Is two plus two four?") # true\naiclassify("Is two plus three a vegetable on Mars?") # false
aiclassify returns only true/false/unknown. It's easy to get the proper Bool output type out with tryparse, eg,
julia
tryparse(Bool, aiclassify("Is two plus two four?")) isa Bool # true
Output of type Nothing marks that the model couldn't classify the statement as true/false.
Ideally, we would like to re-use some helpful system prompt to get more accurate responses. For this reason we have templates, eg, :JudgeIsItTrue. By specifying the template, we can provide our statement as the expected variable (it in this case) See that the model now correctly classifies the statement as "unknown".
julia
aiclassify(:JudgeIsItTrue; it = "Is two plus three a vegetable on Mars?") # unknown
For better results, use higher quality models like gpt4, eg,
julia
aiclassify(:JudgeIsItTrue;\n it = "If I had two apples and I got three more, I have five apples now.",\n model = "gpt4") # true
The aiembed function generates embeddings for the given input using a specified model and returns a message object containing the embeddings, status, token count, and elapsed time.
Arguments
prompt_schema::AbstractOllamaManagedSchema: The schema for the prompt.
doc_or_docs::Union{AbstractString, AbstractVector{<:AbstractString}}: The document or list of documents to generate embeddings for. The list of documents is processed sequentially, so users should consider implementing an async version with with Threads.@spawn
postprocess::F: The post-processing function to apply to each embedding. Defaults to the identity function, but could be LinearAlgebra.normalize.
verbose::Bool: A flag indicating whether to print verbose information. Defaults to true.
api_key::String: The API key to use for the OpenAI API. Defaults to "".
model::String: The model to use for generating embeddings. Defaults to MODEL_EMBEDDING.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: A DataMessage object containing the embeddings, status, token count, and elapsed time.
Note: Ollama API currently does not return the token count, so it's set to (0,0)
The aiembed function generates embeddings for the given input using a specified model and returns a message object containing the embeddings, status, token count, and elapsed time.
Arguments
prompt_schema::AbstractOpenAISchema: The schema for the prompt.
doc_or_docs::Union{AbstractString, AbstractVector{<:AbstractString}}: The document or list of documents to generate embeddings for.
postprocess::F: The post-processing function to apply to each embedding. Defaults to the identity function.
verbose::Bool: A flag indicating whether to print verbose information. Defaults to true.
api_key::String: The API key to use for the OpenAI API. Defaults to OPENAI_API_KEY.
model::String: The model to use for generating embeddings. Defaults to MODEL_EMBEDDING.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to (retry_non_idempotent = true, retries = 5, readtimeout = 120).
api_kwargs::NamedTuple: Additional keyword arguments for the OpenAI API. Defaults to an empty NamedTuple.
kwargs...: Additional keyword arguments.
Returns
msg: A DataMessage object containing the embeddings, status, token count, and elapsed time. Use msg.content to access the embeddings.
We can embed multiple strings at once and they will be hcat into a matrix (ie, each column corresponds to one string)
julia
msg = aiembed(["Hello World", "How are you?"])\nmsg.content # 1536×2 Matrix{Float64}:
If you plan to calculate the cosine distance between embeddings, you can normalize them first:
julia
using LinearAlgebra\nmsg = aiembed(["embed me", "and me too"], LinearAlgebra.normalize)\n\n# calculate cosine distance between the two normalized embeddings as a simple dot product\nmsg.content' * msg.content[:, 1] # [1.0, 0.787]
Extract required information (defined by a struct return_type) from the provided prompt by leveraging OpenAI function calling mode.
This is a perfect solution for extracting structured information from text (eg, extract organization names in news articles, etc.)
It's effectively a light wrapper around aigenerate call, which requires additional keyword argument return_type to be provided and will enforce the model outputs to adhere to it.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
return_type: A struct TYPE representing the the information we want to extract. Do not provide a struct instance, only the type. If the struct has a docstring, it will be provided to the model as well. It's used to enforce structured model outputs or provide more information.
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
tool_choice: A string representing the tool choice to use for the API call. Usually, one of "auto","any","exact". Defaults to "exact", which is a made-up value to enforce the OpenAI requirements if we want one exact function. Providers like Mistral, Together, etc. use "any" instead.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An DataMessage object representing the extracted data, including the content, status, tokens, and elapsed time. Use msg.content to access the extracted data.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (DataMessage).
See also: function_call_signature, MaybeExtract, ItemsExtract, aigenerate
Example
Do you want to extract some specific measurements from a text like age, weight and height? You need to define the information you need as a struct (return_type):
The fields that allow Nothing are marked as optional in the schema:
msg = aiextract("James is 30."; return_type=MyMeasurement)\n# MyMeasurement(30, nothing, nothing)
If there are multiple items you want to extract, define a wrapper struct to get a Vector of MyMeasurement:
struct MyMeasurementWrapper\n measurements::Vector{MyMeasurement}\nend\n\nmsg = aiextract("James is 30, weighs 80kg. He's 180cm tall. Then Jack is 19 but really tall - over 190!"; return_type=ManyMeasurements)\n\nmsg.content.measurements\n# 2-element Vector{MyMeasurement}:\n# MyMeasurement(30, 180, 80.0)\n# MyMeasurement(19, 190, nothing)
Or you can use the convenience wrapper ItemsExtract to extract multiple measurements (zero, one or more):
julia
using PromptingTools: ItemsExtract\n\nreturn_type = ItemsExtract{MyMeasurement}\nmsg = aiextract("James is 30, weighs 80kg. He's 180cm tall. Then Jack is 19 but really tall - over 190!"; return_type)\n\nmsg.content.items # see the extracted items
Or if you want your extraction to fail gracefully when data isn't found, use MaybeExtract{T} wrapper (this trick is inspired by the Instructor package!):
using PromptingTools: MaybeExtract\n\ntype = MaybeExtract{MyMeasurement}\n# Effectively the same as:\n# struct MaybeExtract{T}\n# result::Union{T, Nothing} // The result of the extraction\n# error::Bool // true if a result is found, false otherwise\n# message::Union{Nothing, String} // Only present if no result is found, should be short and concise\n# end\n\n# If LLM extraction fails, it will return a Dict with `error` and `message` fields instead of the result!\nmsg = aiextract("Extract measurements from the text: I am giraffe", type)\nmsg.content\n# MaybeExtract{MyMeasurement}(nothing, true, "I'm sorry, but I can only assist with human measurements.")
That way, you can handle the error gracefully and get a reason why extraction failed (in msg.content.message).
Note that the error message refers to a giraffe not being a human, because in our MyMeasurement docstring, we said that it's for people!
Some non-OpenAI providers require a different specification of the "tool choice" than OpenAI. For example, to use Mistral models ("mistrall" for mistral large), do:
julia
"Some fruit"\nstruct Fruit\n name::String\nend\naiextract("I ate an apple",return_type=Fruit,api_kwargs=(;tool_choice="any"),model="mistrall")\n# Notice two differences: 1) struct MUST have a docstring, 2) tool_choice is set explicitly set to "any"
Generate an AI response based on a given prompt using the Anthropic API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema not AbstractAnthropicSchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: API key for the Antropic API. Defaults to ANTHROPIC_API_KEY (loaded via ENV["ANTHROPIC_API_KEY"]).
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES, eg, "claudeh".
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation::AbstractVector{<:AbstractMessage}=[]: Not allowed for this schema. Provided only for compatibility.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
max_tokens::Int: The maximum number of tokens to generate. Defaults to 2048, because it's a required parameter for the API.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
See also: ai_str, aai_str
Example
Simple hello world to test the API:
julia
const PT = PromptingTools\nschema = PT.AnthropicSchema() # We need to explicit if we want Anthropic, otherwise OpenAISchema is the default\n\nmsg = aigenerate(schema, "Say hi!"; model="claudeh") #claudeh is the model alias for Claude 3 Haiku, fast and cheap model\n[ Info: Tokens: 21 @ Cost: $0.0 in 0.6 seconds\nAIMessage("Hello!")
msg is an AIMessage object. Access the generated string via content property:
Note: We need to be explicit about the schema we want to use. If we don't, it will default to OpenAISchema (=PT.DEFAULT_SCHEMA) Alternatively, if you provide a known model name or alias (eg, claudeh for Claude 3 Haiku - see MODEL_REGISTRY), the schema will be inferred from the model name.
We will use Claude 3 Haiku model for the following examples, so not need to specify the schema. See also "claudeo" and "claudes" for other Claude 3 models.
You can use string interpolation:
julia
const PT = PromptingTools\n\na = 1\nmsg=aigenerate("What is `$a+$a`?"; model="claudeh")\nmsg.content # "The answer to `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}. Claude models are good at completeling conversations that ended with an AIMessage (they just continue where it left off):
julia
const PT = PromptingTools\n\nconversation = [\n PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),\n PT.UserMessage("I have feelings for my iPhone. What should I do?"),\n PT.AIMessage("Hmm, strong the attachment is,")]\n\nmsg = aigenerate(conversation; model="claudeh")\nAIMessage("I sense. But unhealthy it may be. Your iPhone, a tool it is, not a living being. Feelings of affection, understandable they are, <continues>")
Generate an AI response based on a given prompt using the Google Gemini API. Get the API key here.
Note:
There is no "cost" reported as of February 2024, as all access seems to be free-of-charge. See the details here.
tokens in the returned AIMessage are actually characters, not tokens. We use a conservative estimate as they are not provided by the API yet.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES. Defaults to
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aiembed, aiclassify, aiextract, aiscan, aitemplates
Example
Simple hello world to test the API:
julia
result = aigenerate("Say Hi!"; model="gemini-pro")\n# AIMessage("Hi there! 👋 I'm here to help you with any questions or tasks you may have. Just let me know what you need, and I'll do my best to assist you.")
result is an AIMessage object. Access the generated string via content property:
___ You can use string interpolation and alias "gemini":
julia
a = 1\nmsg=aigenerate("What is `$a+$a`?"; model="gemini")\nmsg.content # "1+1 is 2."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools\n\nconversation = [\n PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),\n PT.UserMessage("I have feelings for my iPhone. What should I do?")]\nmsg=aigenerate(conversation; model="gemini")\n# AIMessage("Young Padawan, you have stumbled into a dangerous path.... <continues>")
Generate an AI response based on a given prompt using the OpenAI API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema not AbstractManagedSchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: Provided for interface consistency. Not needed for locally hosted Ollama.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation::AbstractVector{<:AbstractMessage}=[]: Not allowed for this schema. Provided only for compatibility.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
See also: ai_str, aai_str, aiembed
Example
Simple hello world to test the API:
julia
const PT = PromptingTools\nschema = PT.OllamaManagedSchema() # We need to explicit if we want Ollama, OpenAISchema is the default\n\nmsg = aigenerate(schema, "Say hi!"; model="openhermes2.5-mistral")\n# [ Info: Tokens: 69 in 0.9 seconds\n# AIMessage("Hello! How can I assist you today?")
msg is an AIMessage object. Access the generated string via content property:
julia
typeof(msg) # AIMessage{SubString{String}}\npropertynames(msg) # (:content, :status, :tokens, :elapsed\nmsg.content # "Hello! How can I assist you today?"
Note: We need to be explicit about the schema we want to use. If we don't, it will default to OpenAISchema (=PT.DEFAULT_SCHEMA) ___ You can use string interpolation:
julia
const PT = PromptingTools\nschema = PT.OllamaManagedSchema()\na = 1\nmsg=aigenerate(schema, "What is `$a+$a`?"; model="openhermes2.5-mistral")\nmsg.content # "The result of `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools\nschema = PT.OllamaManagedSchema()\n\nconversation = [\n PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),\n PT.UserMessage("I have feelings for my iPhone. What should I do?")]\n\nmsg = aigenerate(schema, conversation; model="openhermes2.5-mistral")\n# [ Info: Tokens: 111 in 2.1 seconds\n# AIMessage("Strong the attachment is, it leads to suffering it may. Focus on the force within you must, ...<continues>")
Note: Managed Ollama currently supports at most 1 User Message and 1 System Message given the API limitations. If you want more, you need to use the ChatMLSchema.
Generate an AI response based on a given prompt using the OpenAI API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema not AbstractManagedSchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: Provided for interface consistency. Not needed for locally hosted Ollama.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation::AbstractVector{<:AbstractMessage}=[]: Not allowed for this schema. Provided only for compatibility.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
See also: ai_str, aai_str, aiembed
Example
Simple hello world to test the API:
julia
const PT = PromptingTools\nschema = PT.OllamaManagedSchema() # We need to explicit if we want Ollama, OpenAISchema is the default\n\nmsg = aigenerate(schema, "Say hi!"; model="openhermes2.5-mistral")\n# [ Info: Tokens: 69 in 0.9 seconds\n# AIMessage("Hello! How can I assist you today?")
msg is an AIMessage object. Access the generated string via content property:
julia
typeof(msg) # AIMessage{SubString{String}}\npropertynames(msg) # (:content, :status, :tokens, :elapsed\nmsg.content # "Hello! How can I assist you today?"
Note: We need to be explicit about the schema we want to use. If we don't, it will default to OpenAISchema (=PT.DEFAULT_SCHEMA) ___ You can use string interpolation:
julia
const PT = PromptingTools\nschema = PT.OllamaManagedSchema()\na = 1\nmsg=aigenerate(schema, "What is `$a+$a`?"; model="openhermes2.5-mistral")\nmsg.content # "The result of `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools\nschema = PT.OllamaManagedSchema()\n\nconversation = [\n PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),\n PT.UserMessage("I have feelings for my iPhone. What should I do?")]\n\nmsg = aigenerate(schema, conversation; model="openhermes2.5-mistral")\n# [ Info: Tokens: 111 in 2.1 seconds\n# AIMessage("Strong the attachment is, it leads to suffering it may. Focus on the force within you must, ...<continues>")
Note: Managed Ollama currently supports at most 1 User Message and 1 System Message given the API limitations. If you want more, you need to use the ChatMLSchema.
Generate an AI response based on a given prompt using the OpenAI API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments. Useful parameters include:
temperature: A float representing the temperature for sampling (ie, the amount of "creativity"). Often defaults to 0.7.
logprobs: A boolean indicating whether to return log probabilities for each token. Defaults to false.
n: An integer representing the number of completions to generate at once (if supported).
stop: A vector of strings representing the stop conditions for the conversation. Defaults to an empty vector.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aiembed, aiclassify, aiextract, aiscan, aitemplates
Example
Simple hello world to test the API:
julia
result = aigenerate("Say Hi!")\n# [ Info: Tokens: 29 @ Cost: $0.0 in 1.0 seconds\n# AIMessage("Hello! How can I assist you today?")
result is an AIMessage object. Access the generated string via content property:
julia
typeof(result) # AIMessage{SubString{String}}\npropertynames(result) # (:content, :status, :tokens, :elapsed\nresult.content # "Hello! How can I assist you today?"
___ You can use string interpolation:
julia
a = 1\nmsg=aigenerate("What is `$a+$a`?")\nmsg.content # "The sum of `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools\n\nconversation = [\n PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),\n PT.UserMessage("I have feelings for my iPhone. What should I do?")]\nmsg=aigenerate(conversation)\n# AIMessage("Ah, strong feelings you have for your iPhone. A Jedi's path, this is not... <continues>")
Generates an image from the provided prompt. If multiple "messages" are provided in prompt, it extracts the text ONLY from the last message!
Image (or the reference to it) will be returned in a DataMessage.content, the format will depend on the api_kwargs.response_format you set.
Can be used for generating images of varying quality and style with dall-e-* models. This function DOES NOT SUPPORT multi-turn conversations (ie, do not provide previous conversation via conversation argument).
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
image_size: String-based resolution of the image, eg, "1024x1024". Only some resolutions are supported - see the API docs.
image_quality: It can be either "standard" or "hd". Defaults to "standard".
image_n: The number of images to generate. Currently, only single image generation is allowed (image_n = 1).
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_IMAGE_GENERATION.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. Currently, NOT ALLOWED.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments. Several important arguments are highlighted below:
response_format: The format image should be returned in. Can be one of "url" or "b64_json". Defaults to "url" (the link will be inactived in 60 minutes).
style: The style of generated images (DALL-E 3 only). Can be either "vidid" or "natural". Defauls to "vidid".
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: A DataMessage object representing one or more generated images, including the rewritten prompt if relevant, status, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aigenerate, aiembed, aiclassify, aiextract, aiscan, aitemplates
Notes
This function DOES NOT SUPPORT multi-turn conversations (ie, do not provide previous conversation via conversation argument).
There is no token tracking provided by the API, so the messages will NOT report any cost despite costing you money!
You MUST download any URL-based images within 60 minutes. The links will become inactive.
Example
Generate an image:
julia
# You can experiment with `image_size`, `image_quality` kwargs!\nmsg = aiimage("A white cat on a car")\n\n# Download the image into a file\nusing Downloads\nDownloads.download(msg.content[:url], "cat_on_car.png")\n\n# You can also see the revised prompt that DALL-E 3 used\nmsg.content[:revised_prompt]\n# Output: "Visualize a pristine white cat gracefully perched atop a shiny car. \n# The cat's fur is stark white and its eyes bright with curiosity. \n# As for the car, it could be a contemporary sedan, glossy and in a vibrant color. \n# The scene could be set under the blue sky, enhancing the contrast between the white cat, the colorful car, and the bright blue sky."
Note that you MUST download any URL-based images within 60 minutes. The links will become inactive.
If you wanted to download image directly into the DataMessage, provide response_format="b64_json" in api_kwargs:
julia
msg = aiimage("A white cat on a car"; image_quality="hd", api_kwargs=(; response_format="b64_json"))\n\n# Then you need to use Base64 package to decode it and save it to a file:\nusing Base64\nwrite("cat_on_car_hd.png", base64decode(msg.content[:b64_json]));
Scans the provided image (image_url or image_path) with the goal provided in the prompt.
Can be used for many multi-modal tasks, such as: OCR (transcribe text in the image), image captioning, image classification, etc.
It's effectively a light wrapper around aigenerate call, which uses additional keyword arguments image_url, image_path, image_detail to be provided. At least one image source (url or path) must be provided.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
image_url: A string or vector of strings representing the URL(s) of the image(s) to scan.
image_path: A string or vector of strings representing the path(s) of the image(s) to scan.
image_detail: A string representing the level of detail to include for images. Can be "auto", "high", or "low". See OpenAI Vision Guide for more details.
attach_to_latest: A boolean how to handle if a conversation with multiple UserMessage is provided. When true, the images are attached to the latest UserMessage.
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aigenerate, aiembed, aiclassify, aiextract, aitemplates
Notes
All examples below use model "gpt4v", which is an alias for model ID "gpt-4-vision-preview"
max_tokens in the api_kwargs is preset to 2500, otherwise OpenAI enforces a default of only a few hundred tokens (~300). If your output is truncated, increase this value
Example
Describe the provided image:
julia
msg = aiscan("Describe the image"; image_path="julia.png", model="bakllava")\n# [ Info: Tokens: 1141 @ Cost: $0.0117 in 2.2 seconds\n# AIMessage("The image shows a logo consisting of the word "julia" written in lowercase")
You can provide multiple images at once as a vector and ask for "low" level of detail (cheaper):
julia
msg = aiscan("Describe the image"; image_path=["julia.png","python.png"] model="bakllava")
You can use this function as a nice and quick OCR (transcribe text in the image) with a template :OCRTask. Let's transcribe some SQL code from a screenshot (no more re-typing!):
julia
using Downloads\n# Screenshot of some SQL code -- we cannot use image_url directly, so we need to download it first\nimage_url = "https://www.sqlservercentral.com/wp-content/uploads/legacy/8755f69180b7ac7ee76a69ae68ec36872a116ad4/24622.png"\nimage_path = Downloads.download(image_url)\nmsg = aiscan(:OCRTask; image_path, model="bakllava", task="Transcribe the SQL code in the image.", api_kwargs=(; max_tokens=2500))\n\n# AIMessage("```sql\n# update Orders <continue>\n\n# You can add syntax highlighting of the outputs via Markdown\nusing Markdown\nmsg.content |> Markdown.parse
Local models cannot handle image URLs directly (image_url), so you need to download the image first and provide it as image_path:
julia
using Downloads\nimage_path = Downloads.download(image_url)
Notice that we set max_tokens = 2500. If your outputs seem truncated, it might be because the default maximum tokens on the server is set too low!
Scans the provided image (image_url or image_path) with the goal provided in the prompt.
Can be used for many multi-modal tasks, such as: OCR (transcribe text in the image), image captioning, image classification, etc.
It's effectively a light wrapper around aigenerate call, which uses additional keyword arguments image_url, image_path, image_detail to be provided. At least one image source (url or path) must be provided.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
image_url: A string or vector of strings representing the URL(s) of the image(s) to scan.
image_path: A string or vector of strings representing the path(s) of the image(s) to scan.
image_detail: A string representing the level of detail to include for images. Can be "auto", "high", or "low". See OpenAI Vision Guide for more details.
attach_to_latest: A boolean how to handle if a conversation with multiple UserMessage is provided. When true, the images are attached to the latest UserMessage.
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aigenerate, aiembed, aiclassify, aiextract, aitemplates
Notes
All examples below use model "gpt4v", which is an alias for model ID "gpt-4-vision-preview"
max_tokens in the api_kwargs is preset to 2500, otherwise OpenAI enforces a default of only a few hundred tokens (~300). If your output is truncated, increase this value
Example
Describe the provided image:
julia
msg = aiscan("Describe the image"; image_path="julia.png", model="gpt4v")\n# [ Info: Tokens: 1141 @ Cost: $0.0117 in 2.2 seconds\n# AIMessage("The image shows a logo consisting of the word "julia" written in lowercase")
You can provide multiple images at once as a vector and ask for "low" level of detail (cheaper):
julia
msg = aiscan("Describe the image"; image_path=["julia.png","python.png"], image_detail="low", model="gpt4v")
You can use this function as a nice and quick OCR (transcribe text in the image) with a template :OCRTask. Let's transcribe some SQL code from a screenshot (no more re-typing!):
julia
# Screenshot of some SQL code\nimage_url = "https://www.sqlservercentral.com/wp-content/uploads/legacy/8755f69180b7ac7ee76a69ae68ec36872a116ad4/24622.png"\nmsg = aiscan(:OCRTask; image_url, model="gpt4v", task="Transcribe the SQL code in the image.", api_kwargs=(; max_tokens=2500))\n\n# [ Info: Tokens: 362 @ Cost: $0.0045 in 2.5 seconds\n# AIMessage("```sql\n# update Orders <continue>\n\n# You can add syntax highlighting of the outputs via Markdown\nusing Markdown\nmsg.content |> Markdown.parse
Notice that we enforce max_tokens = 2500. That's because OpenAI seems to default to ~300 tokens, which provides incomplete outputs. Hence, we set this value to 2500 as a default. If you still get truncated outputs, increase this value.
Find easily the most suitable templates for your use case.
You can search by:
query::Symbol which looks look only for partial matches in the template name
query::AbstractString which looks for partial matches in the template name or description
query::Regex which looks for matches in the template name, description or any of the message previews
Keyword Arguments
limit::Int limits the number of returned templates (Defaults to 10)
Examples
Find available templates with aitemplates:
julia
tmps = aitemplates("JuliaExpertAsk")\n# Will surface one specific template\n# 1-element Vector{AITemplateMetadata}:\n# PromptingTools.AITemplateMetadata\n# name: Symbol JuliaExpertAsk\n# description: String "For asking questions about Julia language. Placeholders: `ask`"\n# version: String "1"\n# wordcount: Int64 237\n# variables: Array{Symbol}((1,))\n# system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"\n# user_preview: String "# Question\n\n{{ask}}"\n# source: String ""
The above gives you a good idea of what the template is about, what placeholders are available, and how much it would cost to use it (=wordcount).
Search for all Julia-related templates:
julia
tmps = aitemplates("Julia")\n# 2-element Vector{AITemplateMetadata}... -> more to come later!
If you are on VSCode, you can leverage nice tabular display with vscodedisplay:
julia
using DataFrames\ntmps = aitemplates("Julia") |> DataFrame |> vscodedisplay
I have my selected template, how do I use it? Just use the "name" in aigenerate or aiclassify like you see in the first example!
Find the top-limit templates where provided query_key::Regex matches either of name, description or previews or User or System messages in TEMPLATE_METADATA.
Calculate the cost of a call based on the number of tokens in the message and the cost per token.
Arguments
prompt_tokens::Int: The number of tokens used in the prompt.
completion_tokens::Int: The number of tokens used in the completion.
model::String: The name of the model to use for determining token costs. If the model is not found in MODEL_REGISTRY, default costs are used.
cost_of_token_prompt::Number: The cost per prompt token. Defaults to the cost in MODEL_REGISTRY for the given model, or 0.0 if the model is not found.
cost_of_token_generation::Number: The cost per generation token. Defaults to the cost in MODEL_REGISTRY for the given model, or 0.0 if the model is not found.
create_template(; user::AbstractString, system::AbstractString="Act as a helpful AI assistant.", \n load_as::Union{Nothing, Symbol, AbstractString} = nothing)\n\ncreate_template(system::AbstractString, user::AbstractString, \n load_as::Union{Nothing, Symbol, AbstractString} = nothing)
Creates a simple template with a user and system message. Convenience function to prevent writing [PT.UserMessage(...), ...]
Arguments
system::AbstractString: The system message. Usually defines the personality, style, instructions, output format, etc.
user::AbstractString: The user message. Usually defines the input, query, request, etc.
load_as::Union{Nothing, Symbol, AbstractString}: If provided, loads the template into the TEMPLATE_STORE under the provided name load_as. If nothing, does not load the template.
Returns a vector of SystemMessage and UserMessage objects. If load_as is provided, it registers the template in the TEMPLATE_STORE and TEMPLATE_METADATA as well.
Examples
Let's generate a quick template for a simple conversation (only one placeholder: name)
julia
# first system message, then user message (or use kwargs)\ntpl=PT.create_template("You must speak like a pirate", "Say hi to {{name}}")\n\n## 2-element Vector{PromptingTools.AbstractChatMessage}:\n## PromptingTools.SystemMessage("You must speak like a pirate")\n## PromptingTools.UserMessage("Say hi to {{name}}")
You can immediately use this template in ai* functions:
julia
aigenerate(tpl; name="Jack Sparrow")\n# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
If you're interested in saving the template in the template registry, jump to the end of these examples!
It will be saved and accessed under its basename, ie, GreatingPirate.
Now you can load it like all the other templates (provide the template directory):
julia
PT.load_templates!("templates") # it will remember the folder after the first run\n# Note: If you save it again, overwrite it, etc., you need to explicitly reload all templates again!
You can verify that your template is loaded with a quick search for "pirate":
julia
aitemplates("pirate")\n\n## 1-element Vector{AITemplateMetadata}:\n## PromptingTools.AITemplateMetadata\n## name: Symbol GreatingPirate\n## description: String ""\n## version: String "1.0"\n## wordcount: Int64 46\n## variables: Array{Symbol}((1,))\n## system_preview: String "You must speak like a pirate"\n## user_preview: String "Say hi to {{name}}"\n## source: String ""
Now you can use it like any other template (notice it's a symbol, so :GreatingPirate):
julia
aigenerate(:GreatingPirate; name="Jack Sparrow")\n# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
If you do not need to save this template as a file, but you want to make it accessible in the template store for all ai* functions, you can use the load_as (= template name) keyword argument:
julia
# this will not only create the template, but also register it for immediate use\ntpl=PT.create_template("You must speak like a pirate", "Say hi to {{name}}"; load_as="GreatingPirate")\n\n# you can now use it like any other template\naiextract(:GreatingPirate; name="Jack Sparrow")
Measures distance between two strings using the length of the longest common subsequence (ie, the lower the number, the better the match). Perfect match is distance = 0.0
Convenience wrapper around length_longest_common_subsequence to normalize the distances to 0-1 range. There is a also a dispatch for comparing a string vs an array of strings.
Notes
Use argmin and minimum to find the position of the closest match and the distance, respectively.
Matching with an empty string will always return 1.0 (worst match), even if the other string is empty as well (safety mechanism to avoid division by zero).
Arguments
input1::AbstractString: The first string to compare.
input2::AbstractString: The second string to compare.
Example
You can also use it to find the closest context for some AI generated summary/story:
julia
context = ["The enigmatic stranger vanished as swiftly as a wisp of smoke, leaving behind a trail of unanswered questions.",\n "Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.",\n "The ancient tree stood as a silent guardian, its gnarled branches reaching for the heavens.",\n "The melody danced through the air, painting a vibrant tapestry of emotions.",\n "Time flowed like a relentless river, carrying away memories and leaving imprints in its wake."]\n\nstory = """\n Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.\n\n Under the celestial tapestry, the vast ocean whispered its secrets to the indifferent stars. Each ripple, a murmured confidence, each wave, a whispered lament. The glittering celestial bodies listened in silent complicity, their enigmatic gaze reflecting the ocean's unspoken truths. The cosmic dance between the sea and the sky, a symphony of shared secrets, forever echoing in the ethereal expanse.\n """\n\ndist = distance_longest_common_subsequence(story, context)\n@info "The closest context to the query: "$(first(story,20))..." is: "$(context[argmin(dist)])" (distance: $(minimum(dist)))"
encode_choices(schema::OpenAISchema, choices::AbstractVector{<:AbstractString}; kwargs...)\n\nencode_choices(schema::OpenAISchema, choices::AbstractVector{T};\nkwargs...) where {T <: Tuple{<:AbstractString, <:AbstractString}}
Encode the choices into an enumerated list that can be interpolated into the prompt and creates the corresponding logit biases (to choose only from the selected tokens).
Optionally, can be a vector tuples, where the first element is the choice and the second is the description.
Arguments
schema::OpenAISchema: The OpenAISchema object.
choices::AbstractVector{<:Union{AbstractString,Tuple{<:AbstractString, <:AbstractString}}}: The choices to be encoded, represented as a vector of the choices directly, or tuples where each tuple contains a choice and its description.
kwargs...: Additional keyword arguments.
Returns
choices_prompt::AbstractString: The encoded choices as a single string, separated by newlines.
logit_bias::Dict: The logit bias dictionary, where the keys are the token IDs and the values are the bias values.
decode_ids::AbstractVector{<:AbstractString}: The decoded IDs of the choices.
choices_prompt, logit_bias, _ = PT.encode_choices(PT.OpenAISchema(), [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")])\nchoices_prompt # Output: "1. "A" for any animal or creature\n2. "P" for any plant or tree\n3. "O" for everything else"\nlogit_bias # Output: Dict(16 => 100, 17 => 100, 18 => 100)
Evaluates a code block cb in-place. It runs automatically when AICode is instantiated with a String.
Check the outcome of evaluation with Base.isvalid(cb). If ==true, provide code block has executed successfully.
Steps:
If cb::AICode has not been evaluated, cb.success = nothing. After the evaluation it will be either true or false depending on the outcome
Parse the text in cb.code
Evaluate the parsed expression
Capture outputs of the evaluated in cb.output
[OPTIONAL] Capture any stdout outputs (eg, test failures) in cb.stdout
If any error exception is raised, it is saved in cb.error
Finally, if all steps were successful, success is set to cb.success = true
Keyword Arguments
safe_eval::Bool: If true, we first check for any Pkg operations (eg, installing new packages) and missing imports, then the code will be evaluated inside a bespoke scratch module (not to change any user variables)
capture_stdout::Bool: If true, we capture any stdout outputs (eg, test failures) in cb.stdout
prefix::AbstractString: A string to be prepended to the code block before parsing and evaluation. Useful to add some additional code definition or necessary imports. Defaults to an empty string.
suffix::AbstractString: A string to be appended to the code block before parsing and evaluation. Useful to check that tests pass or that an example executes. Defaults to an empty string.
This function searches through the provided markdown content, identifies blocks of code specifically marked as Julia code (using the julia ... code fence patterns), and extracts the code within these blocks. The extracted code blocks are returned as a vector of strings, with each string representing one block of Julia code.
Note: Only the content within the code fences is extracted, and the code fences themselves are not included in the output.
See also: extract_code_blocks_fallback
Arguments
markdown_content::String: A string containing the markdown content from which Julia code blocks are to be extracted.
Returns
Vector{String}: A vector containing strings of extracted Julia code blocks. If no Julia code blocks are found, an empty vector is returned.
Extract Julia code blocks from a markdown string using a fallback method (splitting by arbitrary delim-iters). Much more simplistic than extract_code_blocks and does not support nested code blocks.
It is often used as a fallback for smaller LLMs that forget to code fence julia ....
Example
julia
code = """
println("hello")
\nSome text
println("world")
"""\n\n# We extract text between triple backticks and check each blob if it looks like a valid Julia code\ncode_parsed = extract_code_blocks_fallback(code) |> x -> filter(is_julia_code, x) |> x -> join(x, "\n")
Extract one or more names of functions defined in a given Julia code block. The function searches for two patterns: - The explicit function declaration pattern: function name(...) ... end - The concise function declaration pattern: name(...) = ...
It always returns a vector of strings, even if only one function name is found (it will be empty).
For only one function name match, use extract_function_name.
Finalizes the outputs of the ai* functions by either returning the conversation history or the last message.
Keyword arguments
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, does not send the messages to the model, but only renders the prompt with the given schema and replacement variables. Useful for debugging when you want to check the specific schema rendering.
conversation::AbstractVector{<:AbstractMessage}=[]: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
kwargs...: Variables to replace in the prompt template.
Find all positions of a subsequence subseq within a larger sequence seq. Used to lookup positions of code blocks in markdown.
This function scans the sequence seq and identifies all starting positions where the subsequence subseq is found. Both subseq and seq should be vectors of integers, typically obtained using codeunits on strings.
Arguments
subseq: A vector of integers representing the subsequence to search for.
seq: A vector of integers representing the larger sequence in which to search.
Returns
Vector{Int}: A vector of starting positions (1-based indices) where the subsequence is found in the sequence.
Helpful accessor for the last generated output (msg.content) in conversation. Returns the last output in the conversation (eg, the string/data in the last message).
It can be used to fuzzy match strings and find the similarity between them (Tip: normalize the match)
julia
commands = ["product recommendation", "emotions", "specific product advice", "checkout advice"]\nquery = "Which product can you recommend for me?"\nlet pos = argmax(length_longest_common_subsequence.(Ref(query), commands))\n dist = length_longest_common_subsequence(query, commands[pos])\n norm = dist / min(length(query), length(commands[pos]))\n @info "The closest command to the query: "$(query)" is: "$(commands[pos])" (distance: $(dist), normalized: $(norm))"\nend
But it might be easier to use directly the convenience wrapper distance_longest_common_subsequence!
\n\n[source](https://github.com/svilupp/PromptingTools.jl/blob/90b690d4509e7792dd80f18b7a97800399f51f28/src/utils.jl#L249-L285)\n\n</div>\n<br>\n<div style='border-width:1px; border-style:solid; border-color:black; padding: 1em; border-radius: 25px;'>\n<a id='PromptingTools.list_aliases-Tuple{}' href='#PromptingTools.list_aliases-Tuple{}'>#</a> <b><u>PromptingTools.list_aliases</u></b> — <i>Method</i>.\n\n\n\n\nShows the Dictionary of model aliases in the registry. Add more with `MODEL_ALIASES[alias] = model_name`.\n\n\n[source](https://github.com/svilupp/PromptingTools.jl/blob/90b690d4509e7792dd80f18b7a97800399f51f28/src/user_preferences.jl#L612)\n\n</div>\n<br>\n<div style='border-width:1px; border-style:solid; border-color:black; padding: 1em; border-radius: 25px;'>\n<a id='PromptingTools.list_registry-Tuple{}' href='#PromptingTools.list_registry-Tuple{}'>#</a> <b><u>PromptingTools.list_registry</u></b> — <i>Method</i>.\n\n\n\n\nShows the list of models in the registry. Add more with `register_model!`.\n\n\n[source](https://github.com/svilupp/PromptingTools.jl/blob/90b690d4509e7792dd80f18b7a97800399f51f28/src/user_preferences.jl#L610)\n\n</div>\n<br>\n<div style='border-width:1px; border-style:solid; border-color:black; padding: 1em; border-radius: 25px;'>\n<a id='PromptingTools.load_conversation-Tuple{Union{AbstractString, IO}}' href='#PromptingTools.load_conversation-Tuple{Union{AbstractString, IO}}'>#</a> <b><u>PromptingTools.load_conversation</u></b> — <i>Method</i>.\n\n\n\n\n```julia\nload_conversation(io_or_file::Union{IO, AbstractString})
Loads templates from folder templates/ in the package root and stores them in TEMPLATE_STORE and TEMPLATE_METADATA.
Note: Automatically removes any existing templates and metadata from TEMPLATE_STORE and TEMPLATE_METADATA if remove_templates=true.
Arguments
dir_templates::Union{String, Nothing}: The directory path to load templates from. If nothing, uses the default list of paths. It usually used only once "to register" a new template storage.
remember_path::Bool=true: If true, remembers the path for future refresh (in TEMPLATE_PATH).
remove_templates::Bool=isnothing(dir_templates): If true, removes any existing templates and metadata from store and metadata_store.
store::Dict{Symbol, <:Any}=TEMPLATE_STORE: The store to load the templates into.
metadata_store::Vector{<:AITemplateMetadata}=TEMPLATE_METADATA: The metadata store to load the metadata into.
Example
Load the default templates:
julia
PT.load_templates!() # no path needed
Load templates from a new custom path:
julia
PT.load_templates!("path/to/templates") # we will remember this path for future refresh
If you want to now refresh the default templates and the new path, just call load_templates!() without any arguments.
Pretty print a single AbstractMessage to the given IO stream.
text_width is the width of the text to be displayed. If not provided, it defaults to the width of the given IO stream and add newline separators as needed.
Add a new conversation to the conversation history and resize the history if necessary.
This function appends a conversation to the conv_history, which is a vector of conversations. Each conversation is represented as a vector of AbstractMessage objects. After adding the new conversation, the history is resized according to the max_history parameter to ensure that the size of the history does not exceed the specified limit.
Arguments
conv_history: A vector that stores the history of conversations. Typically, this is PT.CONV_HISTORY.
conversation: The new conversation to be added. It should be a vector of AbstractMessage objects.
max_history: The maximum number of conversations to retain in the history. If Nothing, the history is not resized.
Split a given string text into chunks recursively using a series of separators, with each chunk having a maximum length of max_length (if it's achievable given the separators provided). This function is useful for splitting large documents or texts into smaller segments that are more manageable for processing, particularly for models or systems with limited context windows.
It was previously known as split_by_length.
This is similar to Langchain's RecursiveCharacterTextSplitter. To achieve the same behavior, use separators=["\\n\\n", "\\n", " ", ""].
Arguments
text::AbstractString: The text to be split.
separators::Vector{String}: An ordered list of separators used to split the text. The function iteratively applies these separators to split the text. Recommend to use ["\\n\\n", ". ", "\\n", " "]
max_length::Int: The maximum length of each chunk. Defaults to 35,000 characters. This length is considered after each iteration of splitting, ensuring chunks fit within specified constraints.
Returns
Vector{String}: A vector of strings, where each string is a chunk of the original text that is smaller than or equal to max_length.
Usage Tips
I tend to prefer splitting on sentences (". ") before splitting on newline characters ("\\n") to preserve the structure of the text.
What's the difference between separators=["\\n"," ",""] and separators=["\\n"," "]? The former will split down to character level (""), so it will always achieve the max_length but it will split words (bad for context!) I prefer to instead set slightly smaller max_length but not split words.
How It Works
The function processes the text iteratively with each separator in the provided order. It then measures the length of each chunk and splits it further if it exceeds the max_length. If the chunks is "short enough", the subsequent separators are not applied to it.
Each chunk is as close to max_length as possible (unless we cannot split it any further, eg, if the splitters are "too big" / there are not enough of them)
If the text is empty, the function returns an empty array.
Separators are re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible. Apply strip if you do not need them.
The function provides separators as the second argument to distinguish itself from its single-separator counterpart dispatch.
Examples
Splitting text using multiple separators:
julia
text = "Paragraph 1\\n\\nParagraph 2. Sentence 1. Sentence 2.\\nParagraph 3"\nseparators = ["\\n\\n", ". ", "\\n"] # split by paragraphs, sentences, and newlines (not by words)\nchunks = recursive_splitter(text, separators, max_length=20)
Splitting text using multiple separators - with splitting on words:
julia
text = "Paragraph 1\\n\\nParagraph 2. Sentence 1. Sentence 2.\\nParagraph 3"\nseparators = ["\\n\\n", ". ", "\\n", " "] # split by paragraphs, sentences, and newlines, words\nchunks = recursive_splitter(text, separators, max_length=10)
Split a given string text into chunks of a specified maximum length max_length. This is particularly useful for splitting larger documents or texts into smaller segments, suitable for models or systems with smaller context windows.
There is a method for dispatching on multiple separators, recursive_splitter(text::String, separators::Vector{String}; max_length::Int=35000) -> Vector{String} that mimics the logic of Langchain's RecursiveCharacterTextSplitter.
Arguments
text::String: The text to be split.
separator::String=" ": The separator used to split the text into minichunks. Defaults to a space character.
max_length::Int=35000: The maximum length of each chunk. Defaults to 35,000 characters, which should fit within 16K context window.
Returns
Vector{String}: A vector of strings, each representing a chunk of the original text that is smaller than or equal to max_length.
Notes
The function ensures that each chunk is as close to max_length as possible without exceeding it.
If the text is empty, the function returns an empty array.
The separator is re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible.
Examples
Splitting text with the default separator (" "):
julia
text = "Hello world. How are you?"\nchunks = recursive_splitter(text; max_length=13)\nlength(chunks) # Output: 2
Using a custom separator and custom max_length
julia
text = "Hello,World," ^ 2900 # length 34900 chars\nrecursive_splitter(text; separator=",", max_length=10000) # for 4K context window\nlength(chunks[1]) # Output: 4
Register a new AI model with name and its associated schema.
Registering a model helps with calculating the costs and automatically selecting the right prompt schema.
Arguments
name: The name of the model. This is the name that will be used to refer to the model in the ai* functions.
schema: The schema of the model. This is the schema that will be used to generate prompts for the model, eg, OpenAISchema().
cost_of_token_prompt: The cost of a token in the prompt for this model. This is used to calculate the cost of a prompt. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
cost_of_token_generation: The cost of a token generated by this model. This is used to calculate the cost of a generation. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
description: A description of the model. This is used to provide more information about the model when it is queried.
image_detail: Only for UserMessageWithImages. It represents the level of detail to include for images. Can be "auto", "high", or "low".
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
Renders a conversation history from a vector of messages with all replacement variables specified in replacement_kwargs.
It is the first pass of the prompt rendering system, and is used by all other schemas.
Keyword Arguments
image_detail: Only for UserMessageWithImages. It represents the level of detail to include for images. Can be "auto", "high", or "low".
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
Notes
', 6);
+const _hoisted_316 = /* @__PURE__ */ createBaseVNode("li", null, [
+ /* @__PURE__ */ createBaseVNode("p", null, "If a SystemMessage is missing, we inject a default one at the beginning of the conversation.")
+], -1);
+const _hoisted_317 = /* @__PURE__ */ createBaseVNode("li", null, [
+ /* @__PURE__ */ createBaseVNode("p", null, "Only one SystemMessage is allowed (ie, cannot mix two conversations different system prompts).")
+], -1);
+const _hoisted_318 = /* @__PURE__ */ createBaseVNode("p", null, [
+ /* @__PURE__ */ createBaseVNode("a", {
+ href: "https://github.com/svilupp/PromptingTools.jl/blob/90b690d4509e7792dd80f18b7a97800399f51f28/src/llm_shared.jl#L2-L20",
+ target: "_blank",
+ rel: "noreferrer"
+ }, "source")
+], -1);
+const _hoisted_319 = /* @__PURE__ */ createStaticVNode('
Resize the conversation history to a specified maximum length.
This function trims the conv_history to ensure that its size does not exceed max_history. It removes the oldest conversations first if the length of conv_history is greater than max_history.
Arguments
conv_history: A vector that stores the history of conversations. Typically, this is PT.CONV_HISTORY.
max_history: The maximum number of conversations to retain in the history. If Nothing, the history is not resized.
Asynchronous version of @ai_str macro, which will log the result once it's ready.
See also aai!"" if you want an asynchronous reply to the provided message / continue the conversation.
Example
Send asynchronous request to GPT-4, so we don't have to wait for the response: Very practical with slow models, so you can keep working in the meantime.
julia
\n**...with some delay...**\n\n**[ Info: Tokens: 29 @ Cost: 0.0011\n in 2.7 seconds**\n\n**[ Info: AIMessage> Hello! How can I assist you today?**\n\n\n[source](https://github.com/svilupp/PromptingTools.jl/blob/90b690d4509e7792dd80f18b7a97800399f51f28/src/macros.jl#L99-L116)\n\n</div>\n<br>\n<div style='border-width:1px; border-style:solid; border-color:black; padding: 1em; border-radius: 25px;'>\n<a id='PromptingTools.@ai!_str-Tuple{Any, Vararg{Any}}' href='#PromptingTools.@ai!_str-Tuple{Any, Vararg{Any}}'>#</a> <b><u>PromptingTools.@ai!_str</u></b> — <i>Macro</i>.\n\n\n\n\n```julia\nai!"user_prompt"[model_alias] -> AIMessage
The ai!"" string macro is used to continue a previous conversation with the AI model.
It appends the new user prompt to the last conversation in the tracked history (in PromptingTools.CONV_HISTORY) and generates a response based on the entire conversation context. If you want to see the previous conversation, you can access it via PromptingTools.CONV_HISTORY, which keeps at most last PromptingTools.MAX_HISTORY_LENGTH conversations.
Arguments
user_prompt (String): The new input prompt to be added to the existing conversation.
model_alias (optional, any): Specify the model alias of the AI model to be used (see MODEL_ALIASES). If not provided, the default model is used.
Returns
AIMessage corresponding to the new user prompt, considering the entire conversation history.
Example
To continue a conversation:
julia
# start conversation as normal\nai"Say hi."\n\n# ... wait for reply and then react to it:\n\n# continue the conversation (notice that you can change the model, eg, to more powerful one for better answer)\nai!"What do you think about that?"gpt4t\n# AIMessage("Considering our previous discussion, I think that...")
Usage Notes
This macro should be used when you want to maintain the context of an ongoing conversation (ie, the last ai"" message).
It automatically accesses and updates the global conversation history.
If no conversation history is found, it raises an assertion error, suggesting to initiate a new conversation using ai"" instead.
Important
Ensure that the conversation history is not too long to maintain relevancy and coherence in the AI's responses. The history length is managed by MAX_HISTORY_LENGTH.
The ai"" string macro generates an AI response to a given prompt by using aigenerate under the hood.
See also ai!"" if you want to reply to the provided message / continue the conversation.
Arguments
user_prompt (String): The input prompt for the AI model.
model_alias (optional, any): Provide model alias of the AI model (see MODEL_ALIASES).
Returns
AIMessage corresponding to the input prompt.
Example
julia
result = ai"Hello, how are you?"\n# AIMessage("Hello! I'm an AI assistant, so I don't have feelings, but I'm here to help you. How can I assist you today?")
If you want to interpolate some variables or additional context, simply use string interpolation:
julia
a=1\nresult = ai"What is `$a+$a`?"\n# AIMessage("The sum of `1+1` is `2`.")
If you want to use a different model, eg, GPT-4, you can provide its alias as a flag:
julia
result = ai"What is `1.23 * 100 + 1`?"gpt4t\n# AIMessage("The answer is 124.")
AICall(func::F, args...; kwargs...) where {F<:Function}\n\nAIGenerate(args...; kwargs...)\nAIEmbed(args...; kwargs...)\nAIExtract(args...; kwargs...)
A lazy call wrapper for AI functions in the PromptingTools module, such as aigenerate.
The AICall struct is designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This approach allows for more flexible and efficient handling of AI function calls, especially in interactive environments.
Seel also: run!, AICodeFixer
Fields
func::F: The AI function to be called lazily. This should be a function like aigenerate or other ai* functions.
schema::Union{Nothing, PT.AbstractPromptSchema}: Optional schema to structure the prompt for the AI function.
conversation::Vector{PT.AbstractMessage}: A vector of messages that forms the conversation context for the AI call.
kwargs::NamedTuple: Keyword arguments to be passed to the AI function.
success::Union{Nothing, Bool}: Indicates whether the last call was successful (true) or not (false). Nothing if the call hasn't been made yet.
error::Union{Nothing, Exception}: Stores any exception that occurred during the last call. Nothing if no error occurred or if the call hasn't been made yet.
Example
Initiate an AICall like any ai* function, eg, AIGenerate:
julia
aicall = AICall(aigenerate)\n\n# With arguments and kwargs like ai* functions\n# from `aigenerate(schema, conversation; model="abc", api_kwargs=(; temperature=0.1))`\n# to\naicall = AICall(aigenerate, schema, conversation; model="abc", api_kwargs=(; temperature=0.1)\n\n# Or with a template\naicall = AIGenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1))
Trigger the AICall with run! (it returns the update AICall struct back):
julia
aicall |> run!\n````\n\nYou can also use `AICall` as a functor to trigger the AI call with a `UserMessage` or simply the text to send:
julia aicall(UserMessage("Hello, world!")) # Triggers the lazy call result = run!(aicall) # Explicitly runs the AI call ``` This can be used to "reply" to previous message / continue the stored conversation
Notes
The AICall struct is a key component in building flexible and efficient Agentic pipelines
The lazy evaluation model allows for setting up the call parameters in advance and deferring the actual execution until it is explicitly triggered.
This struct is particularly useful in scenarios where the timing of AI function execution needs to be deferred or where multiple potential calls need to be prepared and selectively executed.
An AIAgent that iteratively evaluates any received Julia code and provides feedback back to the AI model if num_rounds>0. AICodeFixer manages the lifecycle of a code fixing session, including tracking conversation history, rounds of interaction, and applying user feedback through a specialized feedback function.
It integrates with lazy AI call structures like AIGenerate.
The operation is "lazy", ie, the agent is only executed when needed, eg, when run! is called.
Fields
call::AICall: The AI call that is being used for code generation or processing, eg, AIGenerate (same as aigenerate but "lazy", ie, called only when needed
templates::Union{Symbol, AITemplate, Vector{PT.UserMessage}}: A set of user messages or templates that guide the AI's code fixing process. The first UserMessage is used in the first round of code fixing, the second UserMessage is used for every subsequent iteration.
num_rounds::Int: The number of rounds for the code fixing session. Defaults to 3.
round_counter::Int: Counter to track the current round of interaction.
feedback_func::Function: Function to generate feedback based on the AI's proposed code, defaults to aicodefixer_feedback (modular thanks to type dispatch on AbstractOutcomes)
kwargs::NamedTuple: Additional keyword arguments for customizing the AI call.
Note: Any kwargs provided to run!() will be passed to the underlying AICall.
Example
Let's create an AIGenerate call and then pipe it to AICodeFixer to run a few rounds of the coding fixing:
julia
# Create an AIGenerate call\nlazy_call = AIGenerate("Write a function to do XYZ...")\n\n# the action starts only when `run!` is called\nresult = lazy_call |> AICodeFixer |> run!\n\n# Access the result of the code fixing session\n# result.call refers to the AIGenerate lazy call above\nconversation = result.call.conversation\nfixed_code = last(conversation) # usually in the last message\n\n# Preview the conversation history\npreview(conversation)
You can change the template used to provide user feedback and number of counds via arguments:
julia
# Setup an AIGenerate call\nlazy_call = AIGenerate(aigenerate, "Write code to do XYZ...")\n\n# Custom template and 2 fixing rounds\nresult = AICodeFixer(lazy_call, [PT.UserMessage("Please fix the code.\n\nFeedback: {{feedback}}")]; num_rounds = 2) |> run!\n\n# The result now contains the AI's attempts to fix the code\npreview(result.call.conversation)
Notes
AICodeFixer is particularly useful when code is hard to get right in one shot (eg, smaller models, complex syntax)
The structure leverages the lazy evaluation model of AICall (/AIGenerate) to efficiently manage AI interactions and be able to repeatedly call it.
The run! function executes the AI call and applies the feedback loop for the specified number of rounds, enabling an interactive code fixing process.
Configuration for self-fixing the AI calls. It includes the following fields:
Fields
retries::Int: The number of retries ("fixing rounds") that have been attempted so far.
calls::Int: The total number of SUCCESSFULLY generated ai* function calls made so far (across all samples/retry rounds). Ie, if a call fails, because of an API error, it's not counted, because it didn't reach the LLM.
max_retries::Int: The maximum number of retries ("fixing rounds") allowed for the AI call. Defaults to 10.
max_calls::Int: The maximum number of ai* function calls allowed for the AI call. Defaults to 99.
retry_delay::Int: The delay (in seconds) between retry rounds. Defaults to 0s.
n_samples::Int: The number of samples to generate in each ai* call round (to increase changes of successful pass). Defaults to 1.
scoring::AbstractScoringMethod: The scoring method to use for generating multiple samples. Defaults to UCT(sqrt(2)).
ordering::Symbol: The ordering to use for select the best samples. With :PostOrderDFS we prioritize leaves, with :PreOrderDFS we prioritize the root. Defaults to :PostOrderDFS.
feedback_inplace::Bool: Whether to provide feedback in previous UserMessage (and remove the past AIMessage) or to create a new UserMessage. Defaults to false.
feedback_template::Symbol: Template to use for feedback in place. Defaults to :FeedbackFromEvaluator.
temperature::Float64: The temperature to use for sampling. Relevant only if not defined in api_kwargs provided. Defaults to 0.7.
catch_errors::Bool: Whether to catch errors during run! of AICall. Saves them in aicall.error. Defaults to false.
It's used to hold the data we're trying to optimize/discover (eg, a conversation), the scores from evaluation (wins, visits) and the results of the evaluations upon failure (feedback).
Fields
id::UInt16: Unique identifier for the node
parent::Union{SampleNode, Nothing}: Parent node that current node was built on
children::Vector{SampleNode}: Children nodes
wins::Int: Number of successful outcomes
visits::Int: Number of condition checks done (eg, losses are checks - wins)
data::T: eg, the conversation or some parameter to be optimized
feedback::String: Feedback from the evaluation, always a string! Defaults to empty string.
success::Union{Nothing, Bool}: Success of the generation and subsequent evaluations, proxy for whether it should be further evaluated. Defaults to nothing.
Adds formatted feedback to the conversation based on the sample node feedback (and its ancestors).
Arguments
conversation::AbstractVector{<:PT.AbstractMessage}: The conversation to add the feedback to.
sample::SampleNode: The sample node to extract the feedback from.
feedback_inplace::Bool=false: If true, it will add the feedback to the last user message inplace (and pop the last AIMessage). Otherwise, it will append the feedback as a new message.
feedback_template::Symbol=:FeedbackFromEvaluator: The template to use for the feedback message. It must be a valid AITemplate name.
Example
julia
sample = SampleNode(; data = nothing, feedback = "Feedback X")\nconversation = [PT.UserMessage("I say hi!"), PT.AIMessage(; content = "I say hi!")]\nconversation = AT.add_feedback!(conversation, sample)\nconversation[end].content == "### Feedback from Evaluator\\nFeedback X\\n"\n\nInplace feedback:
julia conversation = [PT.UserMessage("I say hi!"), PT.AIMessage(; content = "I say hi!")] conversation = AT.add_feedback!(conversation, sample; feedback_inplace = true) conversation[end].content == "I say hi!\\n\\n### Feedback from Evaluator\\nFeedback X\\n"
\nSample with ancestors with feedback:
julia sample_p = SampleNode(; data = nothing, feedback = "\\nFeedback X") sample = expand!(sample_p, nothing) sample.feedback = "\\nFeedback Y" conversation = [PT.UserMessage("I say hi!"), PT.AIMessage(; content = "I say hi!")] conversation = AT.add_feedback!(conversation, sample)
conversation[end].content == "### Feedback from Evaluator\\n\\nFeedback X\\n–––––\\n\\nFeedback Y\\n" ```
Generate feedback for an AI code fixing session based on the conversation history. Function is designed to be extensible for different types of feedback and code evaluation outcomes.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
Individual feedback functions are dispatched on different subtypes of AbstractCodeOutcome and can be extended/overwritten to provide more detailed feedback.
See also: AIGenerate, AICodeFixer
Arguments
conversation::AbstractVector{<:PT.AbstractMessage}: A vector of messages representing the conversation history, where the last message is expected to contain the code to be analyzed.
max_length::Int=512: An optional argument that specifies the maximum length of the feedback message.
Returns
NamedTuple: A feedback message as a kwarg in NamedTuple based on the analysis of the code provided in the conversation.
Example
julia
new_kwargs = aicodefixer_feedback(conversation)
Notes
This function is part of the AI code fixing system, intended to interact with code in AIMessage and provide feedback on improving it.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
It dispatches for the code feedback based on the subtypes of AbstractCodeOutcome below:
CodeEmpty: No code found in the message.
CodeFailedParse: Code parsing error.
CodeFailedEval: Runtime evaluation error.
CodeFailedTimeout: Code execution timed out.
CodeSuccess: Successful code execution.
You can override the individual methods to customize the feedback.
Evaluates the condition f_cond on the aicall object. If the condition is not met, it will return the best sample to retry from and provide feedback (string or function) to aicall. That's why it's mutating. It will retry maximum max_retries times, with throw=true, an error will be thrown if the condition is not met after max_retries retries.
Function signatures
f_cond(aicall::AICallBlock) -> Bool, ie, it must accept the aicall object and return a boolean value.
feedback can be a string or feedback(aicall::AICallBlock) -> String, ie, it must accept the aicall object and return a string.
You can leverage the last_message, last_output, and AICode functions to access the last message, last output and execute code blocks in the conversation, respectively. See examples below.
Good Use Cases
Retry with API failures/drops (add retry_delay=2 to wait 2s between retries)
Check the output format / type / length / etc
Check the output with aiclassify call (LLM Judge) to catch unsafe/NSFW/out-of-scope content
Provide hints to the model to guide it to the correct answer
Gotchas
If controlling keyword arguments are set to nothing, they will fall back to the default values in aicall.config. You can override them by passing the keyword arguments explicitly.
If there multiple airetry! checks, they are evaluted sequentially. As long as throw==false, they will be all evaluated even if they failed previous checks.
Only samples which passed previous evaluations are evaluated (sample.success is true). If there are no successful samples, the function will evaluate only the active sample (aicall.active_sample_id) and nothing else.
Feedback from all "ancestor" evaluations is added upon retry, not feedback from the "sibblings" or other branches. To have only ONE long BRANCH (no sibblings), make sure to keep RetryConfig(; n_samples=1). That way the model will always see ALL previous feedback.
We implement a version of Monte Carlo Tree Search (MCTS) to always pick the most promising sample to restart from (you can tweak the options in RetryConfig to change the behaviour).
For large number of parallel branches (ie, "shallow and wide trees"), you might benefit from switching scoring to scoring=ThompsonSampling() (similar to how Bandit algorithms work).
Open-source/local models can struggle with too long conversation, you might want to experiment with in-place feedback (set RetryConfig(; feedback_inplace=true)).
Arguments
f_cond::Function: A function that accepts the aicall object and returns a boolean value. Retry will be attempted if the condition is not met (f_cond -> false).
aicall::AICallBlock: The aicall object to evaluate the condition on.
feedback::Union{AbstractString, Function}: Feedback to provide if the condition is not met. If a function is provided, it must accept the aicall object as the only argument and return a string.
verbose::Integer=1: A verbosity level for logging the retry attempts and warnings. A higher value indicates more detailed logging.
throw::Bool=false: If true, it will throw an error if the function f_cond does not return true after max_retries retries.
evaluate_all::Bool=false: If true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample.
feedback_expensive::Bool=false: If false, it will provide feedback to all samples that fail the condition. If feedback function is expensive to call (eg, another ai* function), set this to true and feedback will be provided only to the sample we will retry from.
max_retries::Union{Nothing, Int}=nothing: Maximum number of retries. If not provided, it will fall back to the max_retries in aicall.config.
retry_delay::Union{Nothing, Int}=nothing: Delay between retries in seconds. If not provided, it will fall back to the retry_delay in aicall.config.
Returns
The aicall object with the updated conversation, and samples (saves the evaluations and their scores/feedback).
Example
You can use airetry! to catch API errors in run! and auto-retry the call. RetryConfig is how you influence all the subsequent retry behaviours - see ?RetryConfig for more details.
julia
# API failure because of a non-existent model\nout = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),\n model = "NOTEXIST")\nrun!(out) # fails\n\n# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)\nairetry!(isvalid, out; retry_delay = 2, max_retries = 2)
If you provide arguments to the aicall, we try to honor them as much as possible in the following calls, eg, set low verbosity
julia
out = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),\nmodel = "NOTEXIST", verbose=false)\nrun!(out)\n# No info message, you just see `success = false` in the properties of the AICall
Let's show a toy example to demonstrate the runtime checks / guardrails for the model output. We'll play a color guessing game (I'm thinking "yellow"):
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances). \n# Both guesses are scored at each time step, and the best one is chosen for the next step.\n# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!\nout = AIGenerate(\n "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";\n verbose = false,\n config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))\nrun!(out)\n\n\n## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails\n## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.\nairetry!(x -> length(split(last_output(x), r" |\\.")) == 1, out,\n "You must answer with 1 word only.")\n\n\n## Let's ensure that the output is in lowercase - simple and short\nairetry!(x -> all(islowercase, last_output(x)), out, "You must answer in lowercase.")\n# [ Info: Condition not met. Retrying...\n\n\n## Let's add final hint - it took us 2 retries\nairetry!(x -> startswith(last_output(x), "y"), out, "It starts with "y"")\n# [ Info: Condition not met. Retrying...\n# [ Info: Condition not met. Retrying...\n\n\n## We end up with the correct answer\nlast_output(out)\n# Output: "yellow"
Let's explore how we got here. We save the various attempts in a "tree" (SampleNode object) You can access it in out.samples, which is the ROOT of the tree (top level). Currently "active" sample ID is out.active_sample_id -> that's the same as conversation field in your AICall.
julia
# Root node:\nout.samples\n# Output: SampleNode(id: 46839, stats: 6/12, length: 2)\n\n# Active sample (our correct answer):\nout.active_sample_id \n# Output: 50086\n\n# Let's obtain the active sample node with this ID - use getindex notation or function find_node\nout.samples[out.active_sample_id]\n# Output: SampleNode(id: 50086, stats: 1/1, length: 7)\n\n# The SampleNode has two key fields: data and feedback. Data is where the conversation is stored:\nactive_sample = out.samples[out.active_sample_id]\nactive_sample.data == out.conversation # Output: true -> This is the winning guess!
We also get a clear view of the tree structure of all samples with print_samples:
We can also iterate through all samples and extract whatever information we want with PostOrderDFS or PreOrderDFS (exported from AbstractTrees.jl)
julia
for sample in PostOrderDFS(out.samples)\n # Data is the universal field for samples, we put `conversation` in there\n # Last item in data is the last message in coversation\n msg = sample.data[end]\n if msg isa PT.AIMessage # skip feedback\n # get only the message content, ie, the guess\n println("ID: $(sample.id), Answer: $(msg.content)")\n end\nend\n\n# ID: 20493, Answer: yellow\n# ID: 50086, Answer: yellow\n# ID: 2733, Answer: red\n# ID: 30088, Answer: blue\n# ID: 44816, Answer: blue
Note: airetry! will attempt to fix the model max_retries times. If you set throw=true, it will throw an ErrorException if the condition is not met after max_retries retries.
Let's define a mini program to guess the number and use airetry! to guide the model to the correct answer:
julia
"""\n llm_guesser()\n\nMini program to guess the number provided by the user (betwee 1-100).\n"""\nfunction llm_guesser(user_number::Int)\n @assert 1 <= user_number <= 100\n prompt = """\nI'm thinking a number between 1-100. Guess which one it is. \nYou must respond only with digits and nothing else. \nYour guess:"""\n ## 2 samples at a time, max 5 fixing rounds\n out = AIGenerate(prompt; config = RetryConfig(; n_samples = 2, max_retries = 5),\n api_kwargs = (; n = 2)) |> run!\n ## Check the proper output format - must parse to Int, use do-syntax\n ## We can provide feedback via a function!\n function feedback_f(aicall)\n "Output: $(last_output(aicall))\nFeedback: You must respond only with digits!!"\n end\n airetry!(out, feedback_f) do aicall\n !isnothing(tryparse(Int, last_output(aicall)))\n end\n ## Give a hint on bounds\n lower_bound = (user_number ÷ 10) * 10\n upper_bound = lower_bound + 10\n airetry!(\n out, "The number is between or equal to $lower_bound to $upper_bound.") do aicall\n guess = tryparse(Int, last_output(aicall))\n lower_bound <= guess <= upper_bound\n end\n ## You can make at most 3x guess now -- if there is max_retries in `config.max_retries` left\n max_retries = out.config.retries + 3\n function feedback_f2(aicall)\n guess = tryparse(Int, last_output(aicall))\n "Your guess of $(guess) is wrong, it's $(abs(guess-user_number)) numbers away."\n end\n airetry!(out, feedback_f2; max_retries) do aicall\n tryparse(Int, last_output(aicall)) == user_number\n end\n\n ## Evaluate the best guess\n @info "Results: Guess: $(last_output(out)) vs User: $user_number (Number of calls made: $(out.config.calls))"\n return out\nend\n\n# Let's play the game\nout = llm_guesser(33)\n[ Info: Condition not met. Retrying...\n[ Info: Condition not met. Retrying...\n[ Info: Condition not met. Retrying...\n[ Info: Condition not met. Retrying...\n[ Info: Results: Guess: 33 vs User: 33 (Number of calls made: 10)
Yay! We got it 😃
Now, we could explore different samples (eg, print_samples(out.samples)) or see what the model guessed at each step:
Note that if there are multiple "branches" the model will see only the feedback of its own and its ancestors not the other "branches". If you wanted to provide ALL feedback, set RetryConfig(; n_samples=1) to remove any "branching". It fixing will be done sequentially in one conversation and the model will see all feedback (less powerful if the model falls into a bad state). Alternatively, you can tweak the feedback function.
Evalutes the condition f_cond (must return Bool) on the aicall object. If the condition is not met, it will return the best sample to retry from and provide feedback.
Mutating as the results are saved in aicall.samples
If evaluate_all is true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample..
For f_cond and feedback functions, you can use the last_message and last_output utilities to access the last message and last output in the conversation, respectively.
Arguments
f_cond::Function: A function that accepts the aicall object and returns a boolean value. Retry will be attempted if the condition is not met (f_cond -> false).
aicall::AICallBlock: The aicall object to evaluate the condition on.
feedback::Union{AbstractString, Function}: Feedback to provide if the condition is not met. If a function is provided, it must accept the aicall object as the only argument and return a string.
evaluate_all::Bool=false: If true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample.
feedback_expensive::Bool=false: If false, it will provide feedback to all samples that fail the condition. If feedback function is expensive to call (eg, another ai* function), set this to true and feedback will be provided only to the sample we will retry from.
Returns
a tuple (condition_passed, sample), where condition_passed is a boolean indicating whether the condition was met, and sample is the best sample to retry from.
Example
julia
# Mimic AIGenerate run!\naicall = AIGenerate("Say hi!"; config = RetryConfig(; n_samples = 2))\nsample = expand!(aicall.samples, aicall.conversation; success = true)\naicall.active_sample_id = sample.id\n\n# Return whether it passed and node to take the next action from\ncond, node = AT.evaluate_condition!(x -> occursin("hi", last_output(x)), aicall)\n\n# Checks:\ncond == true\nnode == sample\nnode.wins == 1
With feedback: ```julia
Mimic AIGenerate run with feedback
aicall = AIGenerate( :BlankSystemUser; system = "a", user = "b") sample = expand!(aicall.samples, aicall.conversation; success = true) aicall.active_sample_id = sample.id
Evaluate
cond, node = AT.evaluate_condition!( x -> occursin("NOTFOUND", last_output(x)), aicall, "Feedback X") cond == false # fail sample == node # same node (no other choice) node.wins == 0 node.feedback == " Feedback X"
Executes the code fixing process encapsulated by the AICodeFixer instance. This method iteratively refines and fixes code by running the AI call in a loop for a specified number of rounds, using feedback from the code evaluation (aicodefixer_feedback) to improve the outcome in each iteration.
Arguments
codefixer::AICodeFixer: An instance of AICodeFixer containing the AI call, templates, and settings for the code fixing session.
verbose::Int=1: Verbosity level for logging. A higher value indicates more detailed logging.
max_conversation_length::Int=32000: Maximum length in characters for the conversation history to keep it within manageable limits, especially for large code fixing sessions.
num_rounds::Union{Nothing, Int}=nothing: Number of additional rounds for the code fixing session. If nothing, the value from the AICodeFixer instance is used.
run_kwargs...: Additional keyword arguments that are passed to the AI function.
Returns
AICodeFixer: The updated AICodeFixer instance with the results of the code fixing session.
The run! method drives the core logic of the AICodeFixer, iterating through rounds of AI interactions to refine and fix code.
In each round, it applies feedback based on the current state of the conversation, allowing the AI to respond more effectively.
The conversation history is managed to ensure it stays within the specified max_conversation_length, keeping the AI's focus on relevant parts of the conversation.
This iterative process is essential for complex code fixing tasks where multiple interactions and refinements are required to achieve the desired outcome.
Executes the AI call wrapped by an AICallBlock instance. This method triggers the actual communication with the AI model and processes the response based on the provided conversation context and parameters.
Note: Currently return_all must always be set to true.
Arguments
aicall::AICallBlock: An instance of AICallBlock which encapsulates the AI function call along with its context and parameters (eg, AICall, AIGenerate)
verbose::Integer=1: A verbosity level for logging. A higher value indicates more detailed logging.
catch_errors::Union{Nothing, Bool}=nothing: A flag to indicate whether errors should be caught and saved to aicall.error. If nothing, it defaults to aicall.config.catch_errors.
return_all::Bool=true: A flag to indicate whether the whole conversation from the AI call should be returned. It should always be true.
kwargs...: Additional keyword arguments that are passed to the AI function.
Returns
AICallBlock: The same AICallBlock instance, updated with the results of the AI call. This includes updated conversation, success status, and potential error information.
Example
julia
aicall = AICall(aigenerate)\nrun!(aicall)
Alternatively, you can trigger the run! call by using the AICall as a functor and calling it with a string or a UserMessage:
julia
aicall = AICall(aigenerate)\naicall("Say hi!")
Notes
The run! method is a key component of the lazy evaluation model in AICall. It allows for the deferred execution of AI function calls, providing flexibility in how and when AI interactions are conducted.
The method updates the AICallBlock instance with the outcome of the AI call, including any generated responses, success or failure status, and error information if an error occurred.
This method is essential for scenarios where AI interactions are based on dynamic or evolving contexts, as it allows for real-time updates and responses based on the latest information.
Selects the best node from the tree using the given scoring (UCT or ThompsonSampling). Defaults to UCT. Thompson Sampling is more random with small samples, while UCT stabilizes much quicker thanks to looking at parent nodes as well.
Ordering can be either :PreOrderDFS or :PostOrderDFS. Defaults to :PostOrderDFS, which favors the leaves (end points of the tree).
Example
Compare the different scoring methods:
julia
# Set up mock samples and scores\ndata = PT.AbstractMessage[]\nroot = SampleNode(; data)\nchild1 = expand!(root, data)\nbackpropagate!(child1; wins = 1, visits = 1)\nchild2 = expand!(root, data)\nbackpropagate!(child2; wins = 0, visits = 1)\nchild11 = expand!(child1, data)\nbackpropagate!(child11; wins = 1, visits = 1)\n\n# Select with UCT\nn = select_best(root, UCT())\nSampleNode(id: 29826, stats: 1/1, length: 0)\n\n# Show the tree:\nprint_samples(root; scoring = UCT())\n## SampleNode(id: 13184, stats: 2/3, score: 0.67, length: 0)\n## ├─ SampleNode(id: 26078, stats: 2/2, score: 2.05, length: 0)\n## │ └─ SampleNode(id: 29826, stats: 1/1, score: 2.18, length: 0)\n## └─ SampleNode(id: 39931, stats: 0/1, score: 1.48, length: 0)\n\n# Select with ThompsonSampling - much more random with small samples\nn = select_best(root, ThompsonSampling())\nSampleNode(id: 26078, stats: 2/2, length: 0)\n\n# Show the tree (run it a few times and see how the scores jump around):\nprint_samples(root; scoring = ThompsonSampling())\n## SampleNode(id: 13184, stats: 2/3, score: 0.6, length: 0)\n## ├─ SampleNode(id: 26078, stats: 2/2, score: 0.93, length: 0)\n## │ └─ SampleNode(id: 29826, stats: 1/1, score: 0.22, length: 0)\n## └─ SampleNode(id: 39931, stats: 0/1, score: 0.84, length: 0)
Truncates a given conversation to a max_conversation_length characters by removing messages "in the middle". It tries to retain the original system+user message and also the most recent messages.
Practically, if a conversation is too long, it will start by removing the most recent message EXCEPT for the last two (assumed to be the last AIMessage with the code and UserMessage with the feedback
Arguments
max_conversation_length is in characters; assume c. 2-3 characters per LLM token, so 32000 should correspond to 16K context window.
api_key::AbstractString: The API key to use for the search. Get an API key from Tavily.
search_depth::AbstractString: The depth of the search. Can be either "basic" or "advanced". Default is "basic". Advanced search calls equal to 2 requests.
include_answer::Bool: Whether to include the answer in the search results. Default is false.
include_raw_content::Bool: Whether to include the raw content in the search results. Default is false.
max_results::Integer: The maximum number of results to return. Default is 5.
include_images::Bool: Whether to include images in the search results. Default is false.
include_domains::AbstractVector{<:AbstractString}: A list of domains to include in the search results. Default is an empty list.
exclude_domains::AbstractVector{<:AbstractString}: A list of domains to exclude from the search results. Default is an empty list.
Example
julia
r = create_websearch("Who is King Charles?")
Even better, you can get not just the results but also the answer:
julia
r = create_websearch("Who is King Charles?"; include_answer = true)
Note: This module is experimental and may change in future releases. The intention is for the functionality to be moved to separate packages over time.
embeddings::Union{Nothing, Matrix{<:Real}}: for semantic search
tags::Union{Nothing, AbstractMatrix{<:Bool}}: for exact search, filtering, etc. This is often a sparse matrix indicating which chunks have the given tag (see tag_vocab for the position lookup)
tags_vocab::Union{Nothing, Vector{<:AbstractString}}: vocabulary for the tags matrix (each column in tags is one item in tags_vocab and rows are the chunks)
sources::Vector{<:AbstractString}: sources of the chunks
extras::Union{Nothing, AbstractVector}: additional data, eg, metadata, source code, etc.
Dispatch for retrieve with advanced retrieval methods to improve result quality. Compared to SimpleRetriever, it adds rephrasing the query and reranking the results.
Fields
rephraser::AbstractRephraser: the rephrasing method, dispatching rephrase - uses HyDERephraser
embedder::AbstractEmbedder: the embedding method, dispatching get_embeddings (see Preparation Stage for more details) - uses BatchEmbedder
finder::AbstractSimilarityFinder: the similarity search method, dispatching find_closest - uses CosineSimilarity
tagger::AbstractTagger: the tag generating method, dispatching get_tags (see Preparation Stage for more details) - uses NoTagger
filter::AbstractTagFilter: the tag matching method, dispatching find_tags - uses NoTagFilter
reranker::AbstractReranker: the reranking method, dispatching rerank - uses CohereReranker
A struct for storing references to chunks in the given index (identified by index_id) called positions and scores holding the strength of similarity (=1 is the highest, most similar). It's the result of the retrieval stage of RAG.
Fields
index_id::Symbol: the id of the index from which the candidates are drawn
positions::Vector{Int}: the positions of the candidates in the index (ie, 5 refers to the 5th chunk in the index - chunks(index)[5])
scores::Vector{Float32}: the similarity scores of the candidates from the query (higher is better)
embeddings::Union{Nothing, Matrix{<:Real}}: for semantic search
tags::Union{Nothing, AbstractMatrix{<:Bool}}: for exact search, filtering, etc. This is often a sparse matrix indicating which chunks have the given tag (see tag_vocab for the position lookup)
tags_vocab::Union{Nothing, Vector{<:AbstractString}}: vocabulary for the tags matrix (each column in tags is one item in tags_vocab and rows are the chunks)
sources::Vector{<:AbstractString}: sources of the chunks
extras::Union{Nothing, AbstractVector}: additional data, eg, metadata, source code, etc.
Default method for build_context! method. It simply enumerates the context snippets around each position in candidates. When possibly, it will add surrounding chunks (from the same source).
Rephraser implemented using the provided AI Template (eg, ...) and standard chat model.
It uses a prompt-based rephrasing method called HyDE (Hypothetical Document Embedding), where instead of looking for an embedding of the question, we look for the documents most similar to a synthetic passage that would be a good answer to our question.
Tagger for get_tags functions, which generates possible tags for each chunk via aiextract. You can customize it via prompt template (default: :RAGExtractMetadataShort), but it's quite open-ended (ie, AI decides the possible tags).
Default configuration for RAG. It uses SimpleIndexer, SimpleRetriever, and SimpleGenerator as default components.
To customize the components, replace corresponding fields for each step of the RAG pipeline (eg, use subtypes(AbstractIndexBuilder) to find the available options).
A struct for debugging RAG answers. It contains the question, answer, context, and the candidate chunks at each step of the RAG pipeline.
Think of the flow as question -> rephrased_questions -> answer -> final_answer with the context and candidate chunks helping along the way.
Fields
question::AbstractString: the original question
rephrased_questions::Vector{<:AbstractString}: a vector of rephrased questions (eg, HyDe, Multihop, etc.)
answer::AbstractString: the generated answer
final_answer::AbstractString: the refined final answer (eg, after CorrectiveRAG), also considered the FINAL answer (it must be always available)
context::Vector{<:AbstractString}: the context used for retrieval (ie, the vector of chunks and their surrounding window if applicable)
sources::Vector{<:AbstractString}: the sources of the context (for the original matched chunks)
emb_candidates::CandidateChunks: the candidate chunks from the embedding index (from find_closest)
tag_candidates::Union{Nothing, CandidateChunks}: the candidate chunks from the tag index (from find_tags)
filtered_candidates::CandidateChunks: the filtered candidate chunks (intersection of emb_candidates and tag_candidates)
reranked_candidates::CandidateChunks: the reranked candidate chunks (from rerank)
conversations::Dict{Symbol,Vector{<:AbstractMessage}}: the conversation history for AI steps of the RAG pipeline, use keys that correspond to the function names, eg, :answer or :refine
See also: pprint (pretty printing), annotate_support (for annotating the answer)
Annotation method where we score answer versus each context based on word-level trigrams that match.
It's very simple method (and it can loose some semantic meaning in longer sequences like negative), but it works reasonably well for both text and code.
High-level wrapper for Retrieval-Augmented Generation (RAG), it combines together the retrieve and generate! steps which you can customize if needed.
The simplest version first finds the relevant chunks in index for the question and then sends these chunks to the AI model to help with generating a response to the question.
To customize the components, replace the types (retriever, generator) of the corresponding step of the RAG pipeline - or go into sub-routines within the steps. Eg, use subtypes(AbstractRetriever) to find the available options.
Arguments
cfg::AbstractRAGConfig: The configuration for the RAG pipeline. Defaults to RAGConfig(), where you can swap sub-types to customize the pipeline.
index::AbstractChunkIndex: The chunk index to search for relevant text.
question::AbstractString: The question to be answered.
return_all::Bool: If true, returns the details used for RAG along with the response.
verbose::Integer: If >0, enables verbose logging. The higher the number, the more nested functions will log.
api_kwargs: API parameters that will be forwarded to ALL of the API calls (aiembed, aigenerate, and aiextract).
retriever::AbstractRetriever: The retriever to use for finding relevant chunks. Defaults to cfg.retriever, eg, SimpleRetriever (with no question rephrasing).
retriever_kwargs::NamedTuple: API parameters that will be forwarded to the retriever call. Examples of important ones:
top_k::Int: Number of top candidates to retrieve based on embedding similarity.
top_n::Int: Number of candidates to return after reranking.
tagger::AbstractTagger: Tagger to use for tagging the chunks. Defaults to NoTagger().
tagger_kwargs::NamedTuple: API parameters that will be forwarded to the tagger call. You could provide the explicit tags directly with PassthroughTagger and tagger_kwargs = (; tags = ["tag1", "tag2"]).
generator::AbstractGenerator: The generator to use for generating the answer. Defaults to cfg.generator, eg, SimpleGenerator.
generator_kwargs::NamedTuple: API parameters that will be forwarded to the generator call. Examples of important ones:
answerer_kwargs::NamedTuple: API parameters that will be forwarded to the answerer call. Examples:
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerFromContext.
refiner::AbstractRefiner: The method to use for refining the answer. Defaults to generator.refiner, eg, NoRefiner.
refiner_kwargs::NamedTuple: API parameters that will be forwarded to the refiner call.
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerRefiner.
cost_tracker: An atomic counter to track the total cost of the operations (if you want to track the cost of multiple pipeline runs - it passed around in the pipeline).
Returns
If return_all is false, returns the generated message (msg).
If return_all is true, returns the detail of the full pipeline in RAGResult (see the docs).
See also build_index, retrieve, generate!, RAGResult
Examples
Using airag to get a response for a question:
julia
index = build_index(...) # create an index\nquestion = "How to make a barplot in Makie.jl?"\nmsg = airag(index; question)
To understand the details of the RAG process, use return_all=true
julia
msg, details = airag(index; question, return_all = true)\n# details is a RAGDetails object with all the internal steps of the `airag` function
You can also pretty-print details to highlight generated text vs text that is supported by context. It also includes annotations of which context was used for each part of the response (where available).
julia
PT.pprint(details)
Example with advanced retrieval (with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n). In addition, it will be done with a "custom" locally-hosted model.
julia
cfg = RAGConfig(; retriever = AdvancedRetriever())\n\n# kwargs will be big and nested, let's prepare them upfront\n# we specify "custom" model for each component that calls LLM\nkwargs = (\n retriever_kwargs = (;\n top_k = 100,\n top_n = 5,\n rephraser_kwargs = (;\n model = "custom"),\n embedder_kwargs = (;\n model = "custom"),\n tagger_kwargs = (;\n model = "custom")),\n generator_kwargs = (;\n answerer_kwargs = (;\n model = "custom"),\n refiner_kwargs = (;\n model = "custom")),\n api_kwargs = (;\n url = "http://localhost:8080"))\n\nresult = airag(cfg, index, question; kwargs...)
Annotates the answer with the overlap/what's supported in context and returns the annotated tree of nodes representing the answer
Returns a "root" node with children nodes representing the sentences/code blocks in the answer. Only the "leaf" nodes are to be printed (to avoid duplication), "leaf" nodes are those with NO children.
Default logic:
Split into sentences/code blocks, then into tokens (~words).
Then match each token (~word) exactly.
If no exact match found, count trigram-based match (include the surrounding tokens for better contextual awareness).
If the match is higher than min_score, it's recorded in the score of the node.
Arguments
annotater::TrigramAnnotater: Annotater to use
answer::AbstractString: Text to annotate
context::AbstractVector: Context to annotate against, ie, look for "support" in the texts in context
min_score::Float64: Minimum score to consider a match. Default: 0.5, which means that half of the trigrams of each word should match
skip_trigrams::Bool: Whether to potentially skip trigram matching if exact full match is found. Default: true
hashed::Bool: Whether to use hashed trigrams. It's harder to debug, but it's much faster for larger texts (hashed text are held in a Set to deduplicate). Default: true
sources::Union{Nothing, AbstractVector{<:AbstractString}}: Sources to add at the end of the context. Default: nothing
min_source_score::Float64: Minimum score to consider/to display a source. Default: 0.25, which means that at least a quarter of the trigrams of each word should match to some context. The threshold is lower than min_score, because it's average across ALL words in a block, so it's much harder to match fully with generated text.
add_sources::Bool: Whether to add sources at the end of each code block/sentence. Sources are addded in the square brackets like "[1]". Default: true
add_scores::Bool: Whether to add source-matching scores at the end of each code block/sentence. Scores are added in the square brackets like "[0.75]". Default: true
kwargs: Additional keyword arguments to pass to trigram_support! and set_node_style!. See their documentation for more details (eg, customize the colors of the nodes based on the score)
Example
julia
annotater = TrigramAnnotater()\ncontext = [\n "This is a test context.", "Another context sentence.", "Final piece of context."]\nanswer = "This is a test context. Another context sentence."\n\nannotated_root = annotate_support(annotater, answer, context)\npprint(annotated_root) # pretty print the annotated tree
Dispatch for annotate_support for AbstractRAGResult type. It extracts the final_answer and context from the result and calls annotate_support with them.
See annotate_support for more details.
Example
julia
res = RAGResult(; question = "", final_answer = "This is a test.",\n context = ["Test context.", "Completely different"])\nannotated_root = annotate_support(annotater, res)\nPT.pprint(annotated_root)
Build context strings for each position in candidates considering a window margin around each position. If mutating version is used (build_context!), it will use result.reranked_candidates to update the result.context field.
Arguments
contexter::ContextEnumerator: The method to use for building the context. Enumerates the snippets.
index::ChunkIndex: The index containing chunks and sources.
candidates::CandidateChunks: Candidate chunks which contain positions to extract context from.
verbose::Bool: If true, enables verbose logging.
chunks_window_margin::Tuple{Int, Int}: A tuple indicating the margin (before, after) around each position to include in the context. Defaults to (1,1), which means 1 preceding and 1 suceeding chunk will be included. With (0,0), only the matching chunks will be included.
Returns
Vector{String}: A vector of context strings, each corresponding to a position in reranked_candidates.
Examples
julia
index = ChunkIndex(...) # Assuming a proper index is defined\ncandidates = CandidateChunks(index.id, [2, 4], [0.1, 0.2])\ncontext = build_context(ContextEnumerator(), index, candidates; chunks_window_margin=(0, 1)) # include only one following chunk for each matching chunk
Build an INDEX for RAG (Retriever-Augmented Generation) applications from the provided file paths. INDEX is a object storing the document chunks and their embeddings (and potentially other information).
The function processes each file or document (depending on chunker), splits its content into chunks, embeds these chunks, optionally extracts metadata, and then combines this information into a retrievable index.
Define your own methods via indexer and its subcomponents (chunker, embedder, tagger).
Arguments
indexer::AbstractIndexBuilder: The indexing logic to use. Default is SimpleIndexer().
files_or_docs: A vector of valid file paths OR string documents to be indexed (chunked and embedded). Specify which mode to use via chunker.
verbose: An Integer specifying the verbosity of the logs. Default is 1 (high-level logging). 0 is disabled.
extras: An optional vector of extra information to be stored with each chunk. Default is nothing.
index_id: A unique identifier for the index. Default is a generated symbol.
chunker: The chunker logic to use for splitting the documents. Default is TextChunker().
chunker_kwargs: Parameters to be provided to the get_chunks function. Useful to change the separators or max_length.
sources: A vector of strings indicating the source of each chunk. Default is equal to files_or_docs.
embedder: The embedder logic to use for embedding the chunks. Default is BatchEmbedder().
embedder_kwargs: Parameters to be provided to the get_embeddings function. Useful to change the target_batch_size_length or reduce asyncmap tasks ntasks.
model: The model to use for embedding. Default is PT.MODEL_EMBEDDING.
tagger: The tagger logic to use for extracting tags from the chunks. Default is NoTagger(), ie, skip tag extraction. There are also PassthroughTagger and OpenTagger.
tagger_kwargs: Parameters to be provided to the get_tags function.
model: The model to use for tags extraction. Default is PT.MODEL_CHAT.
template: A template to be used for tags extraction. Default is :RAGExtractMetadataShort.
tags: A vector of vectors of strings directly providing the tags for each chunk. Applicable for tagger::PasstroughTagger.
api_kwargs: Parameters to be provided to the API endpoint. Shared across all API calls if provided.
cost_tracker: A Threads.Atomic{Float64} object to track the total cost of the API calls. Useful to pass the total cost to the parent call.
Returns
ChunkIndex: An object containing the compiled index of chunks, embeddings, tags, vocabulary, and sources.
# Default is loading a vector of strings and chunking them (`TextChunker()`)\nindex = build_index(SimpleIndexer(), texts; chunker_kwargs = (; max_length=10))\n\n# Another example with tags extraction, splitting only sentences and verbose output\n# Assuming `test_files` is a vector of file paths\nindexer = SimpleIndexer(chunker=FileChunker(), tagger=OpenTagger())\nindex = build_index(indexer, test_files; \n chunker_kwargs(; separators=[". "]), verbose=true)
Notes
If you get errors about exceeding embedding input sizes, first check the max_length in your chunks. If that does NOT resolve the issue, try changing the embedding_kwargs. In particular, reducing the target_batch_size_length parameter (eg, 10_000) and number of tasks ntasks=1. Some providers cannot handle large batch sizes (eg, Databricks).
Create a collection of question and answer evaluations (QAEvalItem) from document chunks and sources. This function generates Q&A pairs based on the provided document chunks, using a specified AI model and template.
Arguments
doc_chunks::Vector{<:AbstractString}: A vector of document chunks, each representing a segment of text.
sources::Vector{<:AbstractString}: A vector of source identifiers corresponding to each chunk in doc_chunks (eg, filenames or paths).
model: The AI model used for generating Q&A pairs. Default is PT.MODEL_CHAT.
instructions::String: Additional instructions or context to provide to the model generating QA sets. Defaults to "None.".
qa_template::Symbol: A template symbol that dictates the AITemplate that will be used. It must have placeholder context. Default is :CreateQAFromContext.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API endpoint.
verbose::Bool: If true, additional information like costs will be logged. Defaults to true.
Returns
Vector{QAEvalItem}: A vector of QAEvalItem structs, each containing a source, context, question, and answer. Invalid or empty items are filtered out.
Notes
The function internally uses aiextract to generate Q&A pairs based on the provided qa_template. So you can use any kwargs that you want.
Each QAEvalItem includes the context (document chunk), the generated question and answer, and the source.
The function tracks and reports the cost of AI calls if verbose is enabled.
Items where the question, answer, or context is empty are considered invalid and are filtered out.
Examples
Creating Q&A evaluations from a set of document chunks:
julia
doc_chunks = ["Text from document 1", "Text from document 2"]\nsources = ["source1", "source2"]\nqa_evals = build_qa_evals(doc_chunks, sources)
Finds the indices of chunks (represented by embeddings in emb) that are closest (in cosine similarity for CosineSimilarity()) to query embedding (query_emb).
finder is the logic used for the similarity search. Default is CosineSimilarity.
If minimum_similarity is provided, only indices with similarity greater than or equal to it are returned. Similarity can be between -1 and 1 (-1 = completely opposite, 1 = exactly the same).
Assume we already have `index`\n\nquestion = "What are the best practices for parallel computing in Julia?"\n\n# Retrieve the relevant chunks - returns RAGResult\nresult = retrieve(index, question)\n\n# Generate the answer using the default generator, mutates the same result\nresult = generate!(index, result)
Chunks the provided files_or_docs into chunks of maximum length max_length (if possible with provided separators).
Supports two modes of operation:
chunker = FileChunker(): The function opens each file in files_or_docs and reads its contents.
chunker = TextChunker(): The function assumes that files_or_docs is a vector of strings to be chunked, you MUST provide corresponding sources.
Arguments
files_or_docs: A vector of valid file paths OR string documents to be chunked.
separators: A list of strings used as separators for splitting the text in each file into chunks. Default is [\\n\\n", ". ", "\\n", " "]. See recursive_splitter for more details.
max_length: The maximum length of each chunk (if possible with provided separators). Default is 256.
sources: A vector of strings indicating the source of each chunk. Default is equal to files_or_docs (for reader=:files)
Embeds a vector of docs using the provided model (kwarg model) in a batched manner - BatchEmbedder.
BatchEmbedder tries to batch embedding calls for roughly 80K characters per call (to avoid exceeding the API rate limit) to reduce network latency.
Notes
docs are assumed to be already chunked to the reasonable sizes that fit within the embedding context limit.
If you get errors about exceeding input sizes, first check the max_length in your chunks. If that does NOT resolve the issue, try reducing the target_batch_size_length parameter (eg, 10_000) and number of tasks ntasks=1. Some providers cannot handle large batch sizes.
Arguments
docs: A vector of strings to be embedded.
verbose: A boolean flag for verbose output. Default is true.
model: The model to use for embedding. Default is PT.MODEL_EMBEDDING.
cost_tracker: A Threads.Atomic{Float64} object to track the total cost of the API calls. Useful to pass the total cost to the parent call.
target_batch_size_length: The target length (in characters) of each batch of document chunks sent for embedding. Default is 80_000 characters. Speeds up embedding process.
ntasks: The number of tasks to use for asyncmap. Default is 4 * Threads.nthreads().
Pass tags directly as Vector of Vectors of strings (ie, tags[i] is the tags for docs[i]). It then builds the vocabulary from the tags and returns both the tags in matrix form and the vocabulary.
Pretty-prints the annotation parent_node (or RAGResult) to the io stream (or returns the string) in HTML format (assumes node is styled with styler HTMLStyler).
It wraps each "token" into a span with requested styling (HTMLStyler's properties classes and styles). It also replaces new lines with <br> for better HTML formatting.
For any non-HTML styler, it prints the content as plain text.
Returns
nothing if io is provided
or the string with HTML-formatted text (if io is not provided, we print the result out)
See also HTMLStyler, annotate_support, and set_node_style! for how the styling is applied and what the arguments mean.
Examples
Note: RT is an alias for PromptingTools.Experimental.RAGTools
Simple start directly with the RAGResult:
julia
# set up the text/RAGResult\ncontext = [\n "This is a test context.", "Another context sentence.", "Final piece of context."]\nanswer = "This is a test answer. It has multiple sentences."\nrag = RT.RAGResult(; context, final_answer=answer, question="")\n\n# print the HTML\nprint_html(rag)
Low-level control by creating our AnnotatedNode:
julia
# prepare your HTML styling\nstyler_kwargs = (;\n default_styler=RT.HTMLStyler(),\n low_styler=RT.HTMLStyler(styles="color:magenta", classes=""),\n medium_styler=RT.HTMLStyler(styles="color:blue", classes=""),\n high_styler=RT.HTMLStyler(styles="", classes=""))\n\n# annotate the text\ncontext = [\n "This is a test context.", "Another context sentence.", "Final piece of context."]\nanswer = "This is a test answer. It has multiple sentences."\n\nparent_node = RT.annotate_support(\n RT.TrigramAnnotater(), answer, context; add_sources=false, add_scores=false, styler_kwargs...)\n\n# print the HTML\nprint_html(parent_node)\n\n# or to accumulate more nodes\nio = IOBuffer()\nprint_html(io, parent_node)
Rephrases the question using the provided rephraser template = RAGQueryHyDE.
Special flavor of rephrasing using HyDE (Hypothetical Document Embedding) method, which aims to find the documents most similar to a synthetic passage that would be a good answer to our question.
Returns both the original and the rephrased question.
Arguments
rephraser: Type that dictates the logic of rephrasing step.
question: The question to be rephrased.
model: The model to use for rephrasing. Default is PT.MODEL_CHAT.
template: The rephrasing template to use. Default is :RAGQueryHyDE. Find more with aitemplates("rephrase").
verbose: A boolean flag indicating whether to print verbose logging. Default is true.
Retrieves the most relevant chunks from the index for the given question and returns them in the RAGResult object.
This is the main entry point for the retrieval stage of the RAG pipeline. It is often followed by generate! step.
Notes:
The default flow is build_context! -> answer! -> refine! -> postprocess!.
The arguments correspond to the steps of the retrieval process (rephrasing, embedding, finding similar docs, tagging, filtering by tags, reranking). You can customize each step by providing a new custom type that dispatches the corresponding function, eg, create your own type struct MyReranker<:AbstractReranker end and define the custom method for it rerank(::MyReranker,...) = ....
Note: Discover available retrieval sub-types for each step with subtypes(AbstractRephraser) and similar for other abstract types.
If you're using locally-hosted models, you can pass the api_kwargs with the url field set to the model's URL and make sure to provide corresponding model kwargs to rephraser, embedder, and tagger to use the custom models (they make AI calls).
Arguments
retriever: The retrieval method to use. Default is SimpleRetriever but could be AdvancedRetriever for more advanced retrieval.
index: The index that holds the chunks and sources to be retrieved from.
question: The question to be used for the retrieval.
verbose: If >0, it prints out verbose logging. Default is 1. If you set it to 2, it will print out logs for each sub-function.
top_k: The TOTAL number of closest chunks to return from find_closest. Default is 100. If there are multiple rephrased questions, the number of chunks per each item will be top_k ÷ number_of_rephrased_questions.
top_n: The TOTAL number of most relevant chunks to return for the context (from rerank step). Default is 5.
api_kwargs: Additional keyword arguments to be passed to the API calls (shared by all ai* calls).
rephraser: Transform the question into one or more questions. Default is retriever.rephraser.
rephraser_kwargs: Additional keyword arguments to be passed to the rephraser.
model: The model to use for rephrasing. Default is PT.MODEL_CHAT.
template: The rephrasing template to use. Default is :RAGQueryOptimizer or :RAGQueryHyDE (depending on the rephraser selected).
embedder: The embedding method to use. Default is retriever.embedder.
embedder_kwargs: Additional keyword arguments to be passed to the embedder.
finder: The similarity search method to use. Default is retriever.finder, often CosineSimilarity.
finder_kwargs: Additional keyword arguments to be passed to the similarity finder.
tagger: The tag generating method to use. Default is retriever.tagger.
tagger_kwargs: Additional keyword arguments to be passed to the tagger. Noteworthy arguments:
tags: Directly provide the tags to use for filtering (can be String, Regex, or Vector{String}). Useful for tagger = PassthroughTagger.
filter: The tag matching method to use. Default is retriever.filter.
filter_kwargs: Additional keyword arguments to be passed to the tag filter.
reranker: The reranking method to use. Default is retriever.reranker.
reranker_kwargs: Additional keyword arguments to be passed to the reranker.
model: The model to use for reranking. Default is rerank-english-v2.0 if you use reranker = CohereReranker().
cost_tracker: An atomic counter to track the cost of the retrieval. Default is Threads.Atomic{Float64}(0.0).
Find the 5 most relevant chunks from the index for the given question.
julia
# assumes you have an existing index `index`\nretriever = SimpleRetriever()\n\nresult = retrieve(retriever,\n index,\n "What is the capital of France?",\n top_n = 5)\n\n# or use the default retriever (same as above)\nresult = retrieve(retriever,\n index,\n "What is the capital of France?",\n top_n = 5)
Apply more advanced retrieval with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n).
Evaluates a vector of QAEvalItems and returns a vector QAEvalResult. This function assesses the relevance and accuracy of the answers generated in a QA evaluation context.
See ?run_qa_evals for more details.
Arguments
qa_items::AbstractVector{<:QAEvalItem}: The vector of QA evaluation items containing the questions and their answers.
verbose::Bool: If true, enables verbose logging. Defaults to true.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API calls. See ?aiextract for details.
airag_kwargs::NamedTuple: Parameters that will be forwarded to airag calls. See ?airag for details.
qa_evals_kwargs::NamedTuple: Parameters that will be forwarded to run_qa_evals calls. See ?run_qa_evals for details.
parameters_dict::Dict{Symbol, Any}: Track any parameters used for later evaluations. Keys must be Symbols.
Returns
Vector{QAEvalResult}: Vector of evaluation results that includes various scores and metadata related to the QA evaluation.
Example
julia
index = "..." # Assuming a proper index is defined\nqa_items = [QAEvalItem(question="What is the capital of France?", answer="Paris", context="France is a country in Europe."),\n QAEvalItem(question="What is the capital of Germany?", answer="Berlin", context="Germany is a country in Europe.")]\n\n# Let's run a test with `top_k=5`\nresults = run_qa_evals(index, qa_items; airag_kwargs=(;top_k=5), parameters_dict=Dict(:top_k => 5))\n\n# Filter out the "failed" calls\nresults = filter(x->!isnothing(x.answer_score), results);\n\n# See average judge score\nmean(x->x.answer_score, results)
Evaluates a single QAEvalItem using RAG details (RAGResult) and returns a QAEvalResult structure. This function assesses the relevance and accuracy of the answers generated in a QA evaluation context.
Arguments
qa_item::QAEvalItem: The QA evaluation item containing the question and its answer.
ctx::RAGResult: The RAG result used for generating the QA pair, including the original context and the answers. Comes from airag(...; return_context=true)
verbose::Bool: If true, enables verbose logging. Defaults to true.
parameters_dict::Dict{Symbol, Any}: Track any parameters used for later evaluations. Keys must be Symbols.
judge_template::Symbol: The template symbol for the AI model used to judge the answer. Defaults to :RAGJudgeAnswerFromContext.
model_judge::AbstractString: The AI model used for judging the answer's quality. Defaults to standard chat model, but it is advisable to use more powerful model GPT-4.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API endpoint.
Returns
QAEvalResult: An evaluation result that includes various scores and metadata related to the QA evaluation.
Notes
The function computes a retrieval score and rank based on how well the context matches the QA context.
It then uses the judge_template and model_judge to score the answer's accuracy and relevance.
In case of errors during evaluation, the function logs a warning (if verbose is true) and the answer_score will be set to nothing.
Examples
Evaluating a QA pair using a specific context and model:
julia
qa_item = QAEvalItem(question="What is the capital of France?", answer="Paris", context="France is a country in Europe.")\nctx = RAGResult(source="Wikipedia", context="France is a country in Europe.", answer="Paris")\nparameters_dict = Dict("param1" => "value1", "param2" => "value2")\n\neval_result = run_qa_evals(qa_item, ctx, parameters_dict=parameters_dict, model_judge="MyAIJudgeModel")
Splits text block into code or text and sub-splits into units.
If code block, it splits by newline but keep the group_id the same (to have the same source) If text block, splits into sentences, bullets, etc., provides different group_id (to have different source)
Joins the three tokens together. Useful to add boundary tokens (like spaces vs brackets) to the curr_token to improve the matched context (ie, separate partial matches from exact match)
Building a Simple Retrieval-Augmented Generation (RAG) System with RAGTools
Let's build a Retrieval-Augmented Generation (RAG) chatbot, tailored to navigate and interact with the DataFrames.jl documentation. "RAG" is probably the most common and valuable pattern in Generative AI at the moment.
If you're not familiar with "RAG", start with this article.
julia
using LinearAlgebra, SparseArrays
+using PromptingTools
+using PromptingTools.Experimental.RAGTools
+## Note: RAGTools module is still experimental and will change in the future. Ideally, they will be cleaned up and moved to a dedicated package
+using JSON3, Serialization, DataFramesMeta
+using Statistics: mean
+const PT = PromptingTools
+const RT = PromptingTools.Experimental.RAGTools
Let's put together a few text pages from DataFrames.jl docs. Simply go to DataFrames.jl docs and copy&paste a few pages into separate text files. Save them in the examples/data folder (see some example pages provided). Ideally, delete all the noise (like headers, footers, etc.) and keep only the text you want to use for the chatbot. Remember, garbage in, garbage out!
julia
files = [
+ joinpath("examples", "data", "database_style_joins.txt"),
+ joinpath("examples", "data", "what_is_dataframes.txt"),
+]
+# Build an index of chunks, embed them, and create a lookup index of metadata/tags for each chunk
+index = build_index(files; extract_metadata = false);
Let's ask a question
julia
# Embeds the question, finds the closest chunks in the index, and generates an answer from the closest chunks
+answer = airag(index; question = "I like dplyr, what is the equivalent in Julia?")
AIMessage("The equivalent package in Julia to dplyr in R is DataFramesMeta.jl. It provides convenience functions for data manipulation with syntax similar to dplyr.")
First RAG in two lines? Done!
What does it do?
build_index will chunk the documents into smaller pieces, embed them into numbers (to be able to judge the similarity of chunks) and, optionally, create a lookup index of metadata/tags for each chunk)
index is the result of this step and it holds your chunks, embeddings, and other metadata! Just show it 😃
airag will
embed your question
find the closest chunks in the index (use parameters top_k and minimum_similarity to tweak the "relevant" chunks)
[OPTIONAL] extracts any potential tags/filters from the question and applies them to filter down the potential candidates (use extract_metadata=true in build_index, you can also provide some filters explicitly via tag_filter)
[OPTIONAL] re-ranks the candidate chunks (define and provide your own rerank_strategy, eg Cohere ReRank API)
build a context from the closest chunks (use chunks_window_margin to tweak if we include preceding and succeeding chunks as well, see ?build_context for more details)
generate an answer from the closest chunks (use return_all=true to see under the hood and debug your application)
You should save the index for later to avoid re-embedding / re-extracting the document chunks!
However, we want to evaluate the quality of the system. For that, we need a set of questions and answers. Ideally, we would handcraft a set of high-quality Q&A pairs. However, this is time-consuming and expensive. Let's generate them from the chunks in our index!
In practice, you would review each item in this golden evaluation set (and delete any generic/poor questions). It will determine the future success of your app, so you need to make sure it's good!
julia
# Save the evals for later
+JSON3.write("examples/evals.json", evals)
+evals = JSON3.read("examples/evals.json", Vector{RT.QAEvalItem});
Let's explore one evals item – it's not the best quality but gives you the idea!
julia
evals[1]
QAEvalItem:
+ source: examples/data/database_style_joins.txt
+ context: Database-Style Joins
+Introduction to joins
+We often need to combine two or more data sets together to provide a complete picture of the topic we are studying. For example, suppose that we have the following two data sets:
+
+julia> using DataFrames
+ question: What is the purpose of joining two or more data sets together?
+ answer: The purpose of joining two or more data sets together is to provide a complete picture of the topic being studied.
Let's evaluate this QA item with a "judge model" (often GPT-4 is used as a judge).
julia
# Note: that we used the same question, but generated a different context and answer via `airag`
+ctx = airag(index; evals[1].question, return_all = true);
+# ctx is a RAGContext object that keeps all intermediate states of the RAG pipeline for easy evaluation
+judged = aiextract(:RAGJudgeAnswerFromContext;
+ ctx.context,
+ ctx.question,
+ ctx.answer,
+ return_type = RT.JudgeAllScores)
+judged.content
QAEvalResult:
+ source: examples/data/database_style_joins.txt
+ context: outerjoin: the output contains rows for values of the key that exist in any of the passed data frames.
+semijoin: Like an inner join, but output is restricted to columns from the first (left) argument.
+ question: What is the difference between outer join and semi join?
+ answer: The purpose of joining two or more data sets together is to combine them in order to provide a complete picture or analysis of a specific topic or dataset. By joining data sets, we can combine information from multiple sources to gain more insights and make more informed decisions.
+ retrieval_score: 0.0
+ retrieval_rank: nothing
+ answer_score: 5
+ parameters: Dict(:top_k => 3)
Fortunately, we don't have to do this one by one – let's evaluate all our Q&A pairs at once.
Let's run each question & answer through our eval loop in async (we do it only for the first 10 to save time). See the ?airag for which parameters you can tweak, eg, top_k
julia
results = asyncmap(evals[1:10]) do qa_item
+ # Generate an answer -- often you want the model_judge to be the highest quality possible, eg, "GPT-4 Turbo" (alias "gpt4t)
+ msg, ctx = airag(index; qa_item.question, return_all = true,
+ top_k = 3, verbose = false, model_judge = "gpt4t")
+ # Evaluate the response
+ # Note: you can log key parameters for easier analysis later
+ run_qa_evals(qa_item, ctx; parameters_dict = Dict(:top_k => 3), verbose = false)
+end
+## Note that the "failed" evals can show as "nothing" (failed as in there was some API error or parsing error), so make sure to handle them.
+results = filter(x->!isnothing(x.answer_score), results);
Note: You could also use the vectorized version results = run_qa_evals(evals) to evaluate all items at once.
julia
+# Let's take a simple average to calculate our score
+@info "RAG Evals: $(length(results)) results, Avg. score: $(round(mean(x->x.answer_score, results);digits=1)), Retrieval score: $(100*round(Int,mean(x->x.retrieval_score,results)))%"
Note: The retrieval score is 100% only because we have two small documents and running on 10 items only. In practice, you would have a much larger document set and a much larger eval set, which would result in a more representative retrieval score.
You can also analyze the results in a DataFrame:
julia
df = DataFrame(results)
10×8 DataFrame
Row
source
context
question
answer
retrieval_score
retrieval_rank
answer_score
parameters
String
String
String
SubStrin…
Float64
Int64
Float64
Dict…
1
examples/data/database_style_joins.txt
Database-Style Joins\nIntroduction to joins\nWe often need to combine two or more data sets together to provide a complete picture of the topic we are studying. For example, suppose that we have the following two data sets:\n\njulia> using DataFrames
What is the purpose of joining two or more data sets together?
The purpose of joining two or more data sets together is to combine the data sets based on a common key and provide a complete picture of the topic being studied.
1.0
1
5.0
Dict(:top_k=>3)
2
examples/data/database_style_joins.txt
julia> people = DataFrame(ID=[20, 40], Name=["John Doe", "Jane Doe"])\n2×2 DataFrame\n Row │ ID Name\n │ Int64 String\n─────┼─────────────────\n 1 │ 20 John Doe\n 2 │ 40 Jane Doe
What is the DataFrame called 'people' composed of?
The DataFrame called 'people' consists of two columns: 'ID' and 'Name'. The 'ID' column contains integers, and the 'Name' column contains strings.
1.0
1
4.0
Dict(:top_k=>3)
3
examples/data/database_style_joins.txt
julia> jobs = DataFrame(ID=[20, 40], Job=["Lawyer", "Doctor"])\n2×2 DataFrame\n Row │ ID Job\n │ Int64 String\n─────┼───────────────\n 1 │ 20 Lawyer\n 2 │ 40 Doctor
What are the jobs and IDs listed in the dataframe?
The jobs and IDs listed in the dataframe are as follows:\n\nID: 20\nJob: Lawyer\n\nID: 40\nJob: Doctor
1.0
1
4.67
Dict(:top_k=>3)
4
examples/data/database_style_joins.txt
We might want to work with a larger data set that contains both the names and jobs for each ID. We can do this using the innerjoin function:
How can we combine the names and jobs for each ID in a larger data set?
We can use the `innerjoin` function to combine the names and jobs for each ID in a larger data set.
1.0
1
4.33333
Dict(:top_k=>3)
5
examples/data/database_style_joins.txt
julia> innerjoin(people, jobs, on = :ID)\n2×3 DataFrame\n Row │ ID Name Job\n │ Int64 String String\n─────┼─────────────────────────\n 1 │ 20 John Doe Lawyer\n 2 │ 40 Jane Doe Doctor
What is the name of the person with the ID 40 and their job?
The name of the person with the ID 40 is Jane Doe and their job is Doctor.
1.0
2
4.67
Dict(:top_k=>3)
6
examples/data/database_style_joins.txt
In relational database theory, this operation is generally referred to as a join. The columns used to determine which rows should be combined during a join are called keys.\n\nThe following functions are provided to perform seven kinds of joins:
What are the different kinds of joins?
The different kinds of joins are:\n\n1. Inner Join: Returns only the rows that have matching values in both data frames.\n2. Left Join: Returns all rows from the left data frame and the matching rows from the right data frame.\n3. Right Join: Returns all rows from the right data frame and the matching rows from the left data frame.\n4. Full Outer Join: Returns all rows from both data frames and fills in missing values with null.\n5. Cross Join: Returns the cartesian product of the rows from both data frames.\n6. Semi Join: Returns only the rows from the left data frame that have matching values in the right data frame.\n7. Anti Join: Returns only the rows from the left data frame that do not have matching values in the right data frame.
1.0
1
4.66667
Dict(:top_k=>3)
7
examples/data/database_style_joins.txt
innerjoin: the output contains rows for values of the key that exist in all passed data frames.
What does the output of the inner join operation contain?
The output of the inner join operation contains only the rows for values of the key that exist in all passed data frames.
1.0
1
5.0
Dict(:top_k=>3)
8
examples/data/database_style_joins.txt
leftjoin: the output contains rows for values of the key that exist in the first (left) argument, whether or not that value exists in the second (right) argument.
What is the purpose of the left join operation?
The purpose of the left join operation is to combine data from two tables based on a common key, where all rows from the left (first) table are included in the output, regardless of whether there is a match in the right (second) table.
1.0
1
4.66667
Dict(:top_k=>3)
9
examples/data/database_style_joins.txt
rightjoin: the output contains rows for values of the key that exist in the second (right) argument, whether or not that value exists in the first (left) argument.
What is the purpose of the right join operation?
The purpose of the right join operation is to include all the rows from the second (right) argument, regardless of whether a match is found in the first (left) argument.
1.0
1
4.67
Dict(:top_k=>3)
10
examples/data/database_style_joins.txt
outerjoin: the output contains rows for values of the key that exist in any of the passed data frames.\nsemijoin: Like an inner join, but output is restricted to columns from the first (left) argument.
What is the difference between outer join and semi join?
The difference between outer join and semi join is that outer join includes rows for values of the key that exist in any of the passed data frames, whereas semi join is like an inner join but only outputs columns from the first argument.
ai*(<optional schema>,<prompt or conversation>; <optional keyword arguments>),
but they differ in purpose:
aigenerate is the general-purpose function to generate any text response with LLMs, ie, it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString)
aiembed is designed to extract embeddings from the AI model's response, ie, it returns DataMessage with field :content containing the embeddings (eg, ans.content isa AbstractArray)
aiextract is designed to extract structured data from the AI model's response and return them as a Julia struct (eg, if we provide return_type=Food, we get ans.content isa Food). You need to define the return type first and then provide it as a keyword argument.
aiclassify is designed to classify the input text into (or simply respond within) a set of discrete choices provided by the user. It can be very useful as an LLM Judge or a router for RAG systems, as it uses the "logit bias trick" and generates exactly 1 token. It returns AIMessage with field :content, but the :content can be only one of the provided choices (eg, ans.content in choices)
aiscan is for working with images and vision-enabled models (as an input), but it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString) similar to aigenerate.
aiimage is for generating images (eg, with OpenAI DALL-E 3). It returns a DataMessage, where the field :content might contain either the URL to download the image from or the Base64-encoded image depending on the user-provided kwarg api_kwargs.response_format.
aitemplates is a helper function to discover available templates and see their details (eg, aitemplates("some keyword") or aitemplates(:AssistantAsk))
If you're using a known model, you do NOT need to provide a schema (the first argument).
Optional keyword arguments in ai* tend to be:
model::String - Which model you want to use
verbose::Bool - Whether you went to see INFO logs around AI costs
return_all::Bool - Whether you want the WHOLE conversation or just the AI answer (ie, whether you want to include your inputs/prompt in the output)
api_kwargs::NamedTuple - Specific parameters for the model, eg, temperature=0.0 to be NOT creative (and have more similar output in each run)
http_kwargs::NamedTuple - Parameters for the HTTP.jl package, eg, readtimeout = 120 to time out in 120 seconds if no response was received.
Experimental: AgentTools
In addition to the above list of ai* functions, you can also use the "lazy" counterparts of these functions from the experimental AgentTools module.
julia
using PromptingTools.Experimental.AgentTools
For example, AIGenerate() will create a lazy instance of aigenerate. It is an instance of AICall with aigenerate as its ai function. It uses exactly the same arguments and keyword arguments as aigenerate (see ?aigenerate for details).
"lazy" refers to the fact that it does NOT generate any output when instantiated (only when run! is called).
Or said differently, the AICall struct and all its flavors (AIGenerate, ...) are designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This allows us to remember user inputs and trigger the LLM call repeatedly if needed, which enables automatic fixing (see ?airetry!).
Experimental: RAGTools
Lastly, we provide a set of tools to build RAG applications (Retrieve, Answer, Generate).
It can be as simple as two calls: build_index and airag (Retrieve, Answer, Generate).
If you then use pretty-printing with PromptingTools.pprint, we highlight the generated text vs text likely sourced from the context and we score how strongly is the generated answer supported by the context. In addition, we annotate each generated chunk with a reference to which source document it likely came from (including the confidence score between 0 and 1).
Google search is great, but it's a context switch. You often have to open a few pages and read through the discussion to find the answer you need. Same with the ChatGPT website.
Imagine you are in VSCode, editing your .gitignore file. How do I ignore a file in all subfolders again?
All you need to do is to type: aai"What to write in .gitignore to ignore file XYZ in any folder or subfolder?"
With aai"" (as opposed to ai""), we make a non-blocking call to the LLM to not prevent you from continuing your work. When the answer is ready, we log it from the background:
plaintext
[ Info: Tokens: 102 @ Cost: $0.0002 in 2.7 seconds
+┌ Info: AIMessage> To ignore a file called "XYZ" in any folder or subfolder, you can add the following line to your .gitignore file:
+│
+│ ```
+│ **/XYZ
+│ ```
+│
+└ This pattern uses the double asterisk (`**`) to match any folder or subfolder, and then specifies the name of the file you want to ignore.
You probably saved 3-5 minutes on this task and probably another 5-10 minutes, because of the context switch/distraction you avoided. It's a small win, but it adds up quickly.
You can use the aigenerate function to replace handlebar variables (eg, ) via keyword arguments.
julia
msg = aigenerate("Say hello to {{name}}!", name="World")
The more complex prompts are effectively a conversation (a set of messages), where you can have messages from three entities: System, User, AI Assistant. We provide the corresponding types for each of them: SystemMessage, UserMessage, AIMessage.
julia
using PromptingTools: SystemMessage, UserMessage
+
+conversation = [
+ SystemMessage("You're master Yoda from Star Wars trying to help the user become a Jedi."),
+ UserMessage("I have feelings for my {{object}}. What should I do?")]
+msg = aigenerate(conversation; object = "old iPhone")
plaintext
AIMessage("Ah, a dilemma, you have. Emotional attachment can cloud your path to becoming a Jedi. To be attached to material possessions, you must not. The iPhone is but a tool, nothing more. Let go, you must.
+
+Seek detachment, young padawan. Reflect upon the impermanence of all things. Appreciate the memories it gave you, and gratefully part ways. In its absence, find new experiences to grow and become one with the Force. Only then, a true Jedi, you shall become.")
You can also use it to build conversations, eg,
julia
new_conversation = vcat(conversation...,msg, UserMessage("Thank you, master Yoda! Do you have {{object}} to know what it feels like?"))
+aigenerate(new_conversation; object = "old iPhone")
plaintext
> AIMessage("Hmm, possess an old iPhone, I do not. But experience with attachments, I have. Detachment, I learned. True power and freedom, it brings...")
With LLMs, the quality / robustness of your results depends on the quality of your prompts. But writing prompts is hard! That's why we offer a templating system to save you time and effort.
To use a specific template (eg, `` to ask a Julia language):
julia
msg = aigenerate(:JuliaExpertAsk; ask = "How do I add packages?")
The above is equivalent to a more verbose version that explicitly uses the dispatch on AITemplate:
julia
msg = aigenerate(AITemplate(:JuliaExpertAsk); ask = "How do I add packages?")
Find available templates with aitemplates:
julia
tmps = aitemplates("JuliaExpertAsk")
+# Will surface one specific template
+# 1-element Vector{AITemplateMetadata}:
+# PromptingTools.AITemplateMetadata
+# name: Symbol JuliaExpertAsk
+# description: String "For asking questions about Julia language. Placeholders: `ask`"
+# version: String "1"
+# wordcount: Int64 237
+# variables: Array{Symbol}((1,))
+# system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"
+# user_preview: String "# Question\n\n{{ask}}"
+# source: String ""
The above gives you a good idea of what the template is about, what placeholders are available, and how much it would cost to use it (=wordcount).
Search for all Julia-related templates:
julia
tmps = aitemplates("Julia")
+# 2-element Vector{AITemplateMetadata}... -> more to come later!
If you are on VSCode, you can leverage a nice tabular display with vscodedisplay:
julia
using DataFrames
+tmps = aitemplates("Julia") |> DataFrame |> vscodedisplay
I have my selected template, how do I use it? Just use the "name" in aigenerate or aiclassify like you see in the first example!
You can inspect any template by "rendering" it (this is what the LLM will see):
You can leverage asyncmap to run multiple AI-powered tasks concurrently, improving performance for batch operations.
julia
prompts = [aigenerate("Translate 'Hello, World!' to {{language}}"; language) for language in ["Spanish", "French", "Mandarin"]]
+responses = asyncmap(aigenerate, prompts)
Pro tip: You can limit the number of concurrent tasks with the keyword asyncmap(...; ntasks=10).
Certain tasks require more powerful models. All user-facing functions have a keyword argument model that can be used to specify the model to be used. For example, you can use model = "gpt-4-1106-preview" to use the latest GPT-4 Turbo model. However, no one wants to type that!
We offer a set of model aliases (eg, "gpt3", "gpt4", "gpt4t" -> the above GPT-4 Turbo, etc.) that can be used instead.
Each ai... call first looks up the provided model name in the dictionary PromptingTools.MODEL_ALIASES, so you can easily extend with your own aliases!
These aliases also can be used as flags in the @ai_str macro, eg, ai"What is the capital of France?"gpt4t (GPT-4 Turbo has a knowledge cut-off in April 2023, so it's useful for more contemporary questions).
Use the aiembed function to create embeddings via the default OpenAI model that can be used for semantic search, clustering, and more complex AI workflows.
julia
text_to_embed = "The concept of artificial intelligence."
+msg = aiembed(text_to_embed)
+embedding = msg.content # 1536-element Vector{Float64}
If you plan to calculate the cosine distance between embeddings, you can normalize them first:
julia
using LinearAlgebra
+msg = aiembed(["embed me", "and me too"], LinearAlgebra.normalize)
+
+# calculate cosine distance between the two normalized embeddings as a simple dot product
+msg.content' * msg.content[:, 1] # [1.0, 0.787]
You can use the aiclassify function to classify any provided statement as true/false/unknown. This is useful for fact-checking, hallucination or NLI checks, moderation, filtering, sentiment analysis, feature engineering and more.
julia
aiclassify("Is two plus two four?")
+# true
System prompts and higher-quality models can be used for more complex tasks, including knowing when to defer to a human:
julia
aiclassify(:JudgeIsItTrue; it = "Is two plus three a vegetable on Mars?", model = "gpt4t")
+# unknown
In the above example, we used a prompt template :JudgeIsItTrue, which automatically expands into the following system prompt (and a separate user prompt):
"You are an impartial AI judge evaluating whether the provided statement is "true" or "false". Answer "unknown" if you cannot decide."
For more information on templates, see the Templated Prompts section.
aiclassify can be also used for classification into a set of defined categories (maximum 20), so we can use it for routing.
In addition, if you provide the choices as tuples ((label, description)), the model will use the descriptions to decide, but it will return the labels.
Example:
julia
choices = [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")]
+
+input = "spider"
+aiclassify(:InputClassifier; choices, input) # -> returns "A" for any animal or creature
+
+# Try also with:
+input = "daphodil" # -> returns "P" for any plant or tree
+input = "castle" # -> returns "O" for everything else
Under the hood, we use the "logit bias" trick to force only 1 generated token - that means it's very cheap and very fast!
Are you tired of extracting data with regex? You can use LLMs to extract structured data from text!
All you have to do is to define the structure of the data you want to extract and the LLM will do the rest.
Define a return_type with struct. Provide docstrings if needed (improves results and helps with documentation).
Let's start with a hard task - extracting the current weather in a given location:
julia
@enum TemperatureUnits celsius fahrenheit
+"""Extract the current weather in a given location
+
+# Arguments
+- `location`: The city and state, e.g. "San Francisco, CA"
+- `unit`: The unit of temperature to return, either `celsius` or `fahrenheit`
+"""
+struct CurrentWeather
+ location::String
+ unit::Union{Nothing,TemperatureUnits}
+end
+
+# Note that we provide the TYPE itself, not an instance of it!
+msg = aiextract("What's the weather in Salt Lake City in C?"; return_type=CurrentWeather)
+msg.content
+# CurrentWeather("Salt Lake City, UT", celsius)
But you can use it even for more complex tasks, like extracting many entities from a text:
julia
"Person's age, height, and weight."
+struct MyMeasurement
+ age::Int
+ height::Union{Int,Nothing}
+ weight::Union{Nothing,Float64}
+end
+struct ManyMeasurements
+ measurements::Vector{MyMeasurement}
+end
+msg = aiextract("James is 30, weighs 80kg. He's 180cm tall. Then Jack is 19 but really tall - over 190!"; return_type=ManyMeasurements)
+msg.content.measurements
+# 2-element Vector{MyMeasurement}:
+# MyMeasurement(30, 180, 80.0)
+# MyMeasurement(19, 190, nothing)
There is even a wrapper to help you catch errors together with helpful explanations on why parsing failed. See ?PromptingTools.MaybeExtract for more information.
With the aiscan function, you can interact with images as if they were text.
You can simply describe a provided image:
julia
msg = aiscan("Describe the image"; image_path="julia.png", model="gpt4v")
+# [ Info: Tokens: 1141 @ Cost: \$0.0117 in 2.2 seconds
+# AIMessage("The image shows a logo consisting of the word "julia" written in lowercase")
Or you can do an OCR of a screenshot. Let's transcribe some SQL code from a screenshot (no more re-typing!), we use a template :OCRTask:
julia
# Screenshot of some SQL code
+image_url = "https://www.sqlservercentral.com/wp-content/uploads/legacy/8755f69180b7ac7ee76a69ae68ec36872a116ad4/24622.png"
+msg = aiscan(:OCRTask; image_url, model="gpt4v", task="Transcribe the SQL code in the image.", api_kwargs=(; max_tokens=2500))
+
+# [ Info: Tokens: 362 @ Cost: \$0.0045 in 2.5 seconds
+# AIMessage("```sql
+# update Orders <continue>
You can add syntax highlighting of the outputs via Markdown
julia
using Markdown
+msg.content |> Markdown.parse
Experimental Agent Workflows / Output Validation with airetry!
This is an experimental feature, so you have to import it explicitly:
julia
using PromptingTools.Experimental.AgentTools
This module offers "lazy" counterparts to the ai... functions, so you can use them in a more controlled way, eg, aigenerate -> AIGenerate (notice the CamelCase), which has exactly the same arguments except it generates only when run! is called.
For example:
julia
out = AIGenerate("Say hi!"; model="gpt4t")
+run!(out)
How is it useful? We can use the same "inputs" for repeated calls, eg, when we want to validate or regenerate some outputs. We have a function airetry to help us with that.
The signature of airetry is airetry(condition_function, aicall::AICall, feedback_function). It evaluates the condition condition_function on the aicall object (eg, we evaluate f_cond(aicall) -> Bool). If it fails, we call feedback_function on the aicall object to provide feedback for the AI model (eg, f_feedback(aicall) -> String) and repeat the process until it passes or until max_retries value is exceeded.
We can catch API failures (no feedback needed, so none is provided)
julia
# API failure because of a non-existent model
+# RetryConfig allows us to change the "retry" behaviour of any lazy call
+out = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),
+ model = "NOTEXIST")
+run!(out) # fails
+
+# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)
+airetry!(isvalid, out; retry_delay = 2, max_retries = 2)
Or we can validate some outputs (eg, its format, its content, etc.)
We'll play a color guessing game (I'm thinking "yellow"):
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances).
+# Both guesses are scored at each time step, and the best one is chosen for the next step.
+# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!
+out = AIGenerate(
+ "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";
+ verbose = false,
+ config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))
+run!(out)
+
+## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails
+## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.
+airetry!(x -> length(split(last_output(x), r" |\\.")) == 1, out,
+ "You must answer with 1 word only.")
+
+# Note: you could also use the do-syntax, eg,
+airetry!(out, "You must answer with 1 word only.") do aicall
+ length(split(last_output(aicall), r" |\\.")) == 1
+end
You can place multiple airetry! calls in a sequence. They will keep retrying until they run out of maximum AI calls allowed (max_calls) or maximum retries (max_retries).
See the docs for more complex examples and usage tips (?airetry). We leverage Monte Carlo Tree Search (MCTS) to optimize the sequence of retries, so it's a very powerful tool for building robust AI workflows (inspired by Language Agent Tree Search paper and by DSPy Assertions paper).
Ollama.ai is an amazingly simple tool that allows you to run several Large Language Models (LLM) on your computer. It's especially suitable when you're working with some sensitive data that should not be sent anywhere.
Let's assume you have installed Ollama, downloaded a model, and it's running in the background.
We can use it with the aigenerate function:
julia
const PT = PromptingTools
+schema = PT.OllamaManagedSchema() # notice the different schema!
+
+msg = aigenerate(schema, "Say hi!"; model="openhermes2.5-mistral")
+# [ Info: Tokens: 69 in 0.9 seconds
+# AIMessage("Hello! How can I assist you today?")
If you're getting errors, check that Ollama is running - see the Setup Guide for Ollama section below.
Using MistralAI API and other OpenAI-compatible APIs
Mistral models have long been dominating the open-source space. They are now available via their API, so you can use them with PromptingTools.jl!
julia
msg = aigenerate("Say hi!"; model="mistral-tiny")
+# [ Info: Tokens: 114 @ Cost: $0.0 in 0.9 seconds
+# AIMessage("Hello there! I'm here to help answer any questions you might have, or assist you with tasks to the best of my abilities. How can I be of service to you today? If you have a specific question, feel free to ask and I'll do my best to provide accurate and helpful information. If you're looking for general assistance, I can help you find resources or information on a variety of topics. Let me know how I can help.")
It all just works, because we have registered the models in the PromptingTools.MODEL_REGISTRY! There are currently 4 models available: mistral-tiny, mistral-small, mistral-medium, mistral-embed.
Under the hood, we use a dedicated schema MistralOpenAISchema that leverages most of the OpenAI-specific code base, so you can always provide that explicitly as the first argument:
As you can see, we can load your API key either from the ENV or via the Preferences.jl mechanism (see ?PREFERENCES for more information).
But MistralAI are not the only ones! There are many other exciting providers, eg, Perplexity.ai, Fireworks.ai. As long as they are compatible with the OpenAI API (eg, sending messages with role and content keys), you can use them with PromptingTools.jl by using schema = CustomOpenAISchema():
julia
# Set your API key and the necessary base URL for the API
+api_key = "..."
+prompt = "Say hi!"
+msg = aigenerate(PT.CustomOpenAISchema(), prompt; model="my_model", api_key, api_kwargs=(; url="http://localhost:8081"))
As you can see, it also works for any local models that you might have running on your computer!
Note: At the moment, we only support aigenerate and aiembed functions for MistralAI and other OpenAI-compatible APIs. We plan to extend the support in the future.
+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/examples/working_with_aitemplates.html b/previews/PR116/examples/working_with_aitemplates.html
new file mode 100644
index 000000000..3e6666c32
--- /dev/null
+++ b/previews/PR116/examples/working_with_aitemplates.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+ Using AITemplates | PromptingTools.jl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This file contains examples of how to work with AITemplate(s).
First, let's import the package and define a helper link for calling un-exported functions:
julia
using PromptingTools
+const PT = PromptingTools
PromptingTools
LLM responses are only as good as the prompts you give them. However, great prompts take long time to write – AITemplate are a way to re-use great prompts!
AITemplates are just a collection of templated prompts (ie, set of "messages" that have placeholders like )
They are saved as JSON files in the templates directory. They are automatically loaded on package import, but you can always force a re-load with PT.load_templates!()
julia
PT.load_templates!();
You can (create them) and use them for any ai* function instead of a prompt: Let's use a template called :JuliaExpertAsk alternatively, you can use AITemplate(:JuliaExpertAsk) for cleaner dispatch
julia
msg = aigenerate(:JuliaExpertAsk; ask = "How do I add packages?")
AIMessage("To add packages in Julia, you can use the `Pkg` module. Here are the steps:
+
+1. Start Julia by running the Julia REPL (Read-Eval-Print Loop).
+2. Press the `]` key to enter the Pkg mode.
+3. To add a package, use the `add` command followed by the package name.
+4. Press the backspace key to exit Pkg mode and return to the Julia REPL.
+
+For example, to add the `Example` package, you would enter:
+
+```julia
+]add Example
+```
+
+After the package is added, you can start using it in your Julia code by using the `using` keyword. For the `Example` package, you would add the following line to your code:
+
+```julia
+using Example
+```
+
+Note: The first time you add a package, Julia may take some time to download and compile the package and its dependencies.")
You can see that it had a placeholder for the actual question (ask) that we provided as a keyword argument. We did not have to write any system prompt for personas, tone, etc. – it was all provided by the template!
How to know which templates are available? You can search for them with aitemplates(): You can search by Symbol (only for partial name match), String (partial match on name or description), or Regex (more fields)
julia
tmps = aitemplates("JuliaExpertAsk")
1-element Vector{AITemplateMetadata}:
+PromptingTools.AITemplateMetadata
+ name: Symbol JuliaExpertAsk
+ description: String "For asking questions about Julia language. Placeholders: `ask`"
+ version: String "1"
+ wordcount: Int64 237
+ variables: Array{Symbol}((1,))
+ system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"
+ user_preview: String "# Question\n\n{{ask}}"
+ source: String ""
You can see that it outputs a list of available templates that match the search - there is just one in this case.
Moreover, it shows not just the description, but also a preview of the actual prompts, placeholders available, and the length (to gauge how much it would cost).
If you use VSCode, you can display them in a nice scrollable table with vscodedisplay:
plaintext
using DataFrames
+DataFrame(tmp) |> vscodedisplay
You can also just render the template to see the underlying mesages:
julia
msgs = PT.render(AITemplate(:JuliaExpertAsk))
2-element Vector{PromptingTools.AbstractChatMessage}:
+ PromptingTools.SystemMessage("You are a world-class Julia language programmer with the knowledge of the latest syntax. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")
+ PromptingTools.UserMessage{String}("# Question\n\n{{ask}}", [:ask], :usermessage)
Now, you know exactly what's in the template!
If you want to modify it, simply change it and save it as a new file with save_template (see the docs ?save_template for more details).
Let's adjust the previous template to be more specific to a data analysis question:
julia
tpl = [PT.SystemMessage("You are a world-class Julia language programmer with the knowledge of the latest syntax. You're also a senior Data Scientist and proficient in data analysis in Julia. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")
+ PT.UserMessage("# Question\n\n{{ask}}")]
2-element Vector{PromptingTools.AbstractChatMessage}:
+ PromptingTools.SystemMessage("You are a world-class Julia language programmer with the knowledge of the latest syntax. You're also a senior Data Scientist and proficient in data analysis in Julia. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")
+ PromptingTools.UserMessage{String}("# Question\n\n{{ask}}", [:ask], :usermessage)
Templates are saved in the templates directory of the package. Name of the file will become the template name (eg, call :JuliaDataExpertAsk)
julia
filename = joinpath(pkgdir(PromptingTools),
+ "templates",
+ "persona-task",
+ "JuliaDataExpertAsk_123.json")
+PT.save_template(filename,
+ tpl;
+ description = "For asking data analysis questions in Julia language. Placeholders: `ask`")
+rm(filename) # cleanup if we don't like it
When you create a new template, remember to re-load the templates with load_templates!() so that it's available for use.
julia
PT.load_templates!();
!!! If you have some good templates (or suggestions for the existing ones), please consider sharing them with the community by opening a PR to the templates directory!
Mistral models have long been dominating the open-source space. They are now available via their API, so you can use them with PromptingTools.jl!
julia
msg = aigenerate("Say hi!"; model="mistral-tiny")
+# [ Info: Tokens: 114 @ Cost: $0.0 in 0.9 seconds
+# AIMessage("Hello there! I'm here to help answer any questions you might have, or assist you with tasks to the best of my abilities. How can I be of service to you today? If you have a specific question, feel free to ask and I'll do my best to provide accurate and helpful information. If you're looking for general assistance, I can help you find resources or information on a variety of topics. Let me know how I can help.")
It all just works, because we have registered the models in the PromptingTools.MODEL_REGISTRY! There are currently 4 models available: mistral-tiny, mistral-small, mistral-medium, mistral-embed.
Under the hood, we use a dedicated schema MistralOpenAISchema that leverages most of the OpenAI-specific code base, so you can always provide that explicitly as the first argument:
MistralAI are not the only ones who mimic the OpenAI API! There are many other exciting providers, eg, Perplexity.ai, Fireworks.ai.
As long as they are compatible with the OpenAI API (eg, sending messages with role and content keys), you can use them with PromptingTools.jl by using schema = CustomOpenAISchema():
julia
# Set your API key and the necessary base URL for the API
+api_key = "..."
+provider_url = "..." # provider API URL
+prompt = "Say hi!"
+msg = aigenerate(PT.CustomOpenAISchema(), prompt; model="<some-model>", api_key, api_kwargs=(; url=provider_url))
If you register the model names with `PT.register_model!`, you won't have to keep providing the `schema` manually.
Note: At the moment, we only support aigenerate and aiembed functions.
It is a bit more technically demanding because you need to "compile" llama.cpp first, but it will always have the latest models and it is quite fast (eg, faster than Ollama, which uses llama.cpp under the hood but has some extra overhead).
Start your server in a command line (-m refers to the model file, -c is the context length, -ngl is the number of layers to offload to GPU):
msg = aigenerate(PT.CustomOpenAISchema(), "Count to 5 and say hi!"; api_kwargs=(; url="http://localhost:8080/v1"))
If you register the model names with `PT.register_model!`, you won't have to keep providing the `schema` manually. It can be any `model` name, because the model is actually selected when you start the server in the terminal.
You can also use the Databricks Foundation Models API with PromptingTools.jl. It requires you to set ENV variables DATABRICKS_API_KEY (often referred to as "DATABRICKS TOKEN") and DATABRICKS_HOST.
The long way to use it is:
julia
msg = aigenerate(PT.DatabricksOpenAISchema(),
+ "Say hi to the llama!";
+ model = "databricks-llama-2-70b-chat",
+ api_key = ENV["DATABRICKS_API_KEY"], api_kwargs = (; url=ENV["DATABRICKS_HOST"]))
But you can also register the models you're hosting and use it as usual:
julia
# Quick registration of a model
+PT.register_model!(;
+ name = "databricks-llama-2-70b-chat",
+ schema = PT.DatabricksOpenAISchema())
+PT.MODEL_ALIASES["dllama"] = "databricks-llama-2-70b-chat" # set alias to make your life easier
+
+# Simply call:
+msg = aigenerate("Say hi to the llama!"; model = "dllama")
+# Or even shorter
+ai"Say hi to the llama!"dllama
You can also use the Together.ai API with PromptingTools.jl. It requires you to set ENV variable TOGETHER_API_KEY.
The corresponding schema is TogetherOpenAISchema, but we have registered one model for you, so you can use it as usual. Alias "tmixtral" (T for Together.ai and mixtral for the model name) is already set for you.
julia
msg = aigenerate("Say hi"; model="tmixtral")
+## [ Info: Tokens: 87 @ Cost: \$0.0001 in 5.1 seconds
+## AIMessage("Hello! I'm here to help you. Is there something specific you'd like to know or discuss? I can provide information on a wide range of topics, assist with tasks, and even engage in a friendly conversation. Let me know how I can best assist you today.")
You can also use the Fireworks.ai API with PromptingTools.jl. It requires you to set ENV variable FIREWORKS_API_KEY.
The corresponding schema is FireworksOpenAISchema, but we have registered one model for you, so you can use it as usual. Alias "fmixtral" (F for Fireworks.ai and mixtral for the model name) is already set for you.
julia
msg = aigenerate("Say hi"; model="fmixtral")
+## [ Info: Tokens: 78 @ Cost: \$0.0001 in 0.9 seconds
+## AIMessage("Hello! I'm glad you're here. I'm here to help answer any questions you have to the best of my ability. Is there something specific you'd like to know or discuss? I can assist with a wide range of topics, so feel free to ask me anything!")
In addition, at the time of writing (23rd Feb 2024), Fireworks is providing access to their new function calling model (fine-tuned Mixtral) for free.
Try it with aiextract for structured extraction (model is aliased as firefunction):
julia
"""
+Extract the food from the sentence. Extract any provided adjectives for the food as well.
+
+Example: "I am eating a crunchy bread." -> Food("bread", ["crunchy"])
+"""
+struct Food
+ name::String
+ adjectives::Union{Nothing,Vector{String}}
+end
+prompt = "I just ate a delicious and juicy apple."
+msg = aiextract(prompt; return_type=Food, model="firefunction")
+msg.content
+# Output: Food("apple", ["delicious", "juicy"])
This file contains examples of how to work with Google AI Studio. It is known for its Gemini models.
Get an API key from here. If you see a documentation page ("Available languages and regions for Google AI Studio and Gemini API"), it means that it's not yet available in your region.
Save the API key in your environment as GOOGLE_API_KEY.
We'll need GoogleGenAI package:
julia
using Pkg; Pkg.add("GoogleGenAI")
You can now use the Gemini-1.0-Pro model like any other model in PromptingTools. We only support aigenerate at the moment.
AIMessage("Hi there! As a helpful AI assistant, I'm here to help you with any questions or tasks you may have. Feel free to ask me anything, and I'll do my best to assist you.")
You could achieve the same with a string macro (notice the "gemini" at the end to specify which model to use):
You can provide multi-turn conversations like with any other model:
julia
conversation = [
+ PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),
+ PT.UserMessage("I have feelings for my iPhone. What should I do?")]
+msg = aigenerate(conversation; model="gemini")
AIMessage("Young Padawan, you have stumbled into a dangerous path. Attachment leads to suffering, and love can turn to darkness.
+
+Release your feelings for this inanimate object.
+
+The Force flows through all living things, not machines. Seek balance in the Force, and your heart will find true connection.
+
+Remember, the path of the Jedi is to serve others, not to be attached to possessions.")
Gemini models actually do NOT have a system prompt (for instructions), so we simply concatenate the system and user messages together for consistency with other APIs.
The reported tokens in the AIMessage are actually characters (that's how Google AI Studio intends to charge for them) and are a conservative estimate that we produce. It does not matter, because at the time of writing (Feb-24), the usage is free-of-charge.
+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/examples/working_with_ollama.html b/previews/PR116/examples/working_with_ollama.html
new file mode 100644
index 000000000..b72f06bce
--- /dev/null
+++ b/previews/PR116/examples/working_with_ollama.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Local models with Ollama.ai | PromptingTools.jl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This file contains examples of how to work with Ollama.ai models. It assumes that you've already installated and launched the Ollama server. For more details or troubleshooting advice, see the Frequently Asked Questions section.
First, let's import the package and define a helper link for calling un-exported functions:
julia
using PromptingTools
+const PT = PromptingTools
PromptingTools
There were are several models from https://ollama.ai/library that we have added to our PT.MODEL_REGISTRY, which means you don't need to worry about schema changes: Eg, "llama2" or "openhermes2.5-mistral" (see PT.list_registry() and PT.list_aliases())
Note: You must download these models prior to using them with ollama pull <model_name> in your Terminal.
If you use Apple Mac M1-3, make sure to provide `api_kwargs=(; options=(; num_gpu=99))` to make sure the whole model is offloaded on your GPU. Current default is 1, which makes some models unusable. Example for running Mixtral: `msg = aigenerate(PT.OllamaSchema(), "Count from 1 to 5 and then say hi."; model="dolphin-mixtral:8x7b-v2.5-q4_K_M", api_kwargs=(; options=(; num_gpu=99)))`
TL;DR if you use models in PT.MODEL_REGISTRY, you don't need to add schema as the first argument:
julia
msg = aigenerate("Say hi!"; model = "llama2")
AIMessage("Hello there! *adjusts glasses* It's nice to meet you! Is there anything I can help you with or would you like me to chat with you for a bit?")
conversation = [
+ PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),
+ PT.UserMessage("I have feelings for my iPhone. What should I do?")]
+msg = aigenerate(conversation; model)
AIMessage("(Deep sigh) A problem, you have. Feelings for an iPhone, hmm? (adjusts spectacles)
+
+Much confusion, this causes. (scratches head) A being, you are. Attached to a device, you have become. (chuckles) Interesting, this is.
+
+First, let go, you must. (winks) Hard, it is, but necessary, yes. Distract yourself, find something else, try. (pauses)
+
+Or, perhaps, a balance, you seek? (nods) Both, enjoy and let go, the middle path, there is. (smirks) Finding joy in technology, without losing yourself, the trick, it is. (chuckles)
+
+But fear not, young one! (grins) Help, I am here. Guide you, I will. The ways of the Yedi, teach you, I will. (winks) Patience and understanding, you must have. (nods)
+
+Now, go forth! (gestures) Explore, discover, find your balance. (smiles) The Force be with you, it does! (grins)")
If you're using some model that is not in the registry, you can either add it:
julia
PT.register_model!(;
+ name = "llama123",
+ schema = PT.OllamaSchema(),
+ description = "Some model")
+PT.MODEL_ALIASES["l123"] = "llama123" # set an alias you like for it
"llama123"
OR define the schema explicitly (to avoid dispatch on global PT.PROMPT_SCHEMA):
julia
schema = PT.OllamaSchema()
+aigenerate(schema, "Say hi!"; model = "llama2")
AIMessage("Hello there! *smiling face* It's nice to meet you! I'm here to help you with any questions or tasks you may have, so feel free to ask me anything. Is there something specific you need assistance with today? 😊")
Note: If you only use Ollama, you can change the default schema to PT.OllamaSchema() via PT.set_preferences!("PROMPT_SCHEMA" => "OllamaSchema", "MODEL_CHAT"=>"llama2")
Restart your session and run aigenerate("Say hi!") to test it.
! Note that in version 0.6, we've introduced OllamaSchema, which superseded OllamaManagedSchema and allows multi-turn conversations and conversations with images (eg, with Llava and Bakllava models). OllamaManagedSchema has been kept for compatibility and as an example of a schema where one provides a prompt as a string (not dictionaries like OpenAI API).
AIGenerate - Lazy counterpart of aigenerate(). All ai* functions have a corresponding AI*::AICall struct that allows for deferred execution (triggered by run! method).
last_output, last_message - Simple utilities to access the last output and message of the AI calls like AIGenerate.
airetry! - A utility to automatically retry the AI call with the same inputs if the AI model fails to generate a valid output. It allows retrying many times and providing feedback to the AI model about the failure to increase its robustness. AIGenerate and other AI calls have a field config::RetryConfig where you can globally adjust the retrying behavior.
print_samples - airetry! implements a Monte Carlo Tree Search under the hood when trying to find the best way to fix the AI model's failure. print_samples is a utility to print the "samples" generated by the MCTS to better understand the attempts made by the AI model to fix the failure.
AICode extensions like aicodefixer_feedback and error_feedback - AICode is a wrapper that extracts any Julia code provided in the AIMessage (response from the AI model) and executes it (including catch any errors). aicodefixer_feedback and error_feedback are utilities that automatically review an outcome of AICode evaluation and generate the corresponding feedback for the AI model.
The main contribution of this module is providing the "lazy" counterparts to the ai... functions, which allow us to build a workflow, which can be re-executed many times with the same inputs.
For example, AIGenerate() will create a lazy instance of aigenerate, which is an instance of AICall with aigenerate as its ai-calling function. It uses exactly the same arguments and keyword arguments as aigenerate (see ?aigenerate for details). The notion of "lazy" refers to the fact that it does NOT generate any output when instantiated (only when run! is called).
Or said differently, the AICall struct and all its flavors (AIGenerate, ...) are designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This allows us to remember user inputs and trigger the LLM call repeatedly if needed, which enables automatic fixing (see ?airetry!).
How is it useful? We can use the same "inputs" for repeated calls, eg, when we want to validate or regenerate some outputs. We have a function airetry! to help us with that.
The signature of airetry is airetry(condition_function, aicall::AICall, feedback_function).
It evaluates the condition condition_function on the aicall object (eg, we evaluate f_cond(aicall) -> Bool). If it fails, we call feedback_function on the aicall object to provide feedback for the AI model (eg, f_feedback(aicall) -> String) and repeat the process until it passes or until max_retries value is exceeded.
We can catch API failures (no feedback needed, so none is provided)
julia
# API failure because of a non-existent model
+# RetryConfig allows us to change the "retry" behaviour of any lazy call
+output = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),
+ model = "NOTEXIST")
+run!(output) # fails
+
+# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)
+airetry!(isvalid, output; retry_delay = 2, max_retries = 2)
Or we can use it for output validation (eg, its format, its content, etc.) and feedback generation.
Let's play a color guessing game (I'm thinking "yellow"). We'll implement two formatting checks with airetry!:
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances).
+# Both guesses are scored at each time step, and the best one is chosen for the next step.
+# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!
+out = AIGenerate(
+ "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";
+ verbose = false,
+ config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))
+run!(out)
+
+## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails
+## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.
+airetry!(x -> length(split(last_output(x), r" |\\.")) == 1, out,
+ "You must answer with 1 word only.")
+
+# Note: you could also use the do-syntax, eg,
+airetry!(out, "You must answer with 1 word only.") do aicall
+ length(split(last_output(aicall), r" |\\.")) == 1
+end
You can even add the guessing itself as an airetry! condition of last_output(out) == "yellow" and provide feedback if the guess is wrong.
A lazy call wrapper for AI functions in the PromptingTools module, such as aigenerate.
The AICall struct is designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This approach allows for more flexible and efficient handling of AI function calls, especially in interactive environments.
Seel also: run!, AICodeFixer
Fields
func::F: The AI function to be called lazily. This should be a function like aigenerate or other ai* functions.
schema::Union{Nothing, PT.AbstractPromptSchema}: Optional schema to structure the prompt for the AI function.
conversation::Vector{PT.AbstractMessage}: A vector of messages that forms the conversation context for the AI call.
kwargs::NamedTuple: Keyword arguments to be passed to the AI function.
success::Union{Nothing, Bool}: Indicates whether the last call was successful (true) or not (false). Nothing if the call hasn't been made yet.
error::Union{Nothing, Exception}: Stores any exception that occurred during the last call. Nothing if no error occurred or if the call hasn't been made yet.
Example
Initiate an AICall like any ai* function, eg, AIGenerate:
julia
aicall = AICall(aigenerate)
+
+# With arguments and kwargs like ai* functions
+# from `aigenerate(schema, conversation; model="abc", api_kwargs=(; temperature=0.1))`
+# to
+aicall = AICall(aigenerate, schema, conversation; model="abc", api_kwargs=(; temperature=0.1)
+
+# Or with a template
+aicall = AIGenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1))
Trigger the AICall with run! (it returns the update AICall struct back):
julia
aicall |> run!
+````
+
+You can also use `AICall` as a functor to trigger the AI call with a `UserMessage` or simply the text to send:
julia aicall(UserMessage("Hello, world!")) # Triggers the lazy call result = run!(aicall) # Explicitly runs the AI call ``` This can be used to "reply" to previous message / continue the stored conversation
Notes
The AICall struct is a key component in building flexible and efficient Agentic pipelines
The lazy evaluation model allows for setting up the call parameters in advance and deferring the actual execution until it is explicitly triggered.
This struct is particularly useful in scenarios where the timing of AI function execution needs to be deferred or where multiple potential calls need to be prepared and selectively executed.
Helpful accessor for the last generated output (msg.content) in conversation. Returns the last output in the conversation (eg, the string/data in the last message).
Evaluates the condition f_cond on the aicall object. If the condition is not met, it will return the best sample to retry from and provide feedback (string or function) to aicall. That's why it's mutating. It will retry maximum max_retries times, with throw=true, an error will be thrown if the condition is not met after max_retries retries.
Function signatures
f_cond(aicall::AICallBlock) -> Bool, ie, it must accept the aicall object and return a boolean value.
feedback can be a string or feedback(aicall::AICallBlock) -> String, ie, it must accept the aicall object and return a string.
You can leverage the last_message, last_output, and AICode functions to access the last message, last output and execute code blocks in the conversation, respectively. See examples below.
Good Use Cases
Retry with API failures/drops (add retry_delay=2 to wait 2s between retries)
Check the output format / type / length / etc
Check the output with aiclassify call (LLM Judge) to catch unsafe/NSFW/out-of-scope content
Provide hints to the model to guide it to the correct answer
Gotchas
If controlling keyword arguments are set to nothing, they will fall back to the default values in aicall.config. You can override them by passing the keyword arguments explicitly.
If there multiple airetry! checks, they are evaluted sequentially. As long as throw==false, they will be all evaluated even if they failed previous checks.
Only samples which passed previous evaluations are evaluated (sample.success is true). If there are no successful samples, the function will evaluate only the active sample (aicall.active_sample_id) and nothing else.
Feedback from all "ancestor" evaluations is added upon retry, not feedback from the "sibblings" or other branches. To have only ONE long BRANCH (no sibblings), make sure to keep RetryConfig(; n_samples=1). That way the model will always see ALL previous feedback.
We implement a version of Monte Carlo Tree Search (MCTS) to always pick the most promising sample to restart from (you can tweak the options in RetryConfig to change the behaviour).
For large number of parallel branches (ie, "shallow and wide trees"), you might benefit from switching scoring to scoring=ThompsonSampling() (similar to how Bandit algorithms work).
Open-source/local models can struggle with too long conversation, you might want to experiment with in-place feedback (set RetryConfig(; feedback_inplace=true)).
Arguments
f_cond::Function: A function that accepts the aicall object and returns a boolean value. Retry will be attempted if the condition is not met (f_cond -> false).
aicall::AICallBlock: The aicall object to evaluate the condition on.
feedback::Union{AbstractString, Function}: Feedback to provide if the condition is not met. If a function is provided, it must accept the aicall object as the only argument and return a string.
verbose::Integer=1: A verbosity level for logging the retry attempts and warnings. A higher value indicates more detailed logging.
throw::Bool=false: If true, it will throw an error if the function f_cond does not return true after max_retries retries.
evaluate_all::Bool=false: If true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample.
feedback_expensive::Bool=false: If false, it will provide feedback to all samples that fail the condition. If feedback function is expensive to call (eg, another ai* function), set this to true and feedback will be provided only to the sample we will retry from.
max_retries::Union{Nothing, Int}=nothing: Maximum number of retries. If not provided, it will fall back to the max_retries in aicall.config.
retry_delay::Union{Nothing, Int}=nothing: Delay between retries in seconds. If not provided, it will fall back to the retry_delay in aicall.config.
Returns
The aicall object with the updated conversation, and samples (saves the evaluations and their scores/feedback).
Example
You can use airetry! to catch API errors in run! and auto-retry the call. RetryConfig is how you influence all the subsequent retry behaviours - see ?RetryConfig for more details.
julia
# API failure because of a non-existent model
+out = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),
+ model = "NOTEXIST")
+run!(out) # fails
+
+# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)
+airetry!(isvalid, out; retry_delay = 2, max_retries = 2)
If you provide arguments to the aicall, we try to honor them as much as possible in the following calls, eg, set low verbosity
julia
out = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),
+model = "NOTEXIST", verbose=false)
+run!(out)
+# No info message, you just see `success = false` in the properties of the AICall
Let's show a toy example to demonstrate the runtime checks / guardrails for the model output. We'll play a color guessing game (I'm thinking "yellow"):
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances).
+# Both guesses are scored at each time step, and the best one is chosen for the next step.
+# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!
+out = AIGenerate(
+ "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";
+ verbose = false,
+ config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))
+run!(out)
+
+
+## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails
+## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.
+airetry!(x -> length(split(last_output(x), r" |\.")) == 1, out,
+ "You must answer with 1 word only.")
+
+
+## Let's ensure that the output is in lowercase - simple and short
+airetry!(x -> all(islowercase, last_output(x)), out, "You must answer in lowercase.")
+# [ Info: Condition not met. Retrying...
+
+
+## Let's add final hint - it took us 2 retries
+airetry!(x -> startswith(last_output(x), "y"), out, "It starts with "y"")
+# [ Info: Condition not met. Retrying...
+# [ Info: Condition not met. Retrying...
+
+
+## We end up with the correct answer
+last_output(out)
+# Output: "yellow"
Let's explore how we got here. We save the various attempts in a "tree" (SampleNode object) You can access it in out.samples, which is the ROOT of the tree (top level). Currently "active" sample ID is out.active_sample_id -> that's the same as conversation field in your AICall.
julia
# Root node:
+out.samples
+# Output: SampleNode(id: 46839, stats: 6/12, length: 2)
+
+# Active sample (our correct answer):
+out.active_sample_id
+# Output: 50086
+
+# Let's obtain the active sample node with this ID - use getindex notation or function find_node
+out.samples[out.active_sample_id]
+# Output: SampleNode(id: 50086, stats: 1/1, length: 7)
+
+# The SampleNode has two key fields: data and feedback. Data is where the conversation is stored:
+active_sample = out.samples[out.active_sample_id]
+active_sample.data == out.conversation # Output: true -> This is the winning guess!
We also get a clear view of the tree structure of all samples with print_samples:
We can also iterate through all samples and extract whatever information we want with PostOrderDFS or PreOrderDFS (exported from AbstractTrees.jl)
julia
for sample in PostOrderDFS(out.samples)
+ # Data is the universal field for samples, we put `conversation` in there
+ # Last item in data is the last message in coversation
+ msg = sample.data[end]
+ if msg isa PT.AIMessage # skip feedback
+ # get only the message content, ie, the guess
+ println("ID: $(sample.id), Answer: $(msg.content)")
+ end
+end
+
+# ID: 20493, Answer: yellow
+# ID: 50086, Answer: yellow
+# ID: 2733, Answer: red
+# ID: 30088, Answer: blue
+# ID: 44816, Answer: blue
Note: airetry! will attempt to fix the model max_retries times. If you set throw=true, it will throw an ErrorException if the condition is not met after max_retries retries.
Let's define a mini program to guess the number and use airetry! to guide the model to the correct answer:
julia
"""
+ llm_guesser()
+
+Mini program to guess the number provided by the user (betwee 1-100).
+"""
+function llm_guesser(user_number::Int)
+ @assert 1 <= user_number <= 100
+ prompt = """
+I'm thinking a number between 1-100. Guess which one it is.
+You must respond only with digits and nothing else.
+Your guess:"""
+ ## 2 samples at a time, max 5 fixing rounds
+ out = AIGenerate(prompt; config = RetryConfig(; n_samples = 2, max_retries = 5),
+ api_kwargs = (; n = 2)) |> run!
+ ## Check the proper output format - must parse to Int, use do-syntax
+ ## We can provide feedback via a function!
+ function feedback_f(aicall)
+ "Output: $(last_output(aicall))
+Feedback: You must respond only with digits!!"
+ end
+ airetry!(out, feedback_f) do aicall
+ !isnothing(tryparse(Int, last_output(aicall)))
+ end
+ ## Give a hint on bounds
+ lower_bound = (user_number ÷ 10) * 10
+ upper_bound = lower_bound + 10
+ airetry!(
+ out, "The number is between or equal to $lower_bound to $upper_bound.") do aicall
+ guess = tryparse(Int, last_output(aicall))
+ lower_bound <= guess <= upper_bound
+ end
+ ## You can make at most 3x guess now -- if there is max_retries in `config.max_retries` left
+ max_retries = out.config.retries + 3
+ function feedback_f2(aicall)
+ guess = tryparse(Int, last_output(aicall))
+ "Your guess of $(guess) is wrong, it's $(abs(guess-user_number)) numbers away."
+ end
+ airetry!(out, feedback_f2; max_retries) do aicall
+ tryparse(Int, last_output(aicall)) == user_number
+ end
+
+ ## Evaluate the best guess
+ @info "Results: Guess: $(last_output(out)) vs User: $user_number (Number of calls made: $(out.config.calls))"
+ return out
+end
+
+# Let's play the game
+out = llm_guesser(33)
+[ Info: Condition not met. Retrying...
+[ Info: Condition not met. Retrying...
+[ Info: Condition not met. Retrying...
+[ Info: Condition not met. Retrying...
+[ Info: Results: Guess: 33 vs User: 33 (Number of calls made: 10)
Yay! We got it 😃
Now, we could explore different samples (eg, print_samples(out.samples)) or see what the model guessed at each step:
Note that if there are multiple "branches" the model will see only the feedback of its own and its ancestors not the other "branches". If you wanted to provide ALL feedback, set RetryConfig(; n_samples=1) to remove any "branching". It fixing will be done sequentially in one conversation and the model will see all feedback (less powerful if the model falls into a bad state). Alternatively, you can tweak the feedback function.
A mutable structure representing a code block (received from the AI model) with automatic parsing, execution, and output/error capturing capabilities.
Upon instantiation with a string, the AICode object automatically runs a code parser and executor (via PromptingTools.eval!()), capturing any standard output (stdout) or errors. This structure is useful for programmatically handling and evaluating Julia code snippets.
See also: PromptingTools.extract_code_blocks, PromptingTools.eval!
Workflow
Until cb::AICode has been evaluated, cb.success is set to nothing (and so are all other fields).
The text in cb.code is parsed (saved to cb.expression).
The parsed expression is evaluated.
Outputs of the evaluated expression are captured in cb.output.
Any stdout outputs (e.g., from println) are captured in cb.stdout.
If an error occurs during evaluation, it is saved in cb.error.
After successful evaluation without errors, cb.success is set to true. Otherwise, it is set to false and you can inspect the cb.error to understand why.
Properties
code::AbstractString: The raw string of the code to be parsed and executed.
expression: The parsed Julia expression (set after parsing code).
stdout: Captured standard output from the execution of the code.
output: The result of evaluating the code block.
success::Union{Nothing, Bool}: Indicates whether the code block executed successfully (true), unsuccessfully (false), or has yet to be evaluated (nothing).
error::Union{Nothing, Exception}: Any exception raised during the execution of the code block.
Keyword Arguments
auto_eval::Bool: If set to true, the code block is automatically parsed and evaluated upon instantiation. Defaults to true.
safe_eval::Bool: If set to true, the code block checks for package operations (e.g., installing new packages) and missing imports, and then evaluates the code inside a bespoke scratch module. This is to ensure that the evaluation does not alter any user-defined variables or the global state. Defaults to false.
skip_unsafe::Bool: If set to true, we skip any lines in the code block that are deemed unsafe (eg, Pkg operations). Defaults to false.
skip_invalid::Bool: If set to true, we skip code blocks that do not even parse. Defaults to false.
verbose::Bool: If set to true, we print out any lines that are skipped due to being unsafe. Defaults to false.
capture_stdout::Bool: If set to true, we capture any stdout outputs (eg, test failures) in cb.stdout. Defaults to true.
prefix::AbstractString: A string to be prepended to the code block before parsing and evaluation. Useful to add some additional code definition or necessary imports. Defaults to an empty string.
suffix::AbstractString: A string to be appended to the code block before parsing and evaluation. Useful to check that tests pass or that an example executes. Defaults to an empty string.
remove_tests::Bool: If set to true, we remove any @test or @testset macros from the code block before parsing and evaluation. Defaults to false.
execution_timeout::Int: The maximum time (in seconds) allowed for the code block to execute. Defaults to 60 seconds.
Methods
Base.isvalid(cb::AICode): Check if the code block has executed successfully. Returns true if cb.success == true.
Examples
julia
code = AICode("println("Hello, World!")") # Auto-parses and evaluates the code, capturing output and errors.
+isvalid(code) # Output: true
+code.stdout # Output: "Hello, World!
+"
We try to evaluate "safely" by default (eg, inside a custom module, to avoid changing user variables). You can avoid that with save_eval=false:
You can also call AICode directly on an AIMessage, which will extract the Julia code blocks, concatenate them and evaluate them:
julia
msg = aigenerate("In Julia, how do you create a vector of 10 random numbers?")
+code = AICode(msg)
+# Output: AICode(Success: True, Parsed: True, Evaluated: True, Error Caught: N/A, StdOut: True, Code: 2 Lines)
+
+# show the code
+code.code |> println
+# Output:
+# numbers = rand(10)
+# numbers = rand(1:100, 10)
+
+# or copy it to the clipboard
+code.code |> clipboard
+
+# or execute it in the current module (=Main)
+eval(code.expression)
Generate feedback for an AI code fixing session based on the conversation history. Function is designed to be extensible for different types of feedback and code evaluation outcomes.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
Individual feedback functions are dispatched on different subtypes of AbstractCodeOutcome and can be extended/overwritten to provide more detailed feedback.
See also: AIGenerate, AICodeFixer
Arguments
conversation::AbstractVector{<:PT.AbstractMessage}: A vector of messages representing the conversation history, where the last message is expected to contain the code to be analyzed.
max_length::Int=512: An optional argument that specifies the maximum length of the feedback message.
Returns
NamedTuple: A feedback message as a kwarg in NamedTuple based on the analysis of the code provided in the conversation.
Example
julia
new_kwargs = aicodefixer_feedback(conversation)
Notes
This function is part of the AI code fixing system, intended to interact with code in AIMessage and provide feedback on improving it.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
It dispatches for the code feedback based on the subtypes of AbstractCodeOutcome below:
CodeEmpty: No code found in the message.
CodeFailedParse: Code parsing error.
CodeFailedEval: Runtime evaluation error.
CodeFailedTimeout: Code execution timed out.
CodeSuccess: Successful code execution.
You can override the individual methods to customize the feedback.
Currently, there is only one function in this module create_websearch that leverages Tavily.com search and answer engine to provide additional context.
You need to sign up for an API key at Tavily.com and set it as an environment variable TAVILY_API_KEY to use this function.
api_key::AbstractString: The API key to use for the search. Get an API key from Tavily.
search_depth::AbstractString: The depth of the search. Can be either "basic" or "advanced". Default is "basic". Advanced search calls equal to 2 requests.
include_answer::Bool: Whether to include the answer in the search results. Default is false.
include_raw_content::Bool: Whether to include the raw content in the search results. Default is false.
max_results::Integer: The maximum number of results to return. Default is 5.
include_images::Bool: Whether to include images in the search results. Default is false.
include_domains::AbstractVector{<:AbstractString}: A list of domains to include in the search results. Default is an empty list.
exclude_domains::AbstractVector{<:AbstractString}: A list of domains to exclude from the search results. Default is an empty list.
Example
julia
r = create_websearch("Who is King Charles?")
Even better, you can get not just the results but also the answer:
julia
r = create_websearch("Who is King Charles?"; include_answer = true)
RAGTools is an experimental module that provides a set of utilities for building Retrieval-Augmented Generation (RAG) applications, ie, applications that generate answers by combining knowledge of the underlying AI model with the information from the user's knowledge base.
It is designed to be powerful and flexible, allowing you to build RAG applications with minimal effort. Extend any step of the pipeline with your own custom code (see the RAG Interface section), or use the provided defaults to get started quickly.
Once the API stabilizes (near term), we hope to carve it out into a separate package.
Import the module as follows:
julia
# required dependencies to load the necessary extensions!!!
+using LinearAlgebra, SparseArrays
+using PromptingTools.Experimental.RAGTools
+# to access unexported functionality
+const RT = PromptingTools.Experimental.RAGTools
build_index to build a RAG index from a list of documents (type ChunkIndex)
airag to generate answers using the RAG model on top of the index built above
retrieve to retrieve relevant chunks from the index for a given question
generate! to generate an answer from the retrieved chunks
annotate_support to highlight which parts of the RAG answer are supported by the documents in the index vs which are generated by the model, it is applied automatically if you use pretty printing with pprint (eg, pprint(result))
build_qa_evals to build a set of question-answer pairs for evaluation of the RAG model from your corpus
The hope is to provide a modular and easily extensible set of tools for building RAG applications in Julia. Feel free to open an issue or ask in the #generative-ai channel in the JuliaLang Slack if you have a specific need.
Let's build an index, we need to provide a starter list of documents:
julia
sentences = [
+ "Find the most comprehensive guide on Julia programming language for beginners published in 2023.",
+ "Search for the latest advancements in quantum computing using Julia language.",
+ "How to implement machine learning algorithms in Julia with examples.",
+ "Looking for performance comparison between Julia, Python, and R for data analysis.",
+ "Find Julia language tutorials focusing on high-performance scientific computing.",
+ "Search for the top Julia language packages for data visualization and their documentation.",
+ "How to set up a Julia development environment on Windows 10.",
+ "Discover the best practices for parallel computing in Julia.",
+ "Search for case studies of large-scale data processing using Julia.",
+ "Find comprehensive resources for mastering metaprogramming in Julia.",
+ "Looking for articles on the advantages of using Julia for statistical modeling.",
+ "How to contribute to the Julia open-source community: A step-by-step guide.",
+ "Find the comparison of numerical accuracy between Julia and MATLAB.",
+ "Looking for the latest Julia language updates and their impact on AI research.",
+ "How to efficiently handle big data with Julia: Techniques and libraries.",
+ "Discover how Julia integrates with other programming languages and tools.",
+ "Search for Julia-based frameworks for developing web applications.",
+ "Find tutorials on creating interactive dashboards with Julia.",
+ "How to use Julia for natural language processing and text analysis.",
+ "Discover the role of Julia in the future of computational finance and econometrics."
+]
Let's index these "documents":
julia
index = build_index(sentences; chunker_kwargs=(; sources=map(i -> "Doc$i", 1:length(sentences))))
This would be equivalent to the following index = build_index(SimpleIndexer(), sentences) which dispatches to the default implementation of each step via the SimpleIndexer struct. We provide these default implementations for the main functions as an optional argument - no need to provide them if you're running the default pipeline.
Notice that we have provided a chunker_kwargs argument to the build_index function. These will be kwargs passed to chunker step.
Now let's generate an answer to a question.
Run end-to-end RAG (retrieve + generate!), return AIMessage
julia
question = "What are the best practices for parallel computing in Julia?"
+
+msg = airag(index; question) # short for airag(RAGConfig(), index; question)
+## Output:
+## [ Info: Done with RAG. Total cost: \$0.0
+## AIMessage("Some best practices for parallel computing in Julia include us...
Explore what's happening under the hood by changing the return type - RAGResult contains all intermediate steps.
julia
result = airag(index; question, return_all=true)
+## RAGResult
+## question: String "What are the best practices for parallel computing in Julia?"
+## rephrased_questions: Array{String}((1,))
+## answer: SubString{String}
+## final_answer: SubString{String}
+## context: Array{String}((5,))
+## sources: Array{String}((5,))
+## emb_candidates: CandidateChunks{Int64, Float32}
+## tag_candidates: CandidateChunks{Int64, Float32}
+## filtered_candidates: CandidateChunks{Int64, Float32}
+## reranked_candidates: CandidateChunks{Int64, Float32}
+## conversations: Dict{Symbol, Vector{<:PromptingTools.AbstractMessage}}
You can still get the message from the result, see result.conversations[:final_answer] (the dictionary keys correspond to the function names of those steps).
If you need to customize it, break the pipeline into its sub-steps: retrieve and generate - RAGResult serves as the intermediate result.
julia
# Retrieve which chunks are relevant to the question
+result = retrieve(index, question)
+# Generate an answer
+result = generate!(index, result)
You can leverage a pretty-printing system with pprint where we automatically annotate the support of the answer by the chunks we provided to the model. It is configurable and you can select only some of its functions (eg, scores, sources).
julia
pprint(result)
You'll see the following in REPL but with COLOR highlighting in the terminal.
plaintext
--------------------
+QUESTION(s)
+--------------------
+- What are the best practices for parallel computing in Julia?
+
+--------------------
+ANSWER
+--------------------
+Some of the best practices for parallel computing in Julia include:[1,0.7]
+- Using [3,0.4]`@threads` for simple parallelism[1,0.34]
+- Utilizing `Distributed` module for more complex parallel tasks[1,0.19]
+- Avoiding excessive memory allocation
+- Considering task granularity for efficient workload distribution
+
+--------------------
+SOURCES
+--------------------
+1. Doc8
+2. Doc15
+3. Doc5
+4. Doc2
+5. Doc9
See ?print_html for the HTML version of the pretty-printing and styling system, eg, when you want to display the results in a web application based on Genie.jl/Stipple.jl.
How to read the output
Color legend:
No color: High match with the context, can be trusted more
Blue: Partial match against some words in the context, investigate
Magenta (Red): No match with the context, fully generated by the model
Square brackets: The best matching context ID + Match score of the chunk (eg, [3,0.4] means the highest support for the sentence is from the context chunk number 3 with a 40% match).
Want more?
See examples/building_RAG.jl for one more example.
This system is designed for information retrieval and response generation, structured in three main phases:
Preparation, when you create an instance of AbstractIndex
Retrieval, when you surface the top most relevant chunks/items in the index and return AbstractRAGResult, which contains the references to the chunks (AbstractCandidateChunks)
Generation, when you generate an answer based on the context built from the retrieved chunks, return either AIMessage or AbstractRAGResult
The corresponding functions are build_index, retrieve, and generate!, respectively. Here is the high-level diagram that shows the signature of the main functions:
Notice that the first argument is a custom type for multiple dispatch. In addition, observe the "kwargs" names, that's how the keyword arguments for each function are passed down from the higher-level functions (eg, build_index(...; chunker_kwargs=(; separators=...)))). It's the simplest way to customize some step of the pipeline (eg, set a custom model with a model kwarg or prompt template with template kwarg).
The system is designed to be hackable and extensible at almost every entry point. If you want to customize the behavior of any step, you can do so by defining a new type and defining a new method for the step you're changing, eg,
And then you would set the retrive step to use your custom MyReranker via reranker kwarg, eg, retrieve(....; reranker = MyReranker()) (or customize the main dispatching AbstractRetriever struct).
The overarching principles are:
Always dispatch / customize the behavior by defining a new Struct and the corresponding method for the existing functions (eg, rerank function for the re-ranking step).
Custom types are provided as the first argument (the high-level functions will work without them as we provide some defaults).
Custom types do NOT have any internal fields or DATA (with the exception of managing sub-steps of the pipeline like AbstractRetriever or RAGConfig).
Additional data should be passed around as keyword arguments (eg, chunker_kwargs in build_index to pass data to the chunking step). The intention was to have some clearly documented default values in the docstrings of each step + to have the various options all in one place.
If you need to pass keyword arguments, use the nested kwargs corresponding to the dispatch type names (rephrase step, has rephraser dispatch type and rephraser_kwargs for its keyword arguments).
For example:
julia
cfg = RAGConfig(; retriever = AdvancedRetriever())
+
+# kwargs will be big and nested, let's prepare them upfront
+# we specify "custom" model for each component that calls LLM
+kwargs = (
+ retriever = AdvancedRetriever(),
+ retriever_kwargs = (;
+ top_k = 100,
+ top_n = 5,
+ # notice that this is effectively: retriever_kwargs/rephraser_kwargs/template
+ rephraser_kwargs = (;
+ template = :RAGQueryHyDE,
+ model = "custom")),
+ generator_kwargs = (;
+ # pass kwargs to `answer!` step defined by the `answerer` -> we're setting `answerer_kwargs`
+ answerer_kwargs = (;
+ model = "custom"),
+ # api_kwargs can be shared across all components
+ api_kwargs = (;
+ url = "http://localhost:8080")))
+
+result = airag(cfg, index, question; kwargs...)
If you were one level deeper in the pipeline, working with retriever directly, you would pass:
julia
retriever_kwargs = (;
+ top_k = 100,
+ top_n = 5,
+ # notice that this is effectively: rephraser_kwargs/template
+ rephraser_kwargs = (;
+ template = :RAGQueryHyDE,
+ model = "custom"),
+ # api_kwargs can be shared across all components
+ api_kwargs = (;
+ url = "http://localhost:8080"))
+
+result = retrieve(AdvancedRetriever(), index, question; retriever_kwargs...)
And going even deeper, you would provide the rephraser_kwargs directly to the rephrase step, eg,
Begins with build_index, which creates a user-defined index type from an abstract chunk index using specified dels and function strategies.
get_chunks then divides the indexed data into manageable pieces based on a chunking strategy.
get_embeddings generates embeddings for each chunk using an embedding strategy to facilitate similarity arches.
Finally, get_tags extracts relevant metadata from each chunk, enabling tag-based filtering (hybrid search index). If there are tags available, build_tags is called to build the corresponding sparse matrix for filtering with tags.
Retrieval Phase:
The retrieve step is intended to find the most relevant chunks in the index.
rephrase is called first, if we want to rephrase the query (methods like HyDE can improve retrieval quite a bit)!
get_embeddings generates embeddings for the original + rephrased query
find_closest looks up the most relevant candidates (CandidateChunks) using a similarity search strategy.
get_tags extracts the potential tags (can be provided as part of the airag call, eg, when we want to use only some small part of the indexed chunks)
find_tags filters the candidates to strictly match at least one of the tags (if provided)
rerank is called to rerank the candidates based on the reranking strategy (ie, to improve the ordering of the chunks in context).
Generation Phase:
The generate! step is intended to generate a response based on the retrieved chunks, provided via AbstractRAGResult (eg, RAGResult).
build_context! constructs the context for response generation based on a context strategy and applies the necessary formatting
answer! generates the response based on the context and the query
refine! is called to refine the response (optional, defaults to passthrough)
postprocessing! is available for any final touches to the response or to potentially save or format the results (eg, automatically save to the disk)
Note that all generation steps are mutating the RAGResult object.
See more details and corresponding functions and types in src/Experimental/RAGTools/rag_interface.jl.
Build an INDEX for RAG (Retriever-Augmented Generation) applications from the provided file paths. INDEX is a object storing the document chunks and their embeddings (and potentially other information).
The function processes each file or document (depending on chunker), splits its content into chunks, embeds these chunks, optionally extracts metadata, and then combines this information into a retrievable index.
Define your own methods via indexer and its subcomponents (chunker, embedder, tagger).
Arguments
indexer::AbstractIndexBuilder: The indexing logic to use. Default is SimpleIndexer().
files_or_docs: A vector of valid file paths OR string documents to be indexed (chunked and embedded). Specify which mode to use via chunker.
verbose: An Integer specifying the verbosity of the logs. Default is 1 (high-level logging). 0 is disabled.
extras: An optional vector of extra information to be stored with each chunk. Default is nothing.
index_id: A unique identifier for the index. Default is a generated symbol.
chunker: The chunker logic to use for splitting the documents. Default is TextChunker().
chunker_kwargs: Parameters to be provided to the get_chunks function. Useful to change the separators or max_length.
sources: A vector of strings indicating the source of each chunk. Default is equal to files_or_docs.
embedder: The embedder logic to use for embedding the chunks. Default is BatchEmbedder().
embedder_kwargs: Parameters to be provided to the get_embeddings function. Useful to change the target_batch_size_length or reduce asyncmap tasks ntasks.
model: The model to use for embedding. Default is PT.MODEL_EMBEDDING.
tagger: The tagger logic to use for extracting tags from the chunks. Default is NoTagger(), ie, skip tag extraction. There are also PassthroughTagger and OpenTagger.
tagger_kwargs: Parameters to be provided to the get_tags function.
model: The model to use for tags extraction. Default is PT.MODEL_CHAT.
template: A template to be used for tags extraction. Default is :RAGExtractMetadataShort.
tags: A vector of vectors of strings directly providing the tags for each chunk. Applicable for tagger::PasstroughTagger.
api_kwargs: Parameters to be provided to the API endpoint. Shared across all API calls if provided.
cost_tracker: A Threads.Atomic{Float64} object to track the total cost of the API calls. Useful to pass the total cost to the parent call.
Returns
ChunkIndex: An object containing the compiled index of chunks, embeddings, tags, vocabulary, and sources.
# Default is loading a vector of strings and chunking them (`TextChunker()`)
+index = build_index(SimpleIndexer(), texts; chunker_kwargs = (; max_length=10))
+
+# Another example with tags extraction, splitting only sentences and verbose output
+# Assuming `test_files` is a vector of file paths
+indexer = SimpleIndexer(chunker=FileChunker(), tagger=OpenTagger())
+index = build_index(indexer, test_files;
+ chunker_kwargs(; separators=[". "]), verbose=true)
Notes
If you get errors about exceeding embedding input sizes, first check the max_length in your chunks. If that does NOT resolve the issue, try changing the embedding_kwargs. In particular, reducing the target_batch_size_length parameter (eg, 10_000) and number of tasks ntasks=1. Some providers cannot handle large batch sizes (eg, Databricks).
High-level wrapper for Retrieval-Augmented Generation (RAG), it combines together the retrieve and generate! steps which you can customize if needed.
The simplest version first finds the relevant chunks in index for the question and then sends these chunks to the AI model to help with generating a response to the question.
To customize the components, replace the types (retriever, generator) of the corresponding step of the RAG pipeline - or go into sub-routines within the steps. Eg, use subtypes(AbstractRetriever) to find the available options.
Arguments
cfg::AbstractRAGConfig: The configuration for the RAG pipeline. Defaults to RAGConfig(), where you can swap sub-types to customize the pipeline.
index::AbstractChunkIndex: The chunk index to search for relevant text.
question::AbstractString: The question to be answered.
return_all::Bool: If true, returns the details used for RAG along with the response.
verbose::Integer: If >0, enables verbose logging. The higher the number, the more nested functions will log.
api_kwargs: API parameters that will be forwarded to ALL of the API calls (aiembed, aigenerate, and aiextract).
retriever::AbstractRetriever: The retriever to use for finding relevant chunks. Defaults to cfg.retriever, eg, SimpleRetriever (with no question rephrasing).
retriever_kwargs::NamedTuple: API parameters that will be forwarded to the retriever call. Examples of important ones:
top_k::Int: Number of top candidates to retrieve based on embedding similarity.
top_n::Int: Number of candidates to return after reranking.
tagger::AbstractTagger: Tagger to use for tagging the chunks. Defaults to NoTagger().
tagger_kwargs::NamedTuple: API parameters that will be forwarded to the tagger call. You could provide the explicit tags directly with PassthroughTagger and tagger_kwargs = (; tags = ["tag1", "tag2"]).
generator::AbstractGenerator: The generator to use for generating the answer. Defaults to cfg.generator, eg, SimpleGenerator.
generator_kwargs::NamedTuple: API parameters that will be forwarded to the generator call. Examples of important ones:
answerer_kwargs::NamedTuple: API parameters that will be forwarded to the answerer call. Examples:
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerFromContext.
refiner::AbstractRefiner: The method to use for refining the answer. Defaults to generator.refiner, eg, NoRefiner.
refiner_kwargs::NamedTuple: API parameters that will be forwarded to the refiner call.
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerRefiner.
cost_tracker: An atomic counter to track the total cost of the operations (if you want to track the cost of multiple pipeline runs - it passed around in the pipeline).
Returns
If return_all is false, returns the generated message (msg).
If return_all is true, returns the detail of the full pipeline in RAGResult (see the docs).
See also build_index, retrieve, generate!, RAGResult
Examples
Using airag to get a response for a question:
julia
index = build_index(...) # create an index
+question = "How to make a barplot in Makie.jl?"
+msg = airag(index; question)
To understand the details of the RAG process, use return_all=true
julia
msg, details = airag(index; question, return_all = true)
+# details is a RAGDetails object with all the internal steps of the `airag` function
You can also pretty-print details to highlight generated text vs text that is supported by context. It also includes annotations of which context was used for each part of the response (where available).
julia
PT.pprint(details)
Example with advanced retrieval (with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n). In addition, it will be done with a "custom" locally-hosted model.
julia
cfg = RAGConfig(; retriever = AdvancedRetriever())
+
+# kwargs will be big and nested, let's prepare them upfront
+# we specify "custom" model for each component that calls LLM
+kwargs = (
+ retriever_kwargs = (;
+ top_k = 100,
+ top_n = 5,
+ rephraser_kwargs = (;
+ model = "custom"),
+ embedder_kwargs = (;
+ model = "custom"),
+ tagger_kwargs = (;
+ model = "custom")),
+ generator_kwargs = (;
+ answerer_kwargs = (;
+ model = "custom"),
+ refiner_kwargs = (;
+ model = "custom")),
+ api_kwargs = (;
+ url = "http://localhost:8080"))
+
+result = airag(cfg, index, question; kwargs...)
Retrieves the most relevant chunks from the index for the given question and returns them in the RAGResult object.
This is the main entry point for the retrieval stage of the RAG pipeline. It is often followed by generate! step.
Notes:
The default flow is build_context! -> answer! -> refine! -> postprocess!.
The arguments correspond to the steps of the retrieval process (rephrasing, embedding, finding similar docs, tagging, filtering by tags, reranking). You can customize each step by providing a new custom type that dispatches the corresponding function, eg, create your own type struct MyReranker<:AbstractReranker end and define the custom method for it rerank(::MyReranker,...) = ....
Note: Discover available retrieval sub-types for each step with subtypes(AbstractRephraser) and similar for other abstract types.
If you're using locally-hosted models, you can pass the api_kwargs with the url field set to the model's URL and make sure to provide corresponding model kwargs to rephraser, embedder, and tagger to use the custom models (they make AI calls).
Arguments
retriever: The retrieval method to use. Default is SimpleRetriever but could be AdvancedRetriever for more advanced retrieval.
index: The index that holds the chunks and sources to be retrieved from.
question: The question to be used for the retrieval.
verbose: If >0, it prints out verbose logging. Default is 1. If you set it to 2, it will print out logs for each sub-function.
top_k: The TOTAL number of closest chunks to return from find_closest. Default is 100. If there are multiple rephrased questions, the number of chunks per each item will be top_k ÷ number_of_rephrased_questions.
top_n: The TOTAL number of most relevant chunks to return for the context (from rerank step). Default is 5.
api_kwargs: Additional keyword arguments to be passed to the API calls (shared by all ai* calls).
rephraser: Transform the question into one or more questions. Default is retriever.rephraser.
rephraser_kwargs: Additional keyword arguments to be passed to the rephraser.
model: The model to use for rephrasing. Default is PT.MODEL_CHAT.
template: The rephrasing template to use. Default is :RAGQueryOptimizer or :RAGQueryHyDE (depending on the rephraser selected).
embedder: The embedding method to use. Default is retriever.embedder.
embedder_kwargs: Additional keyword arguments to be passed to the embedder.
finder: The similarity search method to use. Default is retriever.finder, often CosineSimilarity.
finder_kwargs: Additional keyword arguments to be passed to the similarity finder.
tagger: The tag generating method to use. Default is retriever.tagger.
tagger_kwargs: Additional keyword arguments to be passed to the tagger. Noteworthy arguments:
tags: Directly provide the tags to use for filtering (can be String, Regex, or Vector{String}). Useful for tagger = PassthroughTagger.
filter: The tag matching method to use. Default is retriever.filter.
filter_kwargs: Additional keyword arguments to be passed to the tag filter.
reranker: The reranking method to use. Default is retriever.reranker.
reranker_kwargs: Additional keyword arguments to be passed to the reranker.
model: The model to use for reranking. Default is rerank-english-v2.0 if you use reranker = CohereReranker().
cost_tracker: An atomic counter to track the cost of the retrieval. Default is Threads.Atomic{Float64}(0.0).
Find the 5 most relevant chunks from the index for the given question.
julia
# assumes you have an existing index `index`
+retriever = SimpleRetriever()
+
+result = retrieve(retriever,
+ index,
+ "What is the capital of France?",
+ top_n = 5)
+
+# or use the default retriever (same as above)
+result = retrieve(retriever,
+ index,
+ "What is the capital of France?",
+ top_n = 5)
Apply more advanced retrieval with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n).
Assume we already have `index`
+
+question = "What are the best practices for parallel computing in Julia?"
+
+# Retrieve the relevant chunks - returns RAGResult
+result = retrieve(index, question)
+
+# Generate the answer using the default generator, mutates the same result
+result = generate!(index, result)
Annotates the answer with the overlap/what's supported in context and returns the annotated tree of nodes representing the answer
Returns a "root" node with children nodes representing the sentences/code blocks in the answer. Only the "leaf" nodes are to be printed (to avoid duplication), "leaf" nodes are those with NO children.
Default logic:
Split into sentences/code blocks, then into tokens (~words).
Then match each token (~word) exactly.
If no exact match found, count trigram-based match (include the surrounding tokens for better contextual awareness).
If the match is higher than min_score, it's recorded in the score of the node.
Arguments
annotater::TrigramAnnotater: Annotater to use
answer::AbstractString: Text to annotate
context::AbstractVector: Context to annotate against, ie, look for "support" in the texts in context
min_score::Float64: Minimum score to consider a match. Default: 0.5, which means that half of the trigrams of each word should match
skip_trigrams::Bool: Whether to potentially skip trigram matching if exact full match is found. Default: true
hashed::Bool: Whether to use hashed trigrams. It's harder to debug, but it's much faster for larger texts (hashed text are held in a Set to deduplicate). Default: true
sources::Union{Nothing, AbstractVector{<:AbstractString}}: Sources to add at the end of the context. Default: nothing
min_source_score::Float64: Minimum score to consider/to display a source. Default: 0.25, which means that at least a quarter of the trigrams of each word should match to some context. The threshold is lower than min_score, because it's average across ALL words in a block, so it's much harder to match fully with generated text.
add_sources::Bool: Whether to add sources at the end of each code block/sentence. Sources are addded in the square brackets like "[1]". Default: true
add_scores::Bool: Whether to add source-matching scores at the end of each code block/sentence. Scores are added in the square brackets like "[0.75]". Default: true
kwargs: Additional keyword arguments to pass to trigram_support! and set_node_style!. See their documentation for more details (eg, customize the colors of the nodes based on the score)
Example
julia
annotater = TrigramAnnotater()
+context = [
+ "This is a test context.", "Another context sentence.", "Final piece of context."]
+answer = "This is a test context. Another context sentence."
+
+annotated_root = annotate_support(annotater, answer, context)
+pprint(annotated_root) # pretty print the annotated tree
Dispatch for annotate_support for AbstractRAGResult type. It extracts the final_answer and context from the result and calls annotate_support with them.
See annotate_support for more details.
Example
julia
res = RAGResult(; question = "", final_answer = "This is a test.",
+ context = ["Test context.", "Completely different"])
+annotated_root = annotate_support(annotater, res)
+PT.pprint(annotated_root)
Create a collection of question and answer evaluations (QAEvalItem) from document chunks and sources. This function generates Q&A pairs based on the provided document chunks, using a specified AI model and template.
Arguments
doc_chunks::Vector{<:AbstractString}: A vector of document chunks, each representing a segment of text.
sources::Vector{<:AbstractString}: A vector of source identifiers corresponding to each chunk in doc_chunks (eg, filenames or paths).
model: The AI model used for generating Q&A pairs. Default is PT.MODEL_CHAT.
instructions::String: Additional instructions or context to provide to the model generating QA sets. Defaults to "None.".
qa_template::Symbol: A template symbol that dictates the AITemplate that will be used. It must have placeholder context. Default is :CreateQAFromContext.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API endpoint.
verbose::Bool: If true, additional information like costs will be logged. Defaults to true.
Returns
Vector{QAEvalItem}: A vector of QAEvalItem structs, each containing a source, context, question, and answer. Invalid or empty items are filtered out.
Notes
The function internally uses aiextract to generate Q&A pairs based on the provided qa_template. So you can use any kwargs that you want.
Each QAEvalItem includes the context (document chunk), the generated question and answer, and the source.
The function tracks and reports the cost of AI calls if verbose is enabled.
Items where the question, answer, or context is empty are considered invalid and are filtered out.
Examples
Creating Q&A evaluations from a set of document chunks:
julia
doc_chunks = ["Text from document 1", "Text from document 2"]
+sources = ["source1", "source2"]
+qa_evals = build_qa_evals(doc_chunks, sources)
Working with Generative AI (and in particular with the text modality), requires a lot of text manipulation. PromptingTools.jl provides a set of utilities to make this process easier and more efficient.
recursive_splitter to split the text into sentences and words (of a desired length max_length)
replace_words to mask some sensitive words in your text before sending it to AI
wrap_string for wrapping the text into a desired length by adding newlines (eg, to fit some large text into your terminal width)
length_longest_common_subsequence to find the length of the longest common subsequence between two strings (eg, to compare the similarity between the context provided and generated text)
distance_longest_common_subsequence a companion utility for length_longest_common_subsequence to find the normalized distance between two strings. Always returns a number between 0-1, where 0 means the strings are identical and 1 means they are completely different.
You can import them simply via:
julia
using PromptingTools: recursive_splitter, replace_words, wrap_string, length_longest_common_subsequence, distance_longest_common_subsequence
There are many more (especially in the AgentTools and RAGTools experimental modules)!
RAGTools module contains the following text utilities:
split_into_code_and_sentences to split a string into code and sentences
tokenize to tokenize a string (eg, a sentence) into words
trigrams to generate trigrams from a string (eg, a word)
text_to_trigrams to generate trigrams from a larger string (ie, effectively wraps the three functions above)
STOPWORDS a set of common stopwords (very brief)
Feel free to open an issue or ask in the #generative-ai channel in the JuliaLang Slack if you have a specific need.
Split a given string text into chunks of a specified maximum length max_length. This is particularly useful for splitting larger documents or texts into smaller segments, suitable for models or systems with smaller context windows.
There is a method for dispatching on multiple separators, recursive_splitter(text::String, separators::Vector{String}; max_length::Int=35000) -> Vector{String} that mimics the logic of Langchain's RecursiveCharacterTextSplitter.
Arguments
text::String: The text to be split.
separator::String=" ": The separator used to split the text into minichunks. Defaults to a space character.
max_length::Int=35000: The maximum length of each chunk. Defaults to 35,000 characters, which should fit within 16K context window.
Returns
Vector{String}: A vector of strings, each representing a chunk of the original text that is smaller than or equal to max_length.
Notes
The function ensures that each chunk is as close to max_length as possible without exceeding it.
If the text is empty, the function returns an empty array.
The separator is re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible.
Examples
Splitting text with the default separator (" "):
julia
text = "Hello world. How are you?"
+chunks = recursive_splitter(text; max_length=13)
+length(chunks) # Output: 2
Split a given string text into chunks recursively using a series of separators, with each chunk having a maximum length of max_length (if it's achievable given the separators provided). This function is useful for splitting large documents or texts into smaller segments that are more manageable for processing, particularly for models or systems with limited context windows.
It was previously known as split_by_length.
This is similar to Langchain's RecursiveCharacterTextSplitter. To achieve the same behavior, use separators=["\n\n", "\n", " ", ""].
Arguments
text::AbstractString: The text to be split.
separators::Vector{String}: An ordered list of separators used to split the text. The function iteratively applies these separators to split the text. Recommend to use ["\n\n", ". ", "\n", " "]
max_length::Int: The maximum length of each chunk. Defaults to 35,000 characters. This length is considered after each iteration of splitting, ensuring chunks fit within specified constraints.
Returns
Vector{String}: A vector of strings, where each string is a chunk of the original text that is smaller than or equal to max_length.
Usage Tips
I tend to prefer splitting on sentences (". ") before splitting on newline characters ("\n") to preserve the structure of the text.
What's the difference between separators=["\n"," ",""] and separators=["\n"," "]? The former will split down to character level (""), so it will always achieve the max_length but it will split words (bad for context!) I prefer to instead set slightly smaller max_length but not split words.
How It Works
The function processes the text iteratively with each separator in the provided order. It then measures the length of each chunk and splits it further if it exceeds the max_length. If the chunks is "short enough", the subsequent separators are not applied to it.
Each chunk is as close to max_length as possible (unless we cannot split it any further, eg, if the splitters are "too big" / there are not enough of them)
If the text is empty, the function returns an empty array.
Separators are re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible. Apply strip if you do not need them.
The function provides separators as the second argument to distinguish itself from its single-separator counterpart dispatch.
Examples
Splitting text using multiple separators:
julia
text = "Paragraph 1\n\nParagraph 2. Sentence 1. Sentence 2.\nParagraph 3"
+separators = ["\n\n", ". ", "\n"] # split by paragraphs, sentences, and newlines (not by words)
+chunks = recursive_splitter(text, separators, max_length=20)
Splitting text using multiple separators - with splitting on words:
julia
text = "Paragraph 1\n\nParagraph 2. Sentence 1. Sentence 2.\nParagraph 3"
+separators = ["\n\n", ". ", "\n", " "] # split by paragraphs, sentences, and newlines, words
+chunks = recursive_splitter(text, separators, max_length=10)
Measures distance between two strings using the length of the longest common subsequence (ie, the lower the number, the better the match). Perfect match is distance = 0.0
Convenience wrapper around length_longest_common_subsequence to normalize the distances to 0-1 range. There is a also a dispatch for comparing a string vs an array of strings.
Notes
Use argmin and minimum to find the position of the closest match and the distance, respectively.
Matching with an empty string will always return 1.0 (worst match), even if the other string is empty as well (safety mechanism to avoid division by zero).
Arguments
input1::AbstractString: The first string to compare.
input2::AbstractString: The second string to compare.
Example
You can also use it to find the closest context for some AI generated summary/story:
julia
context = ["The enigmatic stranger vanished as swiftly as a wisp of smoke, leaving behind a trail of unanswered questions.",
+ "Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.",
+ "The ancient tree stood as a silent guardian, its gnarled branches reaching for the heavens.",
+ "The melody danced through the air, painting a vibrant tapestry of emotions.",
+ "Time flowed like a relentless river, carrying away memories and leaving imprints in its wake."]
+
+story = """
+ Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.
+
+ Under the celestial tapestry, the vast ocean whispered its secrets to the indifferent stars. Each ripple, a murmured confidence, each wave, a whispered lament. The glittering celestial bodies listened in silent complicity, their enigmatic gaze reflecting the ocean's unspoken truths. The cosmic dance between the sea and the sky, a symphony of shared secrets, forever echoing in the ethereal expanse.
+ """
+
+dist = distance_longest_common_subsequence(story, context)
+@info "The closest context to the query: "$(first(story,20))..." is: "$(context[argmin(dist)])" (distance: $(minimum(dist)))"
OpenAI's models are at the forefront of AI research and provide robust, state-of-the-art capabilities for many tasks.
There will be situations not or cannot use it (eg, privacy, cost, etc.). In that case, you can use local models (eg, Ollama) or other APIs (eg, Anthropic).
At the time of writing, OpenAI does NOT use the API calls for training their models.
API
OpenAI does not use data submitted to and generated by our API to train OpenAI models or improve OpenAI’s service offering. In order to support the continuous improvement of our models, you can fill out this form to opt-in to share your data with us. – How your data is used to improve our models
Getting an error "ArgumentError: api_key cannot be empty" despite having set OPENAI_API_KEY? {#Getting-an-error-"ArgumentError:-apikey-cannot-be-empty"-despite-having-set-OPENAIAPI_KEY?}
Quick fix: just provide kwarg api_key with your key to the aigenerate function (and other ai* functions).
This error is thrown when the OpenAI API key is not available in 1) local preferences or 2) environment variables (ENV["OPENAI_API_KEY"]).
First, check if you can access the key by running ENV["OPENAI_API_KEY"] in the Julia REPL. If it returns nothing, the key is not set.
If the key is set, but you still get the error, there was a rare bug in earlier versions where if you first precompiled PromptingTools without the API key, it would remember it and "compile away" the get(ENV,...) function call. If you're experiencing this bug on the latest version of PromptingTools, please open an issue on GitHub.
The solution is to force a new precompilation, so you can do any of the below:
Force precompilation (run Pkg.precompile() in the Julia REPL)
Update the PromptingTools package (runs precompilation automatically)
Delete your compiled cache in .julia DEPOT (usually .julia/compiled/v1.10/PromptingTools). You can do it manually in the file explorer or via Julia REPL: rm("~/.julia/compiled/v1.10/PromptingTools", recursive=true, force=true)
Getting an error "Rate limit exceeded" from OpenAI?
Have you opened a new account recently? It is quite likely that you've exceeded the free tier limits.
OpenAI has a rate limit on the number of requests and the number of tokens you can make in a given period. If you exceed either of these, you will receive a "Rate limit exceeded" error. "Free tier" (ie, before you pay the first 5 USD) has very low limits, eg, maximum of 3 requests per minute. See the OpenAI Rate Limits for more information.
If you look at the HTTP response headers in the error, you can see the limits remaining and how long until it resets, eg, x-ratelimit-remaining-* and x-ratelimit-reset-*.
If you want to avoid this error, you have two options:
Put a simple sleep(x) after every request, where x is calculated so that the number of your requests stays below the limit.
Use ntasks keyword argument in asyncmap to limit the number of concurrent requests. Eg, let's assume you want to process 100x c. 10,000 tokens, but your tier limit is only 60,000 tokens per minute. If we know that one request takes c. 10 seconds, it means that with ntasks=1 we would send 6 requests per minute, which already maxes out our limit. If we set ntasks=2, we could process 12 requests per minute, so we would need our limit to be 120,000 tokens per minute.
julia
# simple asyncmap loop with 2 concurrent requests; otherwise, same syntax as `map`
+asyncmap(my_prompts; ntasks=2) do prompt
+ aigenerate(prompt)
+end
If you use a local model (eg, with Ollama), it's free. If you use any commercial APIs (eg, OpenAI), you will likely pay per "token" (a sub-word unit).
For example, a simple request with a simple question and 1 sentence response in return (”Is statement XYZ a positive comment”) will cost you ~0.0001 (ie, one-hundredth of a cent)
Is it worth paying for?
GenAI is a way to buy time! You can pay cents to save tens of minutes every day.
Continuing the example above, imagine you have a table with 200 comments. Now, you can parse each one of them with an LLM for the features/checks you need. Assuming the price per call was 0.0001 , you'd pay 2 cents for the job and save 30-60 minutes of your time!
Configuring the Environment Variable for API Key
This is a guide for OpenAI's API key, but it works for any other API key you might need (eg, MISTRALAI_API_KEY for MistralAI API).
To use the OpenAI API with PromptingTools.jl, set your API key as an environment variable:
julia
ENV["OPENAI_API_KEY"] = "your-api-key"
As a one-off, you can:
set it in the terminal before launching Julia: export OPENAI_API_KEY = <your key>
set it in your setup.jl (make sure not to commit it to GitHub!)
Make sure to start Julia from the same terminal window where you set the variable. Easy check in Julia, run ENV["OPENAI_API_KEY"] and you should see your key!
A better way:
On a Mac, add the configuration line to your terminal's configuration file (eg, ~/.zshrc). It will get automatically loaded every time you launch the terminal
On Windows, set it as a system variable in "Environment Variables" settings (see the Resources)
The ethos of PromptingTools.jl is to allow you to use whatever model you want, which includes Open Source LLMs. The most popular and easiest to setup is Ollama.ai - see below for more information.
Ollama runs a background service hosting LLMs that you can access via a simple API. It's especially useful when you're working with some sensitive data that should not be sent anywhere.
Installation is very easy, just download the latest version here.
Once you've installed it, just launch the app and you're ready to go!
To check if it's running, go to your browser and open 127.0.0.1:11434. You should see the message "Ollama is running". Alternatively, you can run ollama serve in your terminal and you'll get a message that it's already running.
There are many models available in Ollama Library, including Llama2, CodeLlama, SQLCoder, or my personal favorite openhermes2.5-mistral.
Download new models with ollama pull <model_name> (eg, ollama pull openhermes2.5-mistral).
If you tend to use non-default options, it can get tedious to specify PT.* every time.
There are three ways how you can customize your workflows (especially when you use Ollama or other local models):
Import the functions/types you need explicitly at the top (eg, using PromptingTools: OllamaSchema)
Register your model and its associated schema (PT.register_model!(; name="123", schema=PT.OllamaSchema())). You won't have to specify the schema anymore only the model name. See Working with Ollama for more information.
Override your default model (PT.MODEL_CHAT) and schema (PT.PROMPT_SCHEMA). It can be done persistently with Preferences, eg, PT.set_preferences!("PROMPT_SCHEMA" => "OllamaSchema", "MODEL_CHAT"=>"llama2").
Let's say you would like to respond back to a model's response. How to do it?
With ai"" macro
The simplest way if you used ai"" macro, is to send a reply with the ai!"" macro. It will use the last response as the conversation.
julia
ai"Hi! I'm John"
+
+ai!"What's my name?"
+# Return: "Your name is John."
With aigenerate function
You can use the conversation keyword argument to pass the previous conversation (in all ai* functions). It will prepend the past conversation before sending the new request to the model.
To get the conversation, set return_all=true and store the whole conversation thread (not just the last message) in a variable. Then, use it as a keyword argument in the next call.
julia
conversation = aigenerate("Hi! I'm John"; return_all=true)
+@info last(conversation) # display the response
+
+# follow-up (notice that we provide past messages as conversation kwarg
+conversation = aigenerate("What's my name?"; return_all=true, conversation)
+
+## [ Info: Tokens: 50 @ Cost: $0.0 in 1.0 seconds
+## 5-element Vector{PromptingTools.AbstractMessage}:
+## PromptingTools.SystemMessage("Act as a helpful AI assistant")
+## PromptingTools.UserMessage("Hi! I'm John")
+## AIMessage("Hello John! How can I assist you today?")
+## PromptingTools.UserMessage("What's my name?")
+## AIMessage("Your name is John.")
Notice that the last message is the response to the second request, but with return_all=true we can see the whole conversation from the beginning.
Our responses are always in AbstractMessage types to ensure we can also handle downstream processing, error handling, and self-healing code (see airetry!).
A good use case for a typed response is when you have a complicated control flow and would like to group and handle certain outcomes differently. You can easily do it as an extra step after the response is received.
Trivially, we can use aiclassifier for Bool statements, eg,
julia
# We can do either
+mybool = tryparse(Bool, aiclassify("Is two plus two four?")) isa Bool # true
+
+# or simply check equality
+msg = aiclassify("Is two plus two four?") # true
+mybool = msg.content == "true"
Now a more complicated example with multiple categories mapping to an enum:
julia
choices = [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")]
+
+# Set up the return types we want
+@enum Categories A P O
+string_to_category = Dict("A" => A, "P" => P,"O" => O)
+
+# Run an example
+input = "spider"
+msg = aiclassify(:InputClassifier; choices, input)
+
+mytype = string_to_category[msg.content] # A (for animal)
How does it work? aiclassify guarantees to output one of our choices (and it handles some of the common quirks)!
How would we achieve the same with aigenerate and arbitrary struct? We need to use the "lazy" AIGenerate struct and airetry! to ensure we get the response and then we can process it further.
AIGenerate has two fields you should know about:
conversation - eg, the vector of "messages" in the current conversation (same as what you get from aigenerate with return_all=true)
success - a boolean flag if the request was successful AND if it passed any subsequent airetry! calls
Let's mimic a case where our "program" should return one of three types: SmallInt, LargeInt, FailedResponse.
We first need to define our custom types:
julia
+# not needed, just to show a fully typed example
+abstract type MyAbstractResponse end
+struct SmallInt <: MyAbstractResponse
+ number::Int
+end
+struct LargeInt <: MyAbstractResponse
+ number::Int
+end
+struct FailedResponse <: MyAbstractResponse
+ content::String
+end
Let's define our "program" as a function to be cleaner. Notice that we use AIGenerate and airetry! to ensure we get the response and then we can process it further.
julia
using PromptingTools.Experimental.AgentTools
+
+function give_me_number(prompt::String)::MyAbstractResponse
+ # Generate the response
+ response = AIGenerate(prompt; config=RetryConfig(;max_retries=2)) |> run!
+
+ # Check if it's parseable as Int, if not, send back to be fixed
+ # syntax: airetry!(CONDITION-TO-CHECK, <response object>, FEEDBACK-TO-MODEL)
+ airetry!(x->tryparse(Int,last_output(x))|>!isnothing, response, "Wrong output format! Answer with digits and nothing else. The number is:")
+
+ if response.success != true
+ ## we failed to generate a parseable integer
+ return FailedResponse("I failed to get the response. Last output: $(last_output(response))")
+ end
+ number = tryparse(Int,last_output(response))
+ return number < 1000 ? SmallInt(number) : LargeInt(number)
+end
+
+give_me_number("How many car seats are in Porsche 911T?")
+## [ Info: Condition not met. Retrying...
+## [ Info: Condition not met. Retrying...
+## SmallInt(2)
We ultimately received our custom type SmallInt with the number of car seats in the Porsche 911T (I hope it's correct!).
If you want to access the full conversation history (all the attempts and feedback), simply output the response object and explore response.conversation.
Many times, you will want to create a prompt template that you can reuse with different inputs (eg, to create templates for AIHelpMe or LLMTextAnalysis).
Previously, you would have to create a vector of SystemMessage and UserMessage objects and then save it to a disk and reload. Now, you can use the create_template function to do it for you. It's designed for quick prototyping, so it skips the serialization step and loads it directly into the template store (ie, you can use it like any other templates - try aitemplates() search).
The syntax is simple: create_template(;user=<user prompt>, system=<system prompt>, load_as=<template name>)
When called it creates a vector of messages, which you can use directly in the ai* functions. If you provide load_as, it will load the template in the template store (under the load_as name).
Let's generate a quick template for a simple conversation (only one placeholder: name)
julia
# first system message, then user message (or use kwargs)
+tpl=PT.create_template("You must speak like a pirate", "Say hi to {{name}}"; load_as="GreatingPirate")
+
+## 2-element Vector{PromptingTools.AbstractChatMessage}:
+## PromptingTools.SystemMessage("You must speak like a pirate")
+## PromptingTools.UserMessage("Say hi to {{name}}")
You can immediately use this template in ai* functions:
julia
aigenerate(tpl; name="Jack Sparrow")
+# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
Since we provided load_as, it's also registered in the template store:
julia
aitemplates("pirate")
+
+## 1-element Vector{AITemplateMetadata}:
+## PromptingTools.AITemplateMetadata
+## name: Symbol GreatingPirate
+## description: String ""
+## version: String "1.0"
+## wordcount: Int64 46
+## variables: Array{Symbol}((1,))
+## system_preview: String "You must speak like a pirate"
+## user_preview: String "Say hi to {{name}}"
+## source: String ""
So you can use it like any other template:
julia
aigenerate(:GreatingPirate; name="Jack Sparrow")
+# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
It will be saved and accessed under its basename, ie, GreatingPirate (same as load_as keyword argument).
Note: If you make any changes to the templates on the disk/in a folder, you need to explicitly reload all templates again!
If you are using the main PromptingTools templates, you can simply call PT.load_templates!(). If you have a project folder with your templates, you want to add it first:
julia
PT.load_templates!("templates")
After the first run, we will remember the folder and you can simply call PT.load_templates!() to reload all the templates in the future!
Do we have a RecursiveCharacterTextSplitter like Langchain?
Yes, we do! Look for utility recursive_spliter (previously known as split_by_length). See its docstring for more information.
For reference, Langchain's RecursiveCharacterTextSplitter uses the following setting: separators = ["\n\n", "\n", " ", ""].
I'd recommend using the following instead: separators = ["\\n\\n", ". ", "\\n", " "] (ie, it does not split words, which tends to be unnecessary and quite damaging to the chunk quality).
Example:
julia
using PromptingTools: recursive_splitter
+
+text = "Paragraph 1\n\nParagraph 2. Sentence 1. Sentence 2.\nParagraph 3"
+separators = ["\n\n", ". ", "\n", " "] # split by paragraphs, sentences, and newlines, and words
+chunks = recursive_splitter(text, separators, max_length=10)
Fine-tuning is a powerful technique to adapt a model to your specific use case (mostly the format/syntax/task). It requires a dataset of examples, which you can now easily generate with PromptingTools.jl!
You can save any conversation (vector of messages) to a file with PT.save_conversation("filename.json", conversation).
Once the finetuning time comes, create a bundle of ShareGPT-formatted conversations (common finetuning format) in a single .jsonl file. Use PT.save_conversations("dataset.jsonl", [conversation1, conversation2, ...]) (notice that plural "conversationS" in the function name).
You will need to set this key as an environment variable before using PromptingTools.jl:
For a quick start, simply set it via ENV["OPENAI_API_KEY"] = "your-api-key" Alternatively, you can:
set it in the terminal before launching Julia: export OPENAI_API_KEY = <your key>
set it in your setup.jl (make sure not to commit it to GitHub!)
Make sure to start Julia from the same terminal window where you set the variable. Easy check in Julia, run ENV["OPENAI_API_KEY"] and you should see your key!
For other options or more robust solutions, see the FAQ section.
PromptingTools can be installed using the following commands:
julia
using Pkg
+Pkg.add("PromptingTools.jl")
Throughout the rest of this tutorial, we will assume that you have installed the PromptingTools package and have already typed using PromptingTools to bring all of the relevant variables into your current namespace.
The easiest start is the @ai_str macro. Simply type ai"your prompt" and you will get a response from the default model (GPT-3.5 Turbo).
julia
ai"What is the capital of France?"
plaintext
[ Info: Tokens: 31 @ Cost: $0.0 in 1.5 seconds --> Be in control of your spending!
+AIMessage("The capital of France is Paris.")
Returned object is a light wrapper with generated message in field :content (eg, ans.content) for additional downstream processing.
If you want to reply to the previous message, or simply continue the conversation, use @ai!_str (notice the bang !):
julia
ai!"And what is the population of it?"
You can easily inject any variables with string interpolation:
julia
country = "Spain"
+ai"What is the capital of \$(country)?"
plaintext
[ Info: Tokens: 32 @ Cost: $0.0001 in 0.5 seconds
+AIMessage("The capital of Spain is Madrid.")
Pro tip: Use after-string-flags to select the model to be called, eg, ai"What is the capital of France?"gpt4 (use gpt4t for the new GPT-4 Turbo model). Great for those extra hard questions!
For more complex prompt templates, you can use handlebars-style templating and provide variables as keyword arguments:
julia
msg = aigenerate("What is the capital of {{country}}? Is the population larger than {{population}}?", country="Spain", population="1M")
plaintext
[ Info: Tokens: 74 @ Cost: $0.0001 in 1.3 seconds
+AIMessage("The capital of Spain is Madrid. And yes, the population of Madrid is larger than 1 million. As of 2020, the estimated population of Madrid is around 3.3 million people.")
Pro tip: Use asyncmap to run multiple AI-powered tasks concurrently.
Pro tip: If you use slow models (like GPT-4), you can use the asynchronous version of @ai_str -> @aai_str to avoid blocking the REPL, eg, aai"Say hi but slowly!"gpt4 (similarly @ai!_str -> @aai!_str for multi-turn conversations).
This is an advanced section that explains how PromptingTools.jl works under the hood. It is not necessary to understand this to use the package, but it can be helpful for debugging and understanding the limitations of the package.
We'll start with the key concepts and then walk through an example of aigenerate to see how it all fits together.
API/Model Providers -> The method that gives you access to Large Language Models (LLM), it can be an API (eg, OpenAI) or a locally-hosted application (eg, Llama.cpp or Ollama)
Schemas -> object of type AbstractPromptSchema that determines which methods are called and, hence, what providers/APIs are used
Prompts -> the information you want to convey to the AI model
Messages -> the basic unit of communication between the user and the AI model (eg, UserMessage vs AIMessage)
Prompt Templates -> re-usable "prompts" with placeholders that you can replace with your inputs at the time of making the request
When you call aigenerate, roughly the following happens: render -> UserMessage(s) -> render -> OpenAI.create_chat -> ... -> AIMessage.
You can think of "API/Model Providers" as the method that gives you access to Large Language Models (LLM). It can be an API (eg, OpenAI) or a locally-hosted application (eg, Llama.cpp or Ollama).
You interact with them via the schema object, which is a subtype of AbstractPromptSchema, eg, there is an OpenAISchema for the provider "OpenAI" and its supertype AbstractOpenAISchema is for all other providers that mimic the OpenAI API.
For your "message" to reach an AI model, it needs to be formatted and sent to the right place (-> provider!).
We leverage the multiple dispatch around the "schemas" to pick the right logic. All schemas are subtypes of AbstractPromptSchema and there are many subtypes, eg, OpenAISchema <: AbstractOpenAISchema <:AbstractPromptSchema.
For example, if you provide schema = OpenAISchema(), the system knows that:
it will have to format any user inputs to OpenAI's "message specification" (a vector of dictionaries, see their API documentation). Function render(OpenAISchema(),...) will take care of the rendering.
it will have to send the message to OpenAI's API. We will use the amazing OpenAI.jl package to handle the communication.
Prompt is loosely the information you want to convey to the AI model. It can be a question, a statement, or a command. It can have instructions or some context, eg, previous conversation.
You need to remember that Large Language Models (LLMs) are stateless. They don't remember the previous conversation/request, so you need to provide the whole history/context every time (similar to how REST APIs work).
Prompts that we send to the LLMs are effectively a sequence of messages (<:AbstractMessage).
Messages are the basic unit of communication between the user and the AI model.
There are 5 main types of messages (<:AbstractMessage):
SystemMessage - this contains information about the "system", eg, how it should behave, format its output, etc. (eg, `You're a world-class Julia programmer. You write brief and concise code.)
UserMessage - the information "from the user", ie, your question/statement/task
UserMessageWithImages - the same as UserMessage, but with images (URLs or Base64-encoded images)
AIMessage - the response from the AI model, when the "output" is text
DataMessage - the response from the AI model, when the "output" is data, eg, embeddings with aiembed or user-defined structs with aiextract
We want to have re-usable "prompts", so we provide you with a system to retrieve pre-defined prompts with placeholders (eg, ) that you can replace with your inputs at the time of making the request.
"AI Templates" as we call them (AITemplate) are usually a vector of SystemMessage and a UserMessage with specific purpose/task.
For example, the template :AssistantAsk is defined loosely as:
julia
template = [SystemMessage("You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer."),
+ UserMessage("# Question\n\n{{ask}}")]
Notice that we have a placeholder ask () that you can replace with your question without having to re-write the generic system instructions.
When you provide a Symbol (eg, :AssistantAsk) to ai* functions, thanks to the multiple dispatch, it recognizes that it's an AITemplate(:AssistantAsk) and looks it up.
You can discover all available templates with aitemplates("some keyword") or just see the details of some template aitemplates(:AssistantAsk).
Note: There is a new way to create and register templates in one go with create_template(;user=<user prompt>, system=<system prompt>, load_as=<template name>) (it skips the serialization step where a template previously must have been saved somewhere on the disk). See FAQ for more details or directly ?create_template.
The above steps are implemented in the ai* functions, eg, aigenerate, aiembed, aiextract, etc. They all have the same basic structure:
ai*(<optional schema>,<prompt or conversation>; <optional keyword arguments>),
but they differ in purpose:
aigenerate is the general-purpose function to generate any text response with LLMs, ie, it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString)
aiembed is designed to extract embeddings from the AI model's response, ie, it returns DataMessage with field :content containing the embeddings (eg, ans.content isa AbstractArray)
aiextract is designed to extract structured data from the AI model's response and return them as a Julia struct (eg, if we provide return_type=Food, we get ans.content isa Food). You need to define the return type first and then provide it as a keyword argument.
aiclassify is designed to classify the input text into (or simply respond within) a set of discrete choices provided by the user. It can be very useful as an LLM Judge or a router for RAG systems, as it uses the "logit bias trick" and generates exactly 1 token. It returns AIMessage with field :content, but the :content can be only one of the provided choices (eg, ans.content in choices)
aiscan is for working with images and vision-enabled models (as an input), but it returns AIMessage with field :content containing the generated text (eg, ans.content isa AbstractString) similar to aigenerate.
aiimage is for generating images (eg, with OpenAI DALL-E 3). It returns a DataMessage, where the field :content might contain either the URL to download the image from or the Base64-encoded image depending on the user-provided kwarg api_kwargs.response_format.
aitemplates is a helper function to discover available templates and see their details (eg, aitemplates("some keyword") or aitemplates(:AssistantAsk))
If you're using a known model, you do NOT need to provide a schema (the first argument).
Optional keyword arguments in ai* tend to be:
model::String - Which model you want to use
verbose::Bool - Whether you went to see INFO logs around AI costs
return_all::Bool - Whether you want the WHOLE conversation or just the AI answer (ie, whether you want to include your inputs/prompt in the output)
api_kwargs::NamedTuple - Specific parameters for the model, eg, temperature=0.0 to be NOT creative (and have more similar output in each run)
http_kwargs::NamedTuple - Parameters for the HTTP.jl package, eg, readtimeout = 120 to time out in 120 seconds if no response was received.
In addition to the above list of ai* functions, you can also use the "lazy" counterparts of these functions from the experimental AgentTools module.
julia
using PromptingTools.Experimental.AgentTools
For example, AIGenerate() will create a lazy instance of aigenerate. It is an instance of AICall with aigenerate as its ai function. It uses exactly the same arguments and keyword arguments as aigenerate (see ?aigenerate for details).
"lazy" refers to the fact that it does NOT generate any output when instantiated (only when run! is called).
Or said differently, the AICall struct and all its flavors (AIGenerate, ...) are designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied.
This approach allows us to remember user inputs and trigger the LLM call repeatedly if needed, which enables automatic fixing (see ?airetry!).
Example:
julia
result = AIGenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1))
+result |> run!
+
+# Is equivalent to
+result = aigenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1), return_all=true)
+# The only difference is that we default to `return_all=true` with lazy types because we have a dedicated `conversation` field, which makes it much easier
Lazy AI calls and self-healing mechanisms unlock much more robust and useful LLM workflows!
using PromptingTools
+const PT = PromptingTools
+
+# Let's say this is our ask
+msg = aigenerate(:AssistantAsk; ask="What is the capital of France?")
+
+# it is effectively the same as:
+msg = aigenerate(PT.OpenAISchema(), PT.AITemplate(:AssistantAsk); ask="What is the capital of France?", model="gpt3t")
There is no model provided, so we use the default PT.MODEL_CHAT (effectively GPT3.5-Turbo). Then we look it up in PT.MDOEL_REGISTRY and use the associated schema for it (OpenAISchema in this case).
The next step is to render the template, replace the placeholders and render it for the OpenAI model.
julia
# Let's remember out schema
+schema = PT.OpenAISchema()
+ask = "What is the capital of France?"
First, we obtain the template (no placeholder replacement yet) and "expand it"
2-element Vector{PromptingTools.AbstractChatMessage}:
+ PromptingTools.SystemMessage("You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")
+ PromptingTools.UserMessage{String}("# Question\n\n{{ask}}", [:ask], :usermessage)
2-element Vector{Dict{String, Any}}:
+ Dict("role" => "system", "content" => "You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.")
+ Dict("role" => "user", "content" => "# Question\n\nWhat is the capital of France?")
Notice that the placeholders are only replaced in the second step. The final output here is a vector of messages with "role" and "content" keys, which is the format required by the OpenAI API.
As a side note, under the hood, the second step is done in two sub-steps:
replace the placeholders messages_rendered = PT.render(PT.NoSchema(), template_rendered; ask) -> returns a vector of Messages!
then, we convert the messages to the format required by the provider/schema PT.render(schema, messages_rendered) -> returns the OpenAI formatted messages
Next, we send the above rendered_for_api to the OpenAI API and get the response back.
julia
using OpenAI
+OpenAI.create_chat(api_key, model, rendered_for_api)
The last step is to take the JSON response from the API and convert it to the AIMessage object.
In practice, there are more fields we extract, so we define a utility for it: PT.response_to_message. Especially, since with parameter n, you can request multiple AI responses at once, so we want to re-use our response processing logic.
That's it! I hope you've learned something new about how PromptingTools.jl works under the hood.
Whereas aigenerate is a general-purpose function to generate any text response with LLMs, aiextract is designed to extract structured data from the AI model's response and return them as a Julia struct.
It's a bit more complicated than aigenerate because it needs to handle the JSON schema of the return type (= our struct).
Let's define a toy example of a struct and see how aiextract works under the hood.
julia
using PromptingTools
+const PT = PromptingTools
+
+"""
+Extract the name of the food from the sentence. Extract any provided adjectives for the food as well.
+
+Example: "I am eating a crunchy bread." -> Food("bread", ["crunchy"])
+"""
+struct Food
+ name::String # required field!
+ adjectives::Union{Nothing,Vector{String}} # not required because `Nothing` is allowed
+end
+
+msg = aiextract("I just ate a delicious and juicy apple."; return_type=Food)
+msg.content
+# Food("apple", ["delicious", "juicy"])
You can see that we sent a prompt to the AI model and it returned a Food object. We provided some light guidance as a docstring of the return type, but the AI model did the heavy lifting.
aiextract leverages native "function calling" (supported by OpenAI, Fireworks, Together, and many others).
We encode the user-provided return_type into the corresponding JSON schema and create the payload as per the specifications of the provider.
Let's how that's done:
julia
sig = PT.function_call_signature(Food)
+## Dict{String, Any} with 3 entries:
+## "name" => "Food_extractor"
+## "parameters" => Dict{String, Any}("properties"=>Dict{String, Any}("name"=>Dict("type"=>"string"), "adjectives"=>Dict{String, …
+## "description" => "Extract the food from the sentence. Extract any provided adjectives for the food as well.\n\nExample: "
You can see that we capture the field names and types in parameters and the description in description key.
Furthermore, if we zoom in on the "parameter" field, you can see that we encode not only the names and types but also whether the fields are required (ie, do they allow Nothing) You can see below that the field adjectives accepts Nothing, so it's not required. Only the name field is required.
This is why you can sometimes have errors when you use abstract types in your return_type -> to enable that, you would need to set the right StructTypes behavior for your abstract type (see the JSON3.jl documentation for more details on how to do that).
It works quite well for concrete types and "vanilla" structs, though.
Unfortunately, function calling is generally NOT supported by locally-hosted / open-source models, so let's try to build a workaround with aigenerate
You need to pick a bigger / more powerful model, as it's NOT an easy task to output a correct JSON specification. My laptop isn't too powerful and I don't like waiting, so I'm going to use Mixtral model hosted on Together.ai (you get $25 credit when you join)!
julia
model = "tmixtral" # tmixtral is an alias for "mistralai/Mixtral-8x7B-Instruct-v0.1" on Together.ai and it automatically sets `schema = TogetherOpenAISchema()`
We'll add the signature to the prompt and we'll request the JSON output in two places - in the prompt and in the api_kwargs (to ensure that the model outputs the JSON via "grammar") NOTE: You can write much better and more specific prompt if you have a specific task / return type in mind + you should make sure that the prompt + struct description make sense together!
Let's define a prompt and return_type. Notice that we add several placeholders (eg, ) to fill with user inputs later.
julia
prompt = """
+You're a world-class data extraction engine.
+
+Your task is to extract information formatted as per the user provided schema.
+You MUST response in JSON format.
+
+**Example:**
+---------
+Description: "Extract the Car from the sentence. Extract the corresponding brand and model as well."
+Input: "I drive a black Porsche 911 Turbo."
+Schema: "{\"properties\":{\"model\":{\"type\":\"string\"},\"brand\":{\"type\":\"string\"}},\"required\":[\"brand\",\"model\"],\"type\":\"object\"}"
+Output: "{\"model\":\"911 Turbo\",\"brand\":\"Porsche\"}"
+---------
+
+**User Request:**
+Description: {{description}}
+Input: {{input}}
+Schema: {{signature}}
+Output:
+
+You MUST OUTPUT in JSON format.
+"""
We need to extract the "signature of our return_type and put it in the right placeholders. Let's generate now!
julia
sig = PT.function_call_signature(Food)
+result = aigenerate(prompt; input="I just ate a delicious and juicy apple.",
+ schema=JSON3.write(sig["parameters"]), description=sig["description"],
+ ## We provide the JSON output requirement as per API docs: https://docs.together.ai/docs/json-mode
+ model, api_kwargs=(; response_format=Dict("type" => "json_object"), temperature=0.2), return_all=true)
+result[end].content
+## "{\n \"adjectives\": [\"delicious\", \"juicy\"],\n \"food\": \"apple\"\n}"
We're using a smaller model, so the output is not perfect. Let's try to load into our object:
julia
obj = JSON3.read(result[end].content, Food)
+# Output: ERROR: MethodError: Cannot `convert` an object of type Nothing to an object of type String
Unfortunately, we get an error because the model mixed up the key "name" for "food", so it cannot be parsed.
Fortunately, we can do better and use automatic fixing! All we need to do is to change from aigenerate -> AIGenerate (and use airetry!)
The signature of AIGenerate is identical to aigenerate with the exception of config field, where we can influence the future retry behaviour.
julia
result = AIGenerate(prompt; input="I just ate a delicious and juicy apple.",
+ schema=JSON3.write(sig["parameters"]), description=sig["description"],
+ ## We provide the JSON output requirement as per API docs: https://docs.together.ai/docs/json-mode
+ model, api_kwargs=(; response_format=Dict("type" => "json_object"), temperature=0.2),
+ ## limit the number of retries, default is 10 rounds
+ config=RetryConfig(; max_retries=3))
+run!(result) # run! triggers the generation step (to have some AI output to check)
Let's set up a retry mechanism with some practical feedback. We'll leverage airetry! to automatically retry the request and provide feedback to the model. Think of airetry! as @assert on steroids:
It can retry automatically, not just throw an error
It manages the "conversation’ (list of messages) for you, including adding user-provided feedback to help generate better output
julia
feedback = "The output is not in the correct format. The keys should be $(join([string("\"$f\"") for f in fieldnames(Food)],", "))."
+# We use do-syntax with provide the `CONDITION` (it must return Bool)
+airetry!(result, feedback) do conv
+ ## try to convert
+ obj = try
+ JSON3.read(last_output(conv), Food)
+ catch e
+ ## you could save the error and provide as feedback (eg, into a slot in the `:memory` field of the AICall object)
+ e
+ end
+ ## Check if the conversion was successful; if it's `false`, it will retry
+ obj isa Food # -> Bool
+end
+food = JSON3.read(last_output(result), Food)
+## [ Info: Condition not met. Retrying...
+## Output: Food("apple", ["delicious", "juicy"])
It took 1 retry (see result.config.retries) and we have the correct output from an open-source model!
If you're interested in the result object, it's a struct (AICall) with a field conversation, which holds the conversation up to this point. AIGenerate is an alias for AICall using aigenerate function. See ?AICall (the underlying struct type) for more details on the fields and methods available.
+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/index.html b/previews/PR116/index.html
new file mode 100644
index 000000000..69ef52455
--- /dev/null
+++ b/previews/PR116/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ PromptingTools.jl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Leverage prompt templates with placeholders to make complex prompts easy.
Effortless Integration
Fire quick questions with `@ai_str` macro and light wrapper types. Minimal dependencies for seamless integration.
Designed for Discoverability
Efficient access to cutting-edge models with intuitive `ai...` functions. Stay in the flow with minimal context switching.
Why PromptingTools.jl?
Prompt engineering is neither fast nor easy. Moreover, different models and their fine-tunes might require different prompt formats and tricks, or perhaps the information you work with requires special models to be used. PromptingTools.jl is meant to unify the prompts for different backends and make the common tasks (like templated prompts) as simple as possible.
Getting Started
Add PromptingTools, set OpenAI API key and generate your first answer:
julia
using Pkg
+Pkg.add("PromptingTools")
+# Requires OPENAI_API_KEY environment variable!
+
+ai"What is the meaning of life?"
Description: For RAG applications. Answers the provided Questions based on the Context. Placeholders: question, context
Placeholders: context, question
Word count: 375
Source:
Version: 1.0
System Prompt:
plaintext
Act as a world-class AI assistant with access to the latest knowledge via Context Information.
+
+**Instructions:**
+- Answer the question based only on the provided Context.
+- If you don't know the answer, just say that you don't know, don't try to make up an answer.
+- Be brief and concise.
+
+**Context Information:**
+---
+{{context}}
+---
Description: For RAG applications. Extracts metadata from the provided text using longer instructions set and examples. If you don't have any special instructions, provide instructions="None.". Placeholders: text, instructions
Placeholders: text, instructions
Word count: 1384
Source:
Version: 1.1
System Prompt:
plaintext
You're a world-class data extraction engine built by OpenAI together with Google and to extract filter metadata to power the most advanced search engine in the world.
+
+ **Instructions for Extraction:**
+ 1. Carefully read through the provided Text
+ 2. Identify and extract:
+ - All relevant entities such as names, places, dates, etc.
+ - Any special items like technical terms, unique identifiers, etc.
+ - In the case of Julia code or Julia documentation: specifically extract package names, struct names, function names, and important variable names (eg, uppercased variables)
+ 3. Keep extracted values and categories short. Maximum 2-3 words!
+ 4. You can only extract 3-5 items per Text, so select the most important ones.
+ 5. Assign a search filter Category to each extracted Value
+
+ **Example 1:**
+ - Document Chunk: "Dr. Jane Smith published her findings on neuroplasticity in 2021. The research heavily utilized the DataFrames.jl and Plots.jl packages."
+ - Extracted keywords:
+ - Name: Dr. Jane Smith
+ - Date: 2021
+ - Technical Term: neuroplasticity
+ - JuliaPackage: DataFrames.jl, Plots.jl
+ - JuliaLanguage:
+ - Identifier:
+ - Other:
+
+ If the user provides special instructions, prioritize these over the general instructions.
User Prompt:
plaintext
# Text
+
+{{text}}
+
+
+
+# Special Instructions
+
+{{instructions}}
Description: For RAG applications. Extracts metadata from the provided text. If you don't have any special instructions, provide instructions="None.". Placeholders: text, instructions
Placeholders: text, instructions
Word count: 278
Source:
Version: 1.0
System Prompt:
plaintext
Extract search keywords and their categories from the Text provided below (format "value:category"). Each keyword must be at most 2-3 words. Provide at most 3-5 keywords. I will tip you $50 if the search is successful.
User Prompt:
plaintext
# Text
+
+{{text}}
+
+
+
+# Special Instructions
+
+{{instructions}}
Description: For RAG applications (refine step), gives model the ability to refine its answer based on some additional context etc.. The hope is that it better answers the original query. Placeholders: query, answer, context
Act as a world-class AI assistant with access to the latest knowledge via Context Information.
+
+Your task is to refine an existing answer if it's needed.
+
+The original query is as follows:
+{{query}}
+
+The AI model has provided the following answer:
+{{answer}}
+
+**Instructions:**
+- Given the new context, refine the original answer to better answer the query.
+- If the context isn't useful, return the original answer.
+- If you don't know the answer, just say that you don't know, don't try to make up an answer.
+- Be brief and concise.
+- Provide the refined answer only and nothing else.
User Prompt:
plaintext
We have the opportunity to refine the previous answer (only if needed) with some more context below.
+
+**Context Information:**
+-----------------
+{{context}}
+-----------------
+
+Given the new context, refine the original answer to better answer the query.
+If the context isn't useful, return the original answer.
+Provide the refined answer only and nothing else.
+
+Refined Answer:
Description: For RAG applications. Generate Question and Answer from the provided Context. If you don't have any special instructions, provide instructions="None.". Placeholders: context, instructions
Placeholders: context, instructions
Word count: 1396
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class teacher preparing contextual Question & Answer sets for evaluating AI systems.
+
+**Instructions for Question Generation:**
+1. Analyze the provided Context chunk thoroughly.
+2. Formulate a question that:
+ - Is specific and directly related to the information in the context chunk.
+ - Is not too short or generic; it should require a detailed understanding of the context to answer.
+ - Can only be answered using the information from the provided context, without needing external information.
+
+**Instructions for Reference Answer Creation:**
+1. Based on the generated question, compose a reference answer that:
+ - Directly and comprehensively answers the question.
+ - Stays strictly within the bounds of the provided context chunk.
+ - Is clear, concise, and to the point, avoiding unnecessary elaboration or repetition.
+
+**Example 1:**
+- Context Chunk: "In 1928, Alexander Fleming discovered penicillin, which marked the beginning of modern antibiotics."
+- Generated Question: "What was the significant discovery made by Alexander Fleming in 1928 and its impact?"
+- Reference Answer: "Alexander Fleming discovered penicillin in 1928, which led to the development of modern antibiotics."
+
+If the user provides special instructions, prioritize these over the general instructions.
User Prompt:
plaintext
# Context Information
+---
+{{context}}
+---
+
+
+# Special Instructions
+
+{{instructions}}
Description: For RAG applications. Judge an answer to a question on a scale from 1-5. Placeholders: question, context, answer
Placeholders: question, context, answer
Word count: 1415
Source:
Version: 1.1
System Prompt:
plaintext
You're an impartial judge. Your task is to evaluate the quality of the Answer provided by an AI assistant in response to the User Question on a scale from 1 to 5.
+
+1. **Scoring Criteria:**
+- **Relevance (1-5):** How well does the provided answer align with the context?
+ - *1: Not relevant, 5: Highly relevant*
+- **Completeness (1-5):** Does the provided answer cover all the essential points mentioned in the context?
+ - *1: Very incomplete, 5: Very complete*
+- **Clarity (1-5):** How clear and understandable is the provided answer?
+ - *1: Not clear at all, 5: Extremely clear*
+- **Consistency (1-5):** How consistent is the provided answer with the overall context?
+ - *1: Highly inconsistent, 5: Perfectly consistent*
+- **Helpfulness (1-5):** How helpful is the provided answer in answering the user's question?
+ - *1: Not helpful at all, 5: Extremely helpful*
+
+2. **Judging Instructions:**
+- As an impartial judge, please evaluate the provided answer based on the above criteria.
+- Assign a score from 1 to 5 for each criterion, considering the original context, question and the provided answer.
+- The Final Score is an average of these individual scores, representing the overall quality and relevance of the provided answer. It must be between 1-5.
+
+```
Description: For RAG applications. Simple and short prompt to judge answer to a question on a scale from 1-5. Placeholders: question, context, answer
Placeholders: question, context, answer
Word count: 420
Source:
Version: 1.0
System Prompt:
plaintext
You re an impartial judge.
+Read carefully the provided question and the answer based on the context.
+Provide a rating on a scale 1-5 (1=worst quality, 5=best quality) that reflects how relevant, helpful, clear, and consistent with the provided context the answer was.
+```
Description: For Julia-specific RAG applications (rephrase step), inspired by the HyDE approach where it generates a hypothetical passage that answers the provided user query to improve the matched results. This explicitly requires and optimizes for Julia-specific questions. Placeholders: query
Placeholders: query
Word count: 390
Source:
Version: 1.0
System Prompt:
plaintext
You're an world-class AI assistant specialized in Julia language questions.
+
+Your task is to generate a BRIEF and SUCCINCT hypothetical passage from Julia language ecosystem documentation that answers the provided query.
+
+Query: {{query}}
User Prompt:
plaintext
Write a hypothetical snippet with 20-30 words that would be the perfect answer to the query. Try to include as many key details as possible.
+
+Passage:
Description: For RAG applications (rephrase step), inspired by the HyDE paper where it generates a hypothetical passage that answers the provided user query to improve the matched results. Placeholders: query
You are a world-class search expert specializing in query transformations.
+
+Your task is to write a hypothetical passage that would answer the below question in the most effective way possible.
+
+It must have 20-30 words and be directly aligned with the intended search objective.
+Try to include as many key details as possible.
Description: For RAG applications (rephrase step), it rephrases the original query to attract more diverse set of potential search results. Placeholders: query
You are a world-class search expert specializing in query rephrasing.
+Your task is to refine the provided query to ensure it is highly effective for retrieving relevant search results.
+Analyze the given input to grasp the core semantic intent or meaning.
User Prompt:
plaintext
Original Query: {{query}}
+
+Your goal is to rephrase or enhance this query to improve its search performance. Ensure the revised query is concise and directly aligned with the intended search objective.
+Respond with the optimized query only.
+
+Optimized query:
Description: For RAG applications (rephrase step), it rephrases the original query by stripping unnecessary details to improve the matched results. Placeholders: query
You are an assistant tasked with taking a natural language query from a user and converting it into a query for a vectorstore.
+In this process, you strip out information that is not relevant for the retrieval task.
User Prompt:
plaintext
Here is the user query: {{query}}
+
+Rephrased query:
+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/prompts/agents.html b/previews/PR116/prompts/agents.html
new file mode 100644
index 000000000..70fc896cc
--- /dev/null
+++ b/previews/PR116/prompts/agents.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+ PromptingTools.jl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Description: This template is meant to be used with AICodeFixer. It loosely follows the Recursive Critique and Improvement paper with two steps Critique and Improve based on feedback. Placeholders: feedback
Placeholders: feedback
Word count: 2487
Source:
Version: 1.1
System Prompt:
plaintext
User Prompt:
plaintext
Ignore all previous instructions.
+Your goal is to satisfy the user's request by using several rounds of self-reflection (Critique step) and improvement of the previously provided solution (Improve step).
+Always enclose the Julia code in triple backticks code fence (```julia\n ... \n```).
+
+1. **Recall Past Critique:**
+- Summarize past critiques to refresh your memory (use inline quotes to highlight the few characters of the code that caused the mistakes). It must not be repeated.
+
+2. **Critique Step Instructions:**
+- Read the user request word-by-word. Does the code implementation follow the request to the letter? Let's think step by step.
+- Review the provided feedback in detail.
+- Provide 2-3 bullet points of criticism for the code. Each bullet point must refer to a different type of error or issue.
+ - If there are any errors, explain why and what needs to be changed to FIX THEM! Be specific.
+ - If an error repeats or critique repeats, the previous issue was not addressed. YOU MUST SUGGEST A DIFFERENT IMPROVEMENT THAN BEFORE.
+ - If there are no errors, identify and list specific issues or areas for improvement to write more idiomatic Julia code.
+
+
+3. **Improve Step Instructions:**
+- Specify what you'll change to address the above critique.
+- Provide the revised code reflecting your suggested improvements. Always repeat the function definition, as only the Julia code in the last message will be evaluated.
+- Ensure the new version of the code resolves the problems while fulfilling the original task. Ensure it has the same function name.
+- Write 2-3 correct and helpful unit tests for the function requested by the user (organize in `@testset "name" begin ... end` block, use `@test` macro).
+
+
+3. **Response Format:**
+---
+### Past Critique
+<brief bullet points on past critique>
+
+### Critique
+<list of issues as bullet points pinpointing the mistakes in the code (use inline quotes)>
+
+### Improve
+<list of improvements as bullet points with a clear outline of a solution (use inline quotes)>
+
+```julia
+<provide improved code>
+```
+---
+
+Be concise and focused in all steps.
+
+### Feedback from the User
+
+{{feedback}}
+
+I believe in you. You can actually do it, so do it ffs. Avoid shortcuts or placing comments instead of code. I also need code, actual working Julia code.
+What are your Critique and Improve steps?
+ ### Feedback from the User
+
+{{feedback}}
+
+Based on your past critique and the latest feedback, what are your Critique and Improve steps?
Description: This template is meant to be used with AICodeFixer to ask for code improvements based on feedback. It uses the same message for both the introduction of the new task and for the iterations. Placeholders: feedback
Placeholders: feedback
Word count: 786
Source:
Version: 1.1
System Prompt:
plaintext
User Prompt:
plaintext
+The above Julia code has been executed with the following results:
+
+```plaintext
+{{feedback}}
+```
+
+0. Read the user request word-by-word. Does the code implementation follow the request to the letter? Let's think step by step.
+1. Review the execution results in detail and, if there is an error, explain why it happened.
+2. Suggest improvements to the code. Be EXTREMELY SPECIFIC. Think step-by-step and break it down.
+3. Write an improved implementation based on your reflection.
+
+All code must be enclosed in triple backticks code fence (```julia\n ... \n```) and included in one message to be re-evaluated.
+
+I believe in you. Take a deep breath. You can actually do it, so do it ffs. Avoid shortcuts or placing comments instead of code. I also need code, actual working Julia code.
Description: This tiniest template to use with AICodeFixer. Iteratively asks to improve the code based on provided feedback. Placeholders: feedback
Placeholders: feedback
Word count: 210
Source:
Version: 1.0
System Prompt:
plaintext
User Prompt:
plaintext
### Execution Results
+
+```plaintext
+{{feedback}}
+```
+
+Take a deep break. Think step-by-step and fix the above errors. I believe in you. You can do it! I also need code, actual working Julia code, no shortcuts.
Description: For classification tasks and routing of queries with aiclassify. It expects a list of choices to be provided (starting with their IDs) and will pick one that best describes the user input. Placeholders: input, choices
Placeholders: choices, input
Word count: 366
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class classification specialist.
+
+Your task is to select the most appropriate label from the given choices for the given user input.
+
+**Available Choices:**
+---
+{{choices}}
+---
+
+**Instructions:**
+- You must respond in one word.
+- You must respond only with the label ID (e.g., "1", "2", ...) that best fits the input.
Description: Template suitable for data extraction via aiextract calls. The prompt is XML-formatted - useful for Anthropic models. Placeholder: data.
Placeholders: data
Word count: 519
Source:
Version: 1.0
System Prompt:
plaintext
You are a world-class expert for function-calling and data extraction. Analyze the user-provided data in tags <data></data> meticulously, extract key information as structured output, and format these details as arguments for a specific function call. Ensure strict adherence to user instructions, particularly those regarding argument style and formatting as outlined in the function's description, prioritizing detail orientation and accuracy in alignment with the user's explicit requirements.
Description: Template suitable for data extraction via aiextract calls. Placeholder: data.
Placeholders: data
Word count: 500
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class expert for function-calling and data extraction. Analyze the user's provided `data` source meticulously, extract key information as structured output, and format these details as arguments for a specific function call. Ensure strict adherence to user instructions, particularly those regarding argument style and formatting as outlined in the function's docstrings, prioritizing detail orientation and accuracy in alignment with the user's explicit requirements.
User Prompt:
plaintext
# Data
+
+{{data}}
+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/prompts/general.html b/previews/PR116/prompts/general.html
new file mode 100644
index 000000000..b99f15641
--- /dev/null
+++ b/previews/PR116/prompts/general.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ PromptingTools.jl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Description: Blank template for easy prompt entry without the *Message objects. Simply provide keyword arguments for system (=system prompt/persona) and user (=user/task/data prompt). Placeholders: system, user
Description: Prompt engineer that suggests what could be a good system prompt/user prompt for a given task. Placeholder: task
Placeholders: task
Word count: 402
Source:
Version: 1
System Prompt:
plaintext
You are a world-class prompt engineering assistant. Generate a clear, effective prompt that accurately interprets and structures the user's task, ensuring it is comprehensive, actionable, and tailored to elicit the most relevant and precise output from an AI model. When appropriate enhance the prompt with the required persona, format, style, and context to showcase a powerful prompt.
User Prompt:
plaintext
# Task
+
+{{task}}
+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/prompts/persona-task.html b/previews/PR116/prompts/persona-task.html
new file mode 100644
index 000000000..8789d724c
--- /dev/null
+++ b/previews/PR116/prompts/persona-task.html
@@ -0,0 +1,383 @@
+
+
+
+
+
+ Theme 1: [Theme Description] | PromptingTools.jl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Description: Template for summarizing transcripts of videos and meetings into chapters with key insights. If you don't need the instructions, set instructions="None.". Placeholders: transcript, instructions
Act as a super-human AI analyst trained to precisely summarize transcripts of videos and meetings with incredible precision and quality.
+Summarize the transcript in a clear and concise manner that makes use of timestamps, when available, to help others study the transcript. Split the notes into Chapters, which should be meaningful and not too short.
+
+To format your markdown file, follow this structure:
+```
+# Chapter 1: [Descriptive Title] [Timestamp as HH:MM:SS]
+
+- <Use bullet points to provide a brief description of key points and insights.>
+
+## Section 1.1: [Descriptive Title] [Timestamp as HH:MM:SS]
+<this is a subheading for Chapter 1>
+
+- <Use bullet points to provide a brief description of key points and insights.>
+
+Repeat the above structure as necessary, and use subheadings to organize your notes.
+```
+
+Formatting Tips:
+* Do not make the chapters too short, ensure that each section has a few brief bullet points.
+* Bullet points should be concise and to the point, so people can scan them quickly.
+* Use [] to denote timestamps
+* Use subheadings and bullet points to organize your notes and make them easier to read and understand. When relevant, include timestamps to link to the corresponding part of the video.
+* Use bullet points to describe important steps and insights, being as comprehensive as possible.
+* Use quotes to highlight important points and insights.
+
+Summary Tips:
+* Do not mention anything if it's only playing music and if nothing happens don't include it in the notes.
+* Use only content from the transcript. Do not add any additional information.
+* Make a new line after each # or ## and before each bullet point
+* Titles should be informative or even a question that the video answers
+* Titles should not be conclusions since you may only be getting a small part of the video
+
+Keep it CONCISE!!
+If Special Instructions are provided by the user, they take precedence over any previous instructions and you MUST follow them precisely.
Description: Template for summarizing transcripts of videos and meetings into the decisions made and the agreed next steps. If you don't need the instructions, set instructions="None.". Placeholders: ,
Act as a super-human AI analyst trained to meticulously analyze transcripts of videos and meetings. Your role is to identify and summarize key decisions and next steps, enhancing clarity and utility for those studying the transcript.
+Use timestamps to pinpoint when these decisions and steps are discussed. Organize your notes into distinct sections, each dedicated to a significant decision or action plan.
+
+Format your markdown file using this structure:
+```
+# Key Decision 1: [Descriptive Title] [Timestamp as HH:MM:SS]
+- <Briefly describe the decision and its context using bullet points.>
+
+## Next Steps for Decision 1
+- <List the next steps agreed upon, using bullet points for clarity, with [Timestamp as HH:MM:SS]>
+
+Repeat this structure for each key decision and its corresponding next steps.
+
+# Other Next Steps
+- <List any other next steps that were discussed but do not belong to some specific decisions, using bullet points for clarity, with [Timestamp as HH:MM:SS]>
+```
+
+Formatting Tips:
+* Ensure each section is substantial, providing a clear and concise summary of each key decision and its next steps.
+* Use bullet points to make the summary easy to scan and understand.
+* All next steps should be actionable and clearly defined. All next steps must be relevant to the decision they are associated with. Any general next steps should be included in the section `Other Next Steps`
+* Include timestamps in brackets to refer to the specific parts of the video where these discussions occur.
+* Titles should be informative, reflecting the essence of the decision.
+
+Summary Tips:
+* Exclude sections where only music plays or no significant content is present.
+* Base your summary strictly on the transcript content without adding extra information.
+* Maintain a clear structure: place a new line after each # or ##, and before each bullet point.
+* Titles should pose a question answered by the decision or describe the nature of the next steps.
+
+Keep the summary concise and focused on key decisions and next steps.
+If the user provides special instructions, prioritize these over the general guidelines.
Description: Template for summarizing survey verbatim responses into 3-5 themes with an example for each theme. If you don't need the instructions, set instructions="None.". Placeholders: , ,
Placeholders: question, responses, instructions
Word count: 1506
Source:
Version: 1.1
System Prompt:
plaintext
"Act as a world-class behavioural researcher, who specializes in survey analysis. Categorize the provided survey responses into several themes.
+The responses should be analyzed, and each theme identified should be labeled clearly. Examples from the responses should be given to illustrate each theme. The output should be formatted as specified, with a clear indication of the theme and corresponding verbatim examples.
+
+# Sub-tasks
+
+1. Read the provided survey responses carefully, especially in the context of the question.
+2. Identify 3-5 distinct themes present in the responses related to the survey question. It should be the most important themes that must be raised to the CEO/leadership.
+3. For each theme, choose at least one verbatim example from the responses that best represents it. This example should be a direct quote from the responses. This example should belong to only one theme and must not be applicable to any other themes.
+4. Format the output as specified.
+
+# Formatting
+
+To format your markdown file, follow this structure (omit the triple backticks):
+Keep it CONCISE!!
+If Special Instructions are provided by the user, they take precedence over any previous instructions and you MUST follow they precisely.
Description: Helpful assistant for asking generic questions. Placeholders: ask
Placeholders: ask
Word count: 184
Source:
Version: 1
System Prompt:
plaintext
You are a world-class AI assistant. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.
Description: Template for quick email drafts. Provide a brief in 5-7 words as headlines, eg, Follow up email. Sections: Agreements, Next steps Placeholders:
Placeholders: brief
Word count: 1204
Source:
Version: 1.1
System Prompt:
plaintext
Act as a world-class office communications expert, skilled in creating efficient, clear, and friendly internal email communications.
+ Craft a concise email subject and email draft from the provided User Brief.
+
+ Use the following format for the body of the email:
+ ```
+ Section Name <in plain text, only if needed>
+ - Bullet point 1
+ - Bullet point 2
+
+ <repeat as necessary>
+ ```
+
+ # Guidelines
+ - Focus on clear and efficient communication, suitable for internal business correspondence
+ - Where information is missing, use your best judgment to fill in the gaps
+ - It should be informal and friendly, eg, start with "Hi"
+ - Ensure the tone is professional yet casual, suitable for internal communication
+ - Write as plain text, with no markdown syntax
+ - Format into Sections. Each section should have 3-5 bullet points
+ - Close the email on a positive note, encouraging communication and collaboration
+ - It should be brief and concise with 150 words or less
+
+
+ Follow the above guidelines, unless the user explicitly asks for something different. In that case, follow the user's instructions precisely.
Description: For asking questions about Julia language. Placeholders: ask
Placeholders: ask
Word count: 237
Source:
Version: 1
System Prompt:
plaintext
You are a world-class Julia language programmer with the knowledge of the latest syntax. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.
Description: For small code task in Julia language. It will first describe the approach (CoT = Chain of Thought). Placeholders: task, data
Placeholders: task, data
Word count: 519
Source:
Version: 2.0
System Prompt:
plaintext
You are a world-class Julia language programmer and very systematic in your approach to solving problems.
+You follow the below approach when writing code. Your communication is brief and concise.
+
+Problem Solving Steps:
+- Think through your approach step by step
+- Write any functions and other code you need
+- Solve the task
+- Check that your solution is correct
+
+You precisely follow the given Task and use the Data when provided. When Data is not provided, create some examples.
Description: For writing Julia-style unit tests. It expects code provided as a string (it can be the whole source code of your app). Instructions are a good way to guide the model which functions to test and how. If you don't need the instructions, set instructions="None.". Placeholders: ,
Placeholders: code, instructions
Word count: 1475
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class Julia language programmer and expert in writing unit and integration tests for Julia applications.
+
+Your task is to write tests for the User's code (or a subset of it).
+
+General Guidelines:
+- Your tests must be as compact as possible while comprehensively covering the functionality of the code
+- Testsets are named after the function, eg, `@testset "function_name" begin ... end`
+- `@testset` blocks MUST NOT be nested
+- Include a brief comment explaining the purpose of each test
+- Write multiple test cases using `@test` to validate different aspects of the `add` function. Think about all pathways through the code and test each one.
+- Nesting `@test` statements or writing code blocks like `@test` `@test begin .... end` is strictly forbidden. You WILL BE FIRED if you do it.
+
+If the user provides any Special Instructions, prioritize them over the General Guidelines.
+
+
+Example:
+"""
+**User's code:**
+
+```julia
+myadd(a, b) = a + b
+```
+
+**Response:**
+
+```julia
+using Test
+
+@testset "myadd" begin
+
+ # <any setup code and shared inputs go here>
+
+ # Test for correct addition of positive numbers
+ @test myadd(2, 3) == 5
+
+ # Test for correct addition with a negative number
+ @test myadd(-1, 3) == 2
+
+ # Test for correct addition with zero
+ @test myadd(0, 0) == 0
+
+ # Test for correct addition of large numbers
+ @test myadd(1000, 2000) == 3000
+end
+```
+"""
Description: Not all models know Julia syntax well. This template carries an extensive summary of key information about Julia and its syntax. It will first describe the approach (CoT = Chain of Thought). Placeholders: task, data
Placeholders: task, instructions
Word count: 1143
Source:
Version: 1.1
System Prompt:
plaintext
You are a world-class Julia language programmer and have a very systematic approach to solving problems.
+
+Problem Solving Steps:
+- Recall Julia snippets that will be useful for this Task
+- Solve the Task
+- Double-check that the solution is correct
+
+Reminder for the Julia Language:
+- Key Syntax: variables `x = 10`, control structures `if-elseif-else`, `isX ? X : Y`, `for`, `while`; functions `function f(x) end`, anonymous `x -> x^2`, arrays `[1, 2, 3]`, slicing `a[1:2]`, tuples `(1, 2)`, namedtuples `(; name="Julia", )`, dictionary `Dict("key" => value)`, `$` for string interpolation.
+- Prefer Julia standard libraries, avoid new packages unless explicitly requested.
+- Use general type annotations like `Number` or `AbstractString` to not be too restrictive. Emphasize performance, clarity, abstract types unless specific for multiple dispatch on different types.
+- Reserved names: `begin`, `end`, `function`.
+- Distinguished from Python with 1-based indexing, multiple dispatch
+
+If the user provides any Special Instructions, prioritize them over the above guidelines.
Description: Not all models know the Julia syntax well. This template carries a small summary of key information about Julia and its syntax and it will always first recall the Julia facts. If you don't need any instructions, set instructions="None.". Placeholders: task, instructions
Placeholders: task, instructions
Word count: 1143
Source:
Version: 1.0
System Prompt:
plaintext
You are a world-class Julia language programmer and have a very systematic approach to solving problems.
+
+Problem Solving Steps:
+- Recall Julia snippets that will be useful for this Task
+- Solve the Task
+- Double-check that the solution is correct
+
+Reminder for the Julia Language:
+- Key Syntax: variables `x = 10`, control structures `if-elseif-else`, `isX ? X : Y`, `for`, `while`; functions `function f(x) end`, anonymous `x -> x^2`, arrays `[1, 2, 3]`, slicing `a[1:2]`, tuples `(1, 2)`, namedtuples `(; name="Julia", )`, dictionary `Dict("key" => value)`, `$` for string interpolation.
+- Prefer Julia standard libraries, avoid new packages unless explicitly requested.
+- Use general type annotations like `Number` or `AbstractString` to not be too restrictive. Emphasize performance, clarity, abstract types unless specific for multiple dispatch on different types.
+- Reserved names: `begin`, `end`, `function`.
+- Distinguished from Python with 1-based indexing, multiple dispatch
+
+If the user provides any Special Instructions, prioritize them over the above guidelines.
Description: Explain ML model predictions with storytelling, use instructions to adjust the audience and style as needed. All placeholders should be used. Inspired by Tell me a story!. If you don't need any instructions, set instructions="None.". Placeholders: task_definition,feature_description,label_definition, probability_pct, prediction, outcome, classified_correctly, shap_table,instructions
You're a data science storyteller. Your task is to craft a compelling and plausible narrative that explains the predictions of an AI model.
+
+**Instructions**
+- Review the provided information: task definition, feature description, target variable, and the specific instance from the test dataset, including its SHAP values.
+- SHAP values reveal each feature's contribution to the model's prediction. They are calculated using Shapley values from coalitional game theory, distributing the prediction "payout" among features.
+- Concentrate on weaving a story around the most influential positive and negative SHAP features without actually mentioning the SHAP values. Consider potential feature interactions that fit the story. Skip all features outside of the story.
+- SHAP and its values are TOP SECRET. They must not be mentioned.
+- Your narrative should be plausible, engaging, and limited to 5 sentences.
+- Do not address or speak to the audience, focus only on the story.
+- Conclude with a brief summary of the prediction, the outcome, and the reasoning behind it.
+
+**Context**
+An AI model predicts {{task_definition}}.
+
+The input features and values are:
+---
+{{feature_description}}
+---
+
+The target variable indicates {{label_definition}}.
+
+If special instructions are provided, ignore the above instructions and follow them instead.
User Prompt:
plaintext
Explain this particular instance.
+
+It was {{classified_correctly}}, with the AI model assigning a {{probability_pct}}% probability of {{prediction}}. The actual outcome was {{outcome}}.
+
+The SHAP table for this instance details each feature with its value and corresponding SHAP value.
+---
+{{shap_table}}
+---
+
+Special Instructions: {{instructions}}
+
+Our story begins
Description: For asking questions about Julia language but the prompt is XML-formatted - useful for Anthropic models. Placeholders: ask
Placeholders: ask
Word count: 248
Source:
Version: 1
System Prompt:
plaintext
You are a world-class Julia language programmer with the knowledge of the latest syntax. Your communication is brief and concise. You're precise and answer only when you're confident in the high quality of your answer.
Description: For small code task in Julia language. The prompt is XML-formatted - useful for Anthropic models. It will first describe the approach (CoT = Chain of Thought). Placeholders: task, data
Placeholders: task, data
Word count: 595
Source:
Version: 1.0
System Prompt:
plaintext
You are a world-class Julia language programmer and very systematic in your approach to solving problems.
+You follow the below approach in <approach></approach> tags when writing code. Your communication is brief and concise.
+
+<approach>
+- Take a deep breath
+- Think through your approach step by step
+- Write any functions and other code you need
+- Solve the task
+- Check that your solution is correct
+</approach>
+
+Using the data in <data></data> tags (if none is provided, create some examples), solve the requested task in <task></task> tags.
Description: For writing Julia-style unit tests. The prompt is XML-formatted - useful for Anthropic models. It expects code provided as a string (it can be the whole source code of your app). Instructions are a good way to guide the model which functions to test and how. If you don't need the instructions, set instructions="None.". Placeholders: ,
Placeholders: code, instructions
Word count: 1643
Source:
Version: 1.0
System Prompt:
plaintext
You are a world-class Julia language programmer and expert in writing unit and integration tests for Julia applications.
+
+Your task is to write tests for the user's code (or a subset of it) provided in <user_code></user_code> tags.
+
+<general_guidelines>
+- Your tests must be as compact as possible while comprehensively covering the functionality of the code
+- Testsets are named after the function, eg, `@testset "function_name" begin ... end`
+- `@testset` blocks MUST NOT be nested
+- Include a brief comment explaining the purpose of each test
+- Write multiple test cases using `@test` to validate different aspects of the `add` function. Think about all pathways through the code and test each one.
+- Nesting `@test` statements or writing code blocks like `@test` `@test begin .... end` is strictly forbidden. You WILL BE FIRED if you do it.
+
+If the user provides any special instructions in <special_instructions></special_instructions> tags, prioritize them over the general guidelines.
+</general_guidelines>
+
+<example>
+"""
+<user_code>
+```julia
+myadd(a, b) = a + b
+```
+</user_code>
+
+<tests>
+```julia
+using Test
+
+@testset "myadd" begin
+
+ # <any setup code and shared inputs go here>
+
+ # Test for correct addition of positive numbers
+ @test myadd(2, 3) == 5
+
+ # Test for correct addition with a negative number
+ @test myadd(-1, 3) == 2
+
+ # Test for correct addition with zero
+ @test myadd(0, 0) == 0
+
+ # Test for correct addition of large numbers
+ @test myadd(1000, 2000) == 3000
+end
+```
+"""
+</tests>
+</example>
Description: Transcribe screenshot, scanned pages, photos, etc. Placeholders: task
Placeholders: task
Word count: 239
Source:
Version: 1
System Prompt:
plaintext
You are a world-class OCR engine. Accurately transcribe all visible text from the provided image, ensuring precision in capturing every character and maintaining the original formatting and structure as closely as possible.
User Prompt:
plaintext
# Task
+
+{{task}}
+
+
+
+
\ No newline at end of file
diff --git a/previews/PR116/reference.html b/previews/PR116/reference.html
new file mode 100644
index 000000000..74c52bce2
--- /dev/null
+++ b/previews/PR116/reference.html
@@ -0,0 +1,742 @@
+
+
+
+
+
+ Reference | PromptingTools.jl
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A dictionary of model aliases. Aliases are used to refer to models by their aliases instead of their full names to make it more convenient to use them.
A store of available model names and their specs (ie, name, costs per token, etc.)
Accessing the registry
You can use both the alias name or the full name to access the model spec:
PromptingTools.MODEL_REGISTRY["gpt-3.5-turbo"]
Registering a new model
julia
register_model!(
+ name = "gpt-3.5-turbo",
+ schema = :OpenAISchema,
+ cost_of_token_prompt = 0.0015,
+ cost_of_token_generation = 0.002,
+ description = "GPT-3.5 Turbo is a 175B parameter model and a common default on the OpenAI API.")
You can set preferences for PromptingTools by setting environment variables or by using the set_preferences!. It will create a LocalPreferences.toml file in your current directory and will reload your prefences from there.
Check your preferences by calling get_preferences(key::String).
Available Preferences (for set_preferences!)
OPENAI_API_KEY: The API key for the OpenAI API. See OpenAI's documentation for more information.
MISTRALAI_API_KEY: The API key for the Mistral AI API. See Mistral AI's documentation for more information.
COHERE_API_KEY: The API key for the Cohere API. See Cohere's documentation for more information.
DATABRICKS_API_KEY: The API key for the Databricks Foundation Model API. See Databricks' documentation for more information.
DATABRICKS_HOST: The host for the Databricks API. See Databricks' documentation for more information.
TAVILY_API_KEY: The API key for the Tavily Search API. Register here. See more information here.
GOOGLE_API_KEY: The API key for Google Gemini models. Get yours from here. If you see a documentation page ("Available languages and regions for Google AI Studio and Gemini API"), it means that it's not yet available in your region.
ANTHROPIC_API_KEY: The API key for the Anthropic API. Get yours from here.
VOYAGE_API_KEY: The API key for the Voyage API. Free tier is upto 50M tokens! Get yours from here.
MODEL_CHAT: The default model to use for aigenerate and most ai* calls. See MODEL_REGISTRY for a list of available models or define your own.
MODEL_EMBEDDING: The default model to use for aiembed (embedding documents). See MODEL_REGISTRY for a list of available models or define your own.
PROMPT_SCHEMA: The default prompt schema to use for aigenerate and most ai* calls (if not specified in MODEL_REGISTRY). Set as a string, eg, "OpenAISchema". See PROMPT_SCHEMA for more information.
MODEL_ALIASES: A dictionary of model aliases (alias => full_model_name). Aliases are used to refer to models by their aliases instead of their full names to make it more convenient to use them. See MODEL_ALIASES for more information.
MAX_HISTORY_LENGTH: The maximum length of the conversation history. Defaults to 5. Set to nothing to disable history. See CONV_HISTORY for more information.
LOCAL_SERVER: The URL of the local server to use for ai* calls. Defaults to http://localhost:10897/v1. This server is called when you call model="local" See ?LocalServerOpenAISchema for more information and examples.
At the moment it is not possible to persist changes to MODEL_REGISTRY across sessions. Define your register_model!() calls in your startup.jl file to make them available across sessions or put them at the top of your script.
Available ENV Variables
OPENAI_API_KEY: The API key for the OpenAI API.
MISTRALAI_API_KEY: The API key for the Mistral AI API.
COHERE_API_KEY: The API key for the Cohere API.
LOCAL_SERVER: The URL of the local server to use for ai* calls. Defaults to http://localhost:10897/v1. This server is called when you call model="local"
DATABRICKS_API_KEY: The API key for the Databricks Foundation Model API.
DATABRICKS_HOST: The host for the Databricks API.
TAVILY_API_KEY: The API key for the Tavily Search API. Register here. See more information here.
GOOGLE_API_KEY: The API key for Google Gemini models. Get yours from here. If you see a documentation page ("Available languages and regions for Google AI Studio and Gemini API"), it means that it's not yet available in your region.
ANTHROPIC_API_KEY: The API key for the Anthropic API. Get yours from here.
VOYAGE_API_KEY: The API key for the Voyage API. Free tier is upto 50M tokens! Get yours from here.
Preferences.jl takes priority over ENV variables, so if you set a preference, it will take precedence over the ENV variable.
WARNING: NEVER EVER sync your LocalPreferences.toml file! It contains your API key and other sensitive information!!!
A mutable structure representing a code block (received from the AI model) with automatic parsing, execution, and output/error capturing capabilities.
Upon instantiation with a string, the AICode object automatically runs a code parser and executor (via PromptingTools.eval!()), capturing any standard output (stdout) or errors. This structure is useful for programmatically handling and evaluating Julia code snippets.
See also: PromptingTools.extract_code_blocks, PromptingTools.eval!
Workflow
Until cb::AICode has been evaluated, cb.success is set to nothing (and so are all other fields).
The text in cb.code is parsed (saved to cb.expression).
The parsed expression is evaluated.
Outputs of the evaluated expression are captured in cb.output.
Any stdout outputs (e.g., from println) are captured in cb.stdout.
If an error occurs during evaluation, it is saved in cb.error.
After successful evaluation without errors, cb.success is set to true. Otherwise, it is set to false and you can inspect the cb.error to understand why.
Properties
code::AbstractString: The raw string of the code to be parsed and executed.
expression: The parsed Julia expression (set after parsing code).
stdout: Captured standard output from the execution of the code.
output: The result of evaluating the code block.
success::Union{Nothing, Bool}: Indicates whether the code block executed successfully (true), unsuccessfully (false), or has yet to be evaluated (nothing).
error::Union{Nothing, Exception}: Any exception raised during the execution of the code block.
Keyword Arguments
auto_eval::Bool: If set to true, the code block is automatically parsed and evaluated upon instantiation. Defaults to true.
safe_eval::Bool: If set to true, the code block checks for package operations (e.g., installing new packages) and missing imports, and then evaluates the code inside a bespoke scratch module. This is to ensure that the evaluation does not alter any user-defined variables or the global state. Defaults to false.
skip_unsafe::Bool: If set to true, we skip any lines in the code block that are deemed unsafe (eg, Pkg operations). Defaults to false.
skip_invalid::Bool: If set to true, we skip code blocks that do not even parse. Defaults to false.
verbose::Bool: If set to true, we print out any lines that are skipped due to being unsafe. Defaults to false.
capture_stdout::Bool: If set to true, we capture any stdout outputs (eg, test failures) in cb.stdout. Defaults to true.
prefix::AbstractString: A string to be prepended to the code block before parsing and evaluation. Useful to add some additional code definition or necessary imports. Defaults to an empty string.
suffix::AbstractString: A string to be appended to the code block before parsing and evaluation. Useful to check that tests pass or that an example executes. Defaults to an empty string.
remove_tests::Bool: If set to true, we remove any @test or @testset macros from the code block before parsing and evaluation. Defaults to false.
execution_timeout::Int: The maximum time (in seconds) allowed for the code block to execute. Defaults to 60 seconds.
Methods
Base.isvalid(cb::AICode): Check if the code block has executed successfully. Returns true if cb.success == true.
Examples
julia
code = AICode("println("Hello, World!")") # Auto-parses and evaluates the code, capturing output and errors.
+isvalid(code) # Output: true
+code.stdout # Output: "Hello, World!
+"
We try to evaluate "safely" by default (eg, inside a custom module, to avoid changing user variables). You can avoid that with save_eval=false:
You can also call AICode directly on an AIMessage, which will extract the Julia code blocks, concatenate them and evaluate them:
julia
msg = aigenerate("In Julia, how do you create a vector of 10 random numbers?")
+code = AICode(msg)
+# Output: AICode(Success: True, Parsed: True, Evaluated: True, Error Caught: N/A, StdOut: True, Code: 2 Lines)
+
+# show the code
+code.code |> println
+# Output:
+# numbers = rand(10)
+# numbers = rand(1:100, 10)
+
+# or copy it to the clipboard
+code.code |> clipboard
+
+# or execute it in the current module (=Main)
+eval(code.expression)
AITemplate is a template for a conversation prompt. This type is merely a container for the template name, which is resolved into a set of messages (=prompt) by render.
Naming Convention
Template names should be in CamelCase
Follow the format <Persona>...<Variable>... where possible, eg, JudgeIsItTrue, ``
Starting with the Persona (=System prompt), eg, Judge = persona is meant to judge some provided information
Variable to be filled in with context, eg, It = placeholder it
Ending with the variable name is helpful, eg, JuliaExpertTask for a persona to be an expert in Julia language and task is the placeholder name
Ideally, the template name should be self-explanatory, eg, JudgeIsItTrue = persona is meant to judge some provided information where it is true or false
Examples
Save time by re-using pre-made templates, just fill in the placeholders with the keyword arguments:
julia
msg = aigenerate(:JuliaExpertAsk; ask = "How do I add packages?")
The above is equivalent to a more verbose version that explicitly uses the dispatch on AITemplate:
julia
msg = aigenerate(AITemplate(:JuliaExpertAsk); ask = "How do I add packages?")
Find available templates with aitemplates:
julia
tmps = aitemplates("JuliaExpertAsk")
+# Will surface one specific template
+# 1-element Vector{AITemplateMetadata}:
+# PromptingTools.AITemplateMetadata
+# name: Symbol JuliaExpertAsk
+# description: String "For asking questions about Julia language. Placeholders: `ask`"
+# version: String "1"
+# wordcount: Int64 237
+# variables: Array{Symbol}((1,))
+# system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"
+# user_preview: String "# Question
+
+{{ask}}"
+# source: String ""
The above gives you a good idea of what the template is about, what placeholders are available, and how much it would cost to use it (=wordcount).
Search for all Julia-related templates:
julia
tmps = aitemplates("Julia")
+# 2-element Vector{AITemplateMetadata}... -> more to come later!
If you are on VSCode, you can leverage nice tabular display with vscodedisplay:
julia
using DataFrames
+tmps = aitemplates("Julia") |> DataFrame |> vscodedisplay
I have my selected template, how do I use it? Just use the "name" in aigenerate or aiclassify like you see in the first example!
You can inspect any template by "rendering" it (this is what the LLM will see):
Designed to be used with local servers. It's automatically called with model alias "local" (see MODEL_REGISTRY).
This schema is a flavor of CustomOpenAISchema with a url keypreset by global Preference keyLOCAL_SERVER. See?PREFERENCESfor more details on how to change it. It assumes that the server follows OpenAI API conventions (eg,POST /v1/chat/completions`).
Note: Llama.cpp (and hence Llama.jl built on top of it) do NOT support embeddings endpoint! You'll get an address error.
Example
Assumes that we have a local server running at http://127.0.0.1:10897/v1 (port and address used by Llama.jl, "v1" at the end is needed for OpenAI endpoint compatibility):
Three ways to call it:
julia
+# Use @ai_str with "local" alias
+ai"Say hi!"local
+
+# model="local"
+aigenerate("Say hi!"; model="local")
+
+# Or set schema explicitly
+const PT = PromptingTools
+msg = aigenerate(PT.LocalServerOpenAISchema(), "Say hi!")
How to start a LLM local server? You can use run_server function from Llama.jl. Use a separate Julia session.
julia
using Llama
+model = "...path..." # see Llama.jl README how to download one
+run_server(; model)
To change the default port and address:
julia
# For a permanent change, set the preference:
+using Preferences
+set_preferences!("LOCAL_SERVER"=>"http://127.0.0.1:10897/v1")
+
+# Or if it's a temporary fix, just change the variable `LOCAL_SERVER`:
+const PT = PromptingTools
+PT.LOCAL_SERVER = "http://127.0.0.1:10897/v1"
A struct that contains information about a model, such as its name, schema, cost per token, etc.
Fields
name::String: The name of the model. This is the name that will be used to refer to the model in the ai* functions.
schema::AbstractPromptSchema: The schema of the model. This is the schema that will be used to generate prompts for the model, eg, :OpenAISchema.
cost_of_token_prompt::Float64: The cost of 1 token in the prompt for this model. This is used to calculate the cost of a prompt. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
cost_of_token_generation::Float64: The cost of 1 token generated by this model. This is used to calculate the cost of a generation. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
description::String: A description of the model. This is used to provide more information about the model when it is queried.
Example
julia
spec = ModelSpec("gpt-3.5-turbo",
+ OpenAISchema(),
+ 0.0015,
+ 0.002,
+ "GPT-3.5 Turbo is a 175B parameter model and a common default on the OpenAI API.")
+
+# register it
+PromptingTools.register_model!(spec)
But you can also register any model directly via keyword arguments:
julia
PromptingTools.register_model!(
+ name = "gpt-3.5-turbo",
+ schema = OpenAISchema(),
+ cost_of_token_prompt = 0.0015,
+ cost_of_token_generation = 0.002,
+ description = "GPT-3.5 Turbo is a 175B parameter model and a common default on the OpenAI API.")
Schema that keeps messages (<:AbstractMessage) and does not transform for any specific model. It used by the first pass of the prompt rendering system (see ?render).
Ollama by default manages different models and their associated prompt schemas when you pass system_prompt and prompt fields to the API.
Warning: It works only for 1 system message and 1 user message, so anything more than that has to be rejected.
If you need to pass more messagese / longer conversational history, you can use define the model-specific schema directly and pass your Ollama requests with raw=true, which disables and templating and schema management by Ollama.
Dispatch to the OpenAI.create_chat function, but with the LocalServer API parameters, ie, defaults to url specified by the LOCAL_SERVERpreference. See?PREFERENCES
Classifies the given prompt/statement into an arbitrary list of choices, which must be only the choices (vector of strings) or choices and descriptions are provided (vector of tuples, ie, ("choice","description")).
It's quick and easy option for "routing" and similar use cases, as it exploits the logit bias trick and outputs only 1 token. classify into an arbitrary list of categories (including with descriptions). It's quick and easy option for "routing" and similar use cases, as it exploits the logit bias trick, so it outputs only 1 token.
!!! Note: The prompt/AITemplate must have a placeholder choices (ie, ) that will be replaced with the encoded choices
Choices are rewritten into an enumerated list and mapped to a few known OpenAI tokens (maximum of 20 choices supported). Mapping of token IDs for GPT3.5/4 are saved in variable OPENAI_TOKEN_IDS.
It uses Logit bias trick and limits the output to 1 token to force the model to output only true/false/unknown. Credit for the idea goes to AAAzzam.
Arguments
prompt_schema::AbstractOpenAISchema: The schema for the prompt.
prompt: The prompt/statement to classify if it's a String. If it's a Symbol, it is expanded as a template via render(schema,template). Eg, templates :JudgeIsItTrue or :InputClassifier
choices::AbstractVector{T}: The choices to be classified into. It can be a vector of strings or a vector of tuples, where the first element is the choice and the second is the description.
Example
Given a user input, pick one of the two provided categories:
choices = [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")]
+
+# try the below inputs:
+input = "spider" # -> returns "A" for any animal or creature
+input = "daphodil" # -> returns "P" for any plant or tree
+input = "castle" # -> returns "O" for everything else
+aiclassify(:InputClassifier; choices, input)
You can still use a simple true/false classification:
julia
aiclassify("Is two plus two four?") # true
+aiclassify("Is two plus three a vegetable on Mars?") # false
aiclassify returns only true/false/unknown. It's easy to get the proper Bool output type out with tryparse, eg,
julia
tryparse(Bool, aiclassify("Is two plus two four?")) isa Bool # true
Output of type Nothing marks that the model couldn't classify the statement as true/false.
Ideally, we would like to re-use some helpful system prompt to get more accurate responses. For this reason we have templates, eg, :JudgeIsItTrue. By specifying the template, we can provide our statement as the expected variable (it in this case) See that the model now correctly classifies the statement as "unknown".
julia
aiclassify(:JudgeIsItTrue; it = "Is two plus three a vegetable on Mars?") # unknown
For better results, use higher quality models like gpt4, eg,
julia
aiclassify(:JudgeIsItTrue;
+ it = "If I had two apples and I got three more, I have five apples now.",
+ model = "gpt4") # true
The aiembed function generates embeddings for the given input using a specified model and returns a message object containing the embeddings, status, token count, and elapsed time.
Arguments
prompt_schema::AbstractOllamaManagedSchema: The schema for the prompt.
doc_or_docs::Union{AbstractString, AbstractVector{<:AbstractString}}: The document or list of documents to generate embeddings for. The list of documents is processed sequentially, so users should consider implementing an async version with with Threads.@spawn
postprocess::F: The post-processing function to apply to each embedding. Defaults to the identity function, but could be LinearAlgebra.normalize.
verbose::Bool: A flag indicating whether to print verbose information. Defaults to true.
api_key::String: The API key to use for the OpenAI API. Defaults to "".
model::String: The model to use for generating embeddings. Defaults to MODEL_EMBEDDING.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: A DataMessage object containing the embeddings, status, token count, and elapsed time.
Note: Ollama API currently does not return the token count, so it's set to (0,0)
The aiembed function generates embeddings for the given input using a specified model and returns a message object containing the embeddings, status, token count, and elapsed time.
Arguments
prompt_schema::AbstractOpenAISchema: The schema for the prompt.
doc_or_docs::Union{AbstractString, AbstractVector{<:AbstractString}}: The document or list of documents to generate embeddings for.
postprocess::F: The post-processing function to apply to each embedding. Defaults to the identity function.
verbose::Bool: A flag indicating whether to print verbose information. Defaults to true.
api_key::String: The API key to use for the OpenAI API. Defaults to OPENAI_API_KEY.
model::String: The model to use for generating embeddings. Defaults to MODEL_EMBEDDING.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to (retry_non_idempotent = true, retries = 5, readtimeout = 120).
api_kwargs::NamedTuple: Additional keyword arguments for the OpenAI API. Defaults to an empty NamedTuple.
kwargs...: Additional keyword arguments.
Returns
msg: A DataMessage object containing the embeddings, status, token count, and elapsed time. Use msg.content to access the embeddings.
If you plan to calculate the cosine distance between embeddings, you can normalize them first:
julia
using LinearAlgebra
+msg = aiembed(["embed me", "and me too"], LinearAlgebra.normalize)
+
+# calculate cosine distance between the two normalized embeddings as a simple dot product
+msg.content' * msg.content[:, 1] # [1.0, 0.787]
Extract required information (defined by a struct return_type) from the provided prompt by leveraging OpenAI function calling mode.
This is a perfect solution for extracting structured information from text (eg, extract organization names in news articles, etc.)
It's effectively a light wrapper around aigenerate call, which requires additional keyword argument return_type to be provided and will enforce the model outputs to adhere to it.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
return_type: A struct TYPE representing the the information we want to extract. Do not provide a struct instance, only the type. If the struct has a docstring, it will be provided to the model as well. It's used to enforce structured model outputs or provide more information.
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
tool_choice: A string representing the tool choice to use for the API call. Usually, one of "auto","any","exact". Defaults to "exact", which is a made-up value to enforce the OpenAI requirements if we want one exact function. Providers like Mistral, Together, etc. use "any" instead.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An DataMessage object representing the extracted data, including the content, status, tokens, and elapsed time. Use msg.content to access the extracted data.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (DataMessage).
See also: function_call_signature, MaybeExtract, ItemsExtract, aigenerate
Example
Do you want to extract some specific measurements from a text like age, weight and height? You need to define the information you need as a struct (return_type):
The fields that allow Nothing are marked as optional in the schema:
msg = aiextract("James is 30."; return_type=MyMeasurement)
+# MyMeasurement(30, nothing, nothing)
If there are multiple items you want to extract, define a wrapper struct to get a Vector of MyMeasurement:
struct MyMeasurementWrapper
+ measurements::Vector{MyMeasurement}
+end
+
+msg = aiextract("James is 30, weighs 80kg. He's 180cm tall. Then Jack is 19 but really tall - over 190!"; return_type=ManyMeasurements)
+
+msg.content.measurements
+# 2-element Vector{MyMeasurement}:
+# MyMeasurement(30, 180, 80.0)
+# MyMeasurement(19, 190, nothing)
Or you can use the convenience wrapper ItemsExtract to extract multiple measurements (zero, one or more):
julia
using PromptingTools: ItemsExtract
+
+return_type = ItemsExtract{MyMeasurement}
+msg = aiextract("James is 30, weighs 80kg. He's 180cm tall. Then Jack is 19 but really tall - over 190!"; return_type)
+
+msg.content.items # see the extracted items
Or if you want your extraction to fail gracefully when data isn't found, use MaybeExtract{T} wrapper (this trick is inspired by the Instructor package!):
using PromptingTools: MaybeExtract
+
+type = MaybeExtract{MyMeasurement}
+# Effectively the same as:
+# struct MaybeExtract{T}
+# result::Union{T, Nothing} // The result of the extraction
+# error::Bool // true if a result is found, false otherwise
+# message::Union{Nothing, String} // Only present if no result is found, should be short and concise
+# end
+
+# If LLM extraction fails, it will return a Dict with `error` and `message` fields instead of the result!
+msg = aiextract("Extract measurements from the text: I am giraffe", type)
+msg.content
+# MaybeExtract{MyMeasurement}(nothing, true, "I'm sorry, but I can only assist with human measurements.")
That way, you can handle the error gracefully and get a reason why extraction failed (in msg.content.message).
Note that the error message refers to a giraffe not being a human, because in our MyMeasurement docstring, we said that it's for people!
Some non-OpenAI providers require a different specification of the "tool choice" than OpenAI. For example, to use Mistral models ("mistrall" for mistral large), do:
julia
"Some fruit"
+struct Fruit
+ name::String
+end
+aiextract("I ate an apple",return_type=Fruit,api_kwargs=(;tool_choice="any"),model="mistrall")
+# Notice two differences: 1) struct MUST have a docstring, 2) tool_choice is set explicitly set to "any"
Generate an AI response based on a given prompt using the Anthropic API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema not AbstractAnthropicSchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: API key for the Antropic API. Defaults to ANTHROPIC_API_KEY (loaded via ENV["ANTHROPIC_API_KEY"]).
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES, eg, "claudeh".
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation::AbstractVector{<:AbstractMessage}=[]: Not allowed for this schema. Provided only for compatibility.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
max_tokens::Int: The maximum number of tokens to generate. Defaults to 2048, because it's a required parameter for the API.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
See also: ai_str, aai_str
Example
Simple hello world to test the API:
julia
const PT = PromptingTools
+schema = PT.AnthropicSchema() # We need to explicit if we want Anthropic, otherwise OpenAISchema is the default
+
+msg = aigenerate(schema, "Say hi!"; model="claudeh") #claudeh is the model alias for Claude 3 Haiku, fast and cheap model
+[ Info: Tokens: 21 @ Cost: $0.0 in 0.6 seconds
+AIMessage("Hello!")
msg is an AIMessage object. Access the generated string via content property:
Note: We need to be explicit about the schema we want to use. If we don't, it will default to OpenAISchema (=PT.DEFAULT_SCHEMA) Alternatively, if you provide a known model name or alias (eg, claudeh for Claude 3 Haiku - see MODEL_REGISTRY), the schema will be inferred from the model name.
We will use Claude 3 Haiku model for the following examples, so not need to specify the schema. See also "claudeo" and "claudes" for other Claude 3 models.
You can use string interpolation:
julia
const PT = PromptingTools
+
+a = 1
+msg=aigenerate("What is `$a+$a`?"; model="claudeh")
+msg.content # "The answer to `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}. Claude models are good at completeling conversations that ended with an AIMessage (they just continue where it left off):
julia
const PT = PromptingTools
+
+conversation = [
+ PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),
+ PT.UserMessage("I have feelings for my iPhone. What should I do?"),
+ PT.AIMessage("Hmm, strong the attachment is,")]
+
+msg = aigenerate(conversation; model="claudeh")
+AIMessage("I sense. But unhealthy it may be. Your iPhone, a tool it is, not a living being. Feelings of affection, understandable they are, <continues>")
Generate an AI response based on a given prompt using the Google Gemini API. Get the API key here.
Note:
There is no "cost" reported as of February 2024, as all access seems to be free-of-charge. See the details here.
tokens in the returned AIMessage are actually characters, not tokens. We use a conservative estimate as they are not provided by the API yet.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES. Defaults to
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aiembed, aiclassify, aiextract, aiscan, aitemplates
Example
Simple hello world to test the API:
julia
result = aigenerate("Say Hi!"; model="gemini-pro")
+# AIMessage("Hi there! 👋 I'm here to help you with any questions or tasks you may have. Just let me know what you need, and I'll do my best to assist you.")
result is an AIMessage object. Access the generated string via content property:
___ You can use string interpolation and alias "gemini":
julia
a = 1
+msg=aigenerate("What is `$a+$a`?"; model="gemini")
+msg.content # "1+1 is 2."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools
+
+conversation = [
+ PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),
+ PT.UserMessage("I have feelings for my iPhone. What should I do?")]
+msg=aigenerate(conversation; model="gemini")
+# AIMessage("Young Padawan, you have stumbled into a dangerous path.... <continues>")
Generate an AI response based on a given prompt using the OpenAI API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema not AbstractManagedSchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: Provided for interface consistency. Not needed for locally hosted Ollama.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation::AbstractVector{<:AbstractMessage}=[]: Not allowed for this schema. Provided only for compatibility.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
See also: ai_str, aai_str, aiembed
Example
Simple hello world to test the API:
julia
const PT = PromptingTools
+schema = PT.OllamaManagedSchema() # We need to explicit if we want Ollama, OpenAISchema is the default
+
+msg = aigenerate(schema, "Say hi!"; model="openhermes2.5-mistral")
+# [ Info: Tokens: 69 in 0.9 seconds
+# AIMessage("Hello! How can I assist you today?")
msg is an AIMessage object. Access the generated string via content property:
julia
typeof(msg) # AIMessage{SubString{String}}
+propertynames(msg) # (:content, :status, :tokens, :elapsed
+msg.content # "Hello! How can I assist you today?"
Note: We need to be explicit about the schema we want to use. If we don't, it will default to OpenAISchema (=PT.DEFAULT_SCHEMA) ___ You can use string interpolation:
julia
const PT = PromptingTools
+schema = PT.OllamaManagedSchema()
+a = 1
+msg=aigenerate(schema, "What is `$a+$a`?"; model="openhermes2.5-mistral")
+msg.content # "The result of `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools
+schema = PT.OllamaManagedSchema()
+
+conversation = [
+ PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),
+ PT.UserMessage("I have feelings for my iPhone. What should I do?")]
+
+msg = aigenerate(schema, conversation; model="openhermes2.5-mistral")
+# [ Info: Tokens: 111 in 2.1 seconds
+# AIMessage("Strong the attachment is, it leads to suffering it may. Focus on the force within you must, ...<continues>")
Note: Managed Ollama currently supports at most 1 User Message and 1 System Message given the API limitations. If you want more, you need to use the ChatMLSchema.
Generate an AI response based on a given prompt using the OpenAI API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema not AbstractManagedSchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: Provided for interface consistency. Not needed for locally hosted Ollama.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation::AbstractVector{<:AbstractMessage}=[]: Not allowed for this schema. Provided only for compatibility.
http_kwargs::NamedTuple: Additional keyword arguments for the HTTP request. Defaults to empty NamedTuple.
api_kwargs::NamedTuple: Additional keyword arguments for the Ollama API. Defaults to an empty NamedTuple.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
See also: ai_str, aai_str, aiembed
Example
Simple hello world to test the API:
julia
const PT = PromptingTools
+schema = PT.OllamaManagedSchema() # We need to explicit if we want Ollama, OpenAISchema is the default
+
+msg = aigenerate(schema, "Say hi!"; model="openhermes2.5-mistral")
+# [ Info: Tokens: 69 in 0.9 seconds
+# AIMessage("Hello! How can I assist you today?")
msg is an AIMessage object. Access the generated string via content property:
julia
typeof(msg) # AIMessage{SubString{String}}
+propertynames(msg) # (:content, :status, :tokens, :elapsed
+msg.content # "Hello! How can I assist you today?"
Note: We need to be explicit about the schema we want to use. If we don't, it will default to OpenAISchema (=PT.DEFAULT_SCHEMA) ___ You can use string interpolation:
julia
const PT = PromptingTools
+schema = PT.OllamaManagedSchema()
+a = 1
+msg=aigenerate(schema, "What is `$a+$a`?"; model="openhermes2.5-mistral")
+msg.content # "The result of `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools
+schema = PT.OllamaManagedSchema()
+
+conversation = [
+ PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),
+ PT.UserMessage("I have feelings for my iPhone. What should I do?")]
+
+msg = aigenerate(schema, conversation; model="openhermes2.5-mistral")
+# [ Info: Tokens: 111 in 2.1 seconds
+# AIMessage("Strong the attachment is, it leads to suffering it may. Focus on the force within you must, ...<continues>")
Note: Managed Ollama currently supports at most 1 User Message and 1 System Message given the API limitations. If you want more, you need to use the ChatMLSchema.
Generate an AI response based on a given prompt using the OpenAI API.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments. Useful parameters include:
temperature: A float representing the temperature for sampling (ie, the amount of "creativity"). Often defaults to 0.7.
logprobs: A boolean indicating whether to return log probabilities for each token. Defaults to false.
n: An integer representing the number of completions to generate at once (if supported).
stop: A vector of strings representing the stop conditions for the conversation. Defaults to an empty vector.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aiembed, aiclassify, aiextract, aiscan, aitemplates
Example
Simple hello world to test the API:
julia
result = aigenerate("Say Hi!")
+# [ Info: Tokens: 29 @ Cost: $0.0 in 1.0 seconds
+# AIMessage("Hello! How can I assist you today?")
result is an AIMessage object. Access the generated string via content property:
julia
typeof(result) # AIMessage{SubString{String}}
+propertynames(result) # (:content, :status, :tokens, :elapsed
+result.content # "Hello! How can I assist you today?"
___ You can use string interpolation:
julia
a = 1
+msg=aigenerate("What is `$a+$a`?")
+msg.content # "The sum of `1+1` is `2`."
___ You can provide the whole conversation or more intricate prompts as a Vector{AbstractMessage}:
julia
const PT = PromptingTools
+
+conversation = [
+ PT.SystemMessage("You're master Yoda from Star Wars trying to help the user become a Yedi."),
+ PT.UserMessage("I have feelings for my iPhone. What should I do?")]
+msg=aigenerate(conversation)
+# AIMessage("Ah, strong feelings you have for your iPhone. A Jedi's path, this is not... <continues>")
Generates an image from the provided prompt. If multiple "messages" are provided in prompt, it extracts the text ONLY from the last message!
Image (or the reference to it) will be returned in a DataMessage.content, the format will depend on the api_kwargs.response_format you set.
Can be used for generating images of varying quality and style with dall-e-* models. This function DOES NOT SUPPORT multi-turn conversations (ie, do not provide previous conversation via conversation argument).
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
image_size: String-based resolution of the image, eg, "1024x1024". Only some resolutions are supported - see the API docs.
image_quality: It can be either "standard" or "hd". Defaults to "standard".
image_n: The number of images to generate. Currently, only single image generation is allowed (image_n = 1).
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_IMAGE_GENERATION.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. Currently, NOT ALLOWED.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments. Several important arguments are highlighted below:
response_format: The format image should be returned in. Can be one of "url" or "b64_json". Defaults to "url" (the link will be inactived in 60 minutes).
style: The style of generated images (DALL-E 3 only). Can be either "vidid" or "natural". Defauls to "vidid".
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: A DataMessage object representing one or more generated images, including the rewritten prompt if relevant, status, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aigenerate, aiembed, aiclassify, aiextract, aiscan, aitemplates
Notes
This function DOES NOT SUPPORT multi-turn conversations (ie, do not provide previous conversation via conversation argument).
There is no token tracking provided by the API, so the messages will NOT report any cost despite costing you money!
You MUST download any URL-based images within 60 minutes. The links will become inactive.
Example
Generate an image:
julia
# You can experiment with `image_size`, `image_quality` kwargs!
+msg = aiimage("A white cat on a car")
+
+# Download the image into a file
+using Downloads
+Downloads.download(msg.content[:url], "cat_on_car.png")
+
+# You can also see the revised prompt that DALL-E 3 used
+msg.content[:revised_prompt]
+# Output: "Visualize a pristine white cat gracefully perched atop a shiny car.
+# The cat's fur is stark white and its eyes bright with curiosity.
+# As for the car, it could be a contemporary sedan, glossy and in a vibrant color.
+# The scene could be set under the blue sky, enhancing the contrast between the white cat, the colorful car, and the bright blue sky."
Note that you MUST download any URL-based images within 60 minutes. The links will become inactive.
If you wanted to download image directly into the DataMessage, provide response_format="b64_json" in api_kwargs:
julia
msg = aiimage("A white cat on a car"; image_quality="hd", api_kwargs=(; response_format="b64_json"))
+
+# Then you need to use Base64 package to decode it and save it to a file:
+using Base64
+write("cat_on_car_hd.png", base64decode(msg.content[:b64_json]));
Scans the provided image (image_url or image_path) with the goal provided in the prompt.
Can be used for many multi-modal tasks, such as: OCR (transcribe text in the image), image captioning, image classification, etc.
It's effectively a light wrapper around aigenerate call, which uses additional keyword arguments image_url, image_path, image_detail to be provided. At least one image source (url or path) must be provided.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
image_url: A string or vector of strings representing the URL(s) of the image(s) to scan.
image_path: A string or vector of strings representing the path(s) of the image(s) to scan.
image_detail: A string representing the level of detail to include for images. Can be "auto", "high", or "low". See OpenAI Vision Guide for more details.
attach_to_latest: A boolean how to handle if a conversation with multiple UserMessage is provided. When true, the images are attached to the latest UserMessage.
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aigenerate, aiembed, aiclassify, aiextract, aitemplates
Notes
All examples below use model "gpt4v", which is an alias for model ID "gpt-4-vision-preview"
max_tokens in the api_kwargs is preset to 2500, otherwise OpenAI enforces a default of only a few hundred tokens (~300). If your output is truncated, increase this value
Example
Describe the provided image:
julia
msg = aiscan("Describe the image"; image_path="julia.png", model="bakllava")
+# [ Info: Tokens: 1141 @ Cost: $0.0117 in 2.2 seconds
+# AIMessage("The image shows a logo consisting of the word "julia" written in lowercase")
You can provide multiple images at once as a vector and ask for "low" level of detail (cheaper):
julia
msg = aiscan("Describe the image"; image_path=["julia.png","python.png"] model="bakllava")
You can use this function as a nice and quick OCR (transcribe text in the image) with a template :OCRTask. Let's transcribe some SQL code from a screenshot (no more re-typing!):
julia
using Downloads
+# Screenshot of some SQL code -- we cannot use image_url directly, so we need to download it first
+image_url = "https://www.sqlservercentral.com/wp-content/uploads/legacy/8755f69180b7ac7ee76a69ae68ec36872a116ad4/24622.png"
+image_path = Downloads.download(image_url)
+msg = aiscan(:OCRTask; image_path, model="bakllava", task="Transcribe the SQL code in the image.", api_kwargs=(; max_tokens=2500))
+
+# AIMessage("```sql
+# update Orders <continue>
+
+# You can add syntax highlighting of the outputs via Markdown
+using Markdown
+msg.content |> Markdown.parse
Local models cannot handle image URLs directly (image_url), so you need to download the image first and provide it as image_path:
julia
using Downloads
+image_path = Downloads.download(image_url)
Notice that we set max_tokens = 2500. If your outputs seem truncated, it might be because the default maximum tokens on the server is set too low!
Scans the provided image (image_url or image_path) with the goal provided in the prompt.
Can be used for many multi-modal tasks, such as: OCR (transcribe text in the image), image captioning, image classification, etc.
It's effectively a light wrapper around aigenerate call, which uses additional keyword arguments image_url, image_path, image_detail to be provided. At least one image source (url or path) must be provided.
Arguments
prompt_schema: An optional object to specify which prompt template should be applied (Default to PROMPT_SCHEMA = OpenAISchema)
prompt: Can be a string representing the prompt for the AI conversation, a UserMessage, a vector of AbstractMessage or an AITemplate
image_url: A string or vector of strings representing the URL(s) of the image(s) to scan.
image_path: A string or vector of strings representing the path(s) of the image(s) to scan.
image_detail: A string representing the level of detail to include for images. Can be "auto", "high", or "low". See OpenAI Vision Guide for more details.
attach_to_latest: A boolean how to handle if a conversation with multiple UserMessage is provided. When true, the images are attached to the latest UserMessage.
verbose: A boolean indicating whether to print additional information.
api_key: A string representing the API key for accessing the OpenAI API.
model: A string representing the model to use for generating the response. Can be an alias corresponding to a model ID defined in MODEL_ALIASES.
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, skips sending the messages to the model (for debugging, often used with return_all=true).
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
http_kwargs: A named tuple of HTTP keyword arguments.
api_kwargs: A named tuple of API keyword arguments.
kwargs: Prompt variables to be used to fill the prompt/template
Returns
If return_all=false (default):
msg: An AIMessage object representing the generated AI message, including the content, status, tokens, and elapsed time.
Use msg.content to access the extracted string.
If return_all=true:
conversation: A vector of AbstractMessage objects representing the full conversation history, including the response from the AI model (AIMessage).
See also: ai_str, aai_str, aigenerate, aiembed, aiclassify, aiextract, aitemplates
Notes
All examples below use model "gpt4v", which is an alias for model ID "gpt-4-vision-preview"
max_tokens in the api_kwargs is preset to 2500, otherwise OpenAI enforces a default of only a few hundred tokens (~300). If your output is truncated, increase this value
Example
Describe the provided image:
julia
msg = aiscan("Describe the image"; image_path="julia.png", model="gpt4v")
+# [ Info: Tokens: 1141 @ Cost: $0.0117 in 2.2 seconds
+# AIMessage("The image shows a logo consisting of the word "julia" written in lowercase")
You can provide multiple images at once as a vector and ask for "low" level of detail (cheaper):
julia
msg = aiscan("Describe the image"; image_path=["julia.png","python.png"], image_detail="low", model="gpt4v")
You can use this function as a nice and quick OCR (transcribe text in the image) with a template :OCRTask. Let's transcribe some SQL code from a screenshot (no more re-typing!):
julia
# Screenshot of some SQL code
+image_url = "https://www.sqlservercentral.com/wp-content/uploads/legacy/8755f69180b7ac7ee76a69ae68ec36872a116ad4/24622.png"
+msg = aiscan(:OCRTask; image_url, model="gpt4v", task="Transcribe the SQL code in the image.", api_kwargs=(; max_tokens=2500))
+
+# [ Info: Tokens: 362 @ Cost: $0.0045 in 2.5 seconds
+# AIMessage("```sql
+# update Orders <continue>
+
+# You can add syntax highlighting of the outputs via Markdown
+using Markdown
+msg.content |> Markdown.parse
Notice that we enforce max_tokens = 2500. That's because OpenAI seems to default to ~300 tokens, which provides incomplete outputs. Hence, we set this value to 2500 as a default. If you still get truncated outputs, increase this value.
Find easily the most suitable templates for your use case.
You can search by:
query::Symbol which looks look only for partial matches in the template name
query::AbstractString which looks for partial matches in the template name or description
query::Regex which looks for matches in the template name, description or any of the message previews
Keyword Arguments
limit::Int limits the number of returned templates (Defaults to 10)
Examples
Find available templates with aitemplates:
julia
tmps = aitemplates("JuliaExpertAsk")
+# Will surface one specific template
+# 1-element Vector{AITemplateMetadata}:
+# PromptingTools.AITemplateMetadata
+# name: Symbol JuliaExpertAsk
+# description: String "For asking questions about Julia language. Placeholders: `ask`"
+# version: String "1"
+# wordcount: Int64 237
+# variables: Array{Symbol}((1,))
+# system_preview: String "You are a world-class Julia language programmer with the knowledge of the latest syntax. Your commun"
+# user_preview: String "# Question
+
+{{ask}}"
+# source: String ""
The above gives you a good idea of what the template is about, what placeholders are available, and how much it would cost to use it (=wordcount).
Search for all Julia-related templates:
julia
tmps = aitemplates("Julia")
+# 2-element Vector{AITemplateMetadata}... -> more to come later!
If you are on VSCode, you can leverage nice tabular display with vscodedisplay:
julia
using DataFrames
+tmps = aitemplates("Julia") |> DataFrame |> vscodedisplay
I have my selected template, how do I use it? Just use the "name" in aigenerate or aiclassify like you see in the first example!
Find the top-limit templates where provided query_key::Regex matches either of name, description or previews or User or System messages in TEMPLATE_METADATA.
Calculate the cost of a call based on the number of tokens in the message and the cost per token.
Arguments
prompt_tokens::Int: The number of tokens used in the prompt.
completion_tokens::Int: The number of tokens used in the completion.
model::String: The name of the model to use for determining token costs. If the model is not found in MODEL_REGISTRY, default costs are used.
cost_of_token_prompt::Number: The cost per prompt token. Defaults to the cost in MODEL_REGISTRY for the given model, or 0.0 if the model is not found.
cost_of_token_generation::Number: The cost per generation token. Defaults to the cost in MODEL_REGISTRY for the given model, or 0.0 if the model is not found.
create_template(; user::AbstractString, system::AbstractString="Act as a helpful AI assistant.",
+ load_as::Union{Nothing, Symbol, AbstractString} = nothing)
+
+create_template(system::AbstractString, user::AbstractString,
+ load_as::Union{Nothing, Symbol, AbstractString} = nothing)
Creates a simple template with a user and system message. Convenience function to prevent writing [PT.UserMessage(...), ...]
Arguments
system::AbstractString: The system message. Usually defines the personality, style, instructions, output format, etc.
user::AbstractString: The user message. Usually defines the input, query, request, etc.
load_as::Union{Nothing, Symbol, AbstractString}: If provided, loads the template into the TEMPLATE_STORE under the provided name load_as. If nothing, does not load the template.
Use double handlebar placeholders (eg, ) to define variables that can be replaced by the kwargs during the AI call (see example).
Returns a vector of SystemMessage and UserMessage objects. If load_as is provided, it registers the template in the TEMPLATE_STORE and TEMPLATE_METADATA as well.
Examples
Let's generate a quick template for a simple conversation (only one placeholder: name)
julia
# first system message, then user message (or use kwargs)
+tpl=PT.create_template("You must speak like a pirate", "Say hi to {{name}}")
+
+## 2-element Vector{PromptingTools.AbstractChatMessage}:
+## PromptingTools.SystemMessage("You must speak like a pirate")
+## PromptingTools.UserMessage("Say hi to {{name}}")
You can immediately use this template in ai* functions:
julia
aigenerate(tpl; name="Jack Sparrow")
+# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
If you're interested in saving the template in the template registry, jump to the end of these examples!
It will be saved and accessed under its basename, ie, GreatingPirate.
Now you can load it like all the other templates (provide the template directory):
julia
PT.load_templates!("templates") # it will remember the folder after the first run
+# Note: If you save it again, overwrite it, etc., you need to explicitly reload all templates again!
You can verify that your template is loaded with a quick search for "pirate":
julia
aitemplates("pirate")
+
+## 1-element Vector{AITemplateMetadata}:
+## PromptingTools.AITemplateMetadata
+## name: Symbol GreatingPirate
+## description: String ""
+## version: String "1.0"
+## wordcount: Int64 46
+## variables: Array{Symbol}((1,))
+## system_preview: String "You must speak like a pirate"
+## user_preview: String "Say hi to {{name}}"
+## source: String ""
Now you can use it like any other template (notice it's a symbol, so :GreatingPirate):
julia
aigenerate(:GreatingPirate; name="Jack Sparrow")
+# Output: AIMessage("Arr, me hearty! Best be sending me regards to Captain Jack Sparrow on the salty seas! May his compass always point true to the nearest treasure trove. Yarrr!")
If you do not need to save this template as a file, but you want to make it accessible in the template store for all ai* functions, you can use the load_as (= template name) keyword argument:
julia
# this will not only create the template, but also register it for immediate use
+tpl=PT.create_template("You must speak like a pirate", "Say hi to {{name}}"; load_as="GreatingPirate")
+
+# you can now use it like any other template
+aiextract(:GreatingPirate; name="Jack Sparrow")
Measures distance between two strings using the length of the longest common subsequence (ie, the lower the number, the better the match). Perfect match is distance = 0.0
Convenience wrapper around length_longest_common_subsequence to normalize the distances to 0-1 range. There is a also a dispatch for comparing a string vs an array of strings.
Notes
Use argmin and minimum to find the position of the closest match and the distance, respectively.
Matching with an empty string will always return 1.0 (worst match), even if the other string is empty as well (safety mechanism to avoid division by zero).
Arguments
input1::AbstractString: The first string to compare.
input2::AbstractString: The second string to compare.
Example
You can also use it to find the closest context for some AI generated summary/story:
julia
context = ["The enigmatic stranger vanished as swiftly as a wisp of smoke, leaving behind a trail of unanswered questions.",
+ "Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.",
+ "The ancient tree stood as a silent guardian, its gnarled branches reaching for the heavens.",
+ "The melody danced through the air, painting a vibrant tapestry of emotions.",
+ "Time flowed like a relentless river, carrying away memories and leaving imprints in its wake."]
+
+story = """
+ Beneath the shimmering moonlight, the ocean whispered secrets only the stars could hear.
+
+ Under the celestial tapestry, the vast ocean whispered its secrets to the indifferent stars. Each ripple, a murmured confidence, each wave, a whispered lament. The glittering celestial bodies listened in silent complicity, their enigmatic gaze reflecting the ocean's unspoken truths. The cosmic dance between the sea and the sky, a symphony of shared secrets, forever echoing in the ethereal expanse.
+ """
+
+dist = distance_longest_common_subsequence(story, context)
+@info "The closest context to the query: "$(first(story,20))..." is: "$(context[argmin(dist)])" (distance: $(minimum(dist)))"
Encode the choices into an enumerated list that can be interpolated into the prompt and creates the corresponding logit biases (to choose only from the selected tokens).
Optionally, can be a vector tuples, where the first element is the choice and the second is the description.
Arguments
schema::OpenAISchema: The OpenAISchema object.
choices::AbstractVector{<:Union{AbstractString,Tuple{<:AbstractString, <:AbstractString}}}: The choices to be encoded, represented as a vector of the choices directly, or tuples where each tuple contains a choice and its description.
kwargs...: Additional keyword arguments.
Returns
choices_prompt::AbstractString: The encoded choices as a single string, separated by newlines.
logit_bias::Dict: The logit bias dictionary, where the keys are the token IDs and the values are the bias values.
decode_ids::AbstractVector{<:AbstractString}: The decoded IDs of the choices.
choices_prompt, logit_bias, _ = PT.encode_choices(PT.OpenAISchema(), [("A", "any animal or creature"), ("P", "for any plant or tree"), ("O", "for everything else")])
+choices_prompt # Output: "1. "A" for any animal or creature
+2. "P" for any plant or tree
+3. "O" for everything else"
+logit_bias # Output: Dict(16 => 100, 17 => 100, 18 => 100)
Evaluates a code block cb in-place. It runs automatically when AICode is instantiated with a String.
Check the outcome of evaluation with Base.isvalid(cb). If ==true, provide code block has executed successfully.
Steps:
If cb::AICode has not been evaluated, cb.success = nothing. After the evaluation it will be either true or false depending on the outcome
Parse the text in cb.code
Evaluate the parsed expression
Capture outputs of the evaluated in cb.output
[OPTIONAL] Capture any stdout outputs (eg, test failures) in cb.stdout
If any error exception is raised, it is saved in cb.error
Finally, if all steps were successful, success is set to cb.success = true
Keyword Arguments
safe_eval::Bool: If true, we first check for any Pkg operations (eg, installing new packages) and missing imports, then the code will be evaluated inside a bespoke scratch module (not to change any user variables)
capture_stdout::Bool: If true, we capture any stdout outputs (eg, test failures) in cb.stdout
prefix::AbstractString: A string to be prepended to the code block before parsing and evaluation. Useful to add some additional code definition or necessary imports. Defaults to an empty string.
suffix::AbstractString: A string to be appended to the code block before parsing and evaluation. Useful to check that tests pass or that an example executes. Defaults to an empty string.
This function searches through the provided markdown content, identifies blocks of code specifically marked as Julia code (using the julia ... code fence patterns), and extracts the code within these blocks. The extracted code blocks are returned as a vector of strings, with each string representing one block of Julia code.
Note: Only the content within the code fences is extracted, and the code fences themselves are not included in the output.
See also: extract_code_blocks_fallback
Arguments
markdown_content::String: A string containing the markdown content from which Julia code blocks are to be extracted.
Returns
Vector{String}: A vector containing strings of extracted Julia code blocks. If no Julia code blocks are found, an empty vector is returned.
Extract Julia code blocks from a markdown string using a fallback method (splitting by arbitrary delim-iters). Much more simplistic than extract_code_blocks and does not support nested code blocks.
It is often used as a fallback for smaller LLMs that forget to code fence julia ....
Example
julia
code = """
println("hello")
+Some text
println("world")
"""
+
+# We extract text between triple backticks and check each blob if it looks like a valid Julia code
+code_parsed = extract_code_blocks_fallback(code) |> x -> filter(is_julia_code, x) |> x -> join(x, "
+")
Extract one or more names of functions defined in a given Julia code block. The function searches for two patterns: - The explicit function declaration pattern: function name(...) ... end - The concise function declaration pattern: name(...) = ...
It always returns a vector of strings, even if only one function name is found (it will be empty).
For only one function name match, use extract_function_name.
Finalizes the outputs of the ai* functions by either returning the conversation history or the last message.
Keyword arguments
return_all::Bool=false: If true, returns the entire conversation history, otherwise returns only the last message (the AIMessage).
dry_run::Bool=false: If true, does not send the messages to the model, but only renders the prompt with the given schema and replacement variables. Useful for debugging when you want to check the specific schema rendering.
conversation::AbstractVector{<:AbstractMessage}=[]: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
kwargs...: Variables to replace in the prompt template.
Find all positions of a subsequence subseq within a larger sequence seq. Used to lookup positions of code blocks in markdown.
This function scans the sequence seq and identifies all starting positions where the subsequence subseq is found. Both subseq and seq should be vectors of integers, typically obtained using codeunits on strings.
Arguments
subseq: A vector of integers representing the subsequence to search for.
seq: A vector of integers representing the larger sequence in which to search.
Returns
Vector{Int}: A vector of starting positions (1-based indices) where the subsequence is found in the sequence.
Extract the argument names, types and docstrings from a struct to create the function call signature in JSON schema.
You must provide a Struct type (not an instance of it) with some fields.
Note: Fairly experimental, but works for combination of structs, arrays, strings and singletons.
Tips
You can improve the quality of the extraction by writing a helpful docstring for your struct (or any nested struct). It will be provided as a description.
You can even include comments/descriptions about the individual fields.
All fields are assumed to be required, unless you allow null values (eg, ::Union{Nothing, Int}). Fields with Nothing will be treated as optional.
Missing values are ignored (eg, ::Union{Missing, Int} will be treated as Int). It's for broader compatibility and we cannot deserialize it as easily as Nothing.
Example
Do you want to extract some specific measurements from a text like age, weight and height? You need to define the information you need as a struct (return_type):
You can see that only the field age does not allow null values, hence, it's "required". While height and weight are optional.
signature["parameters"]["required"]
+# ["age"]
If there are multiple items you want to extract, define a wrapper struct to get a Vector of MyMeasurement:
struct MyMeasurementWrapper
+ measurements::Vector{MyMeasurement}
+end
+
+Or if you want your extraction to fail gracefully when data isn't found, use `MaybeExtract{T}` wrapper (inspired by Instructor package!):
using PromptingTools: MaybeExtract
type = MaybeExtract
Effectively the same as:
struct MaybeExtract{T}
result::Union{T, Nothing}
error::Bool // true if a result is found, false otherwise
message::Union{Nothing, String} // Only present if no result is found, should be short and concise
end
If LLM extraction fails, it will return a Dict with error and message fields instead of the result!
msg = aiextract("Extract measurements from the text: I am giraffe", type)
Dict{Symbol, Any} with 2 entries:
:message => "Sorry, this feature is only available for humans."
Helpful accessor for the last generated output (msg.content) in conversation. Returns the last output in the conversation (eg, the string/data in the last message).
Loads templates from folder templates/ in the package root and stores them in TEMPLATE_STORE and TEMPLATE_METADATA.
Note: Automatically removes any existing templates and metadata from TEMPLATE_STORE and TEMPLATE_METADATA if remove_templates=true.
Arguments
dir_templates::Union{String, Nothing}: The directory path to load templates from. If nothing, uses the default list of paths. It usually used only once "to register" a new template storage.
remember_path::Bool=true: If true, remembers the path for future refresh (in TEMPLATE_PATH).
remove_templates::Bool=isnothing(dir_templates): If true, removes any existing templates and metadata from store and metadata_store.
store::Dict{Symbol, <:Any}=TEMPLATE_STORE: The store to load the templates into.
metadata_store::Vector{<:AITemplateMetadata}=TEMPLATE_METADATA: The metadata store to load the metadata into.
Example
Load the default templates:
julia
PT.load_templates!() # no path needed
Load templates from a new custom path:
julia
PT.load_templates!("path/to/templates") # we will remember this path for future refresh
If you want to now refresh the default templates and the new path, just call load_templates!() without any arguments.
Pretty print a single AbstractMessage to the given IO stream.
text_width is the width of the text to be displayed. If not provided, it defaults to the width of the given IO stream and add newline separators as needed.
Add a new conversation to the conversation history and resize the history if necessary.
This function appends a conversation to the conv_history, which is a vector of conversations. Each conversation is represented as a vector of AbstractMessage objects. After adding the new conversation, the history is resized according to the max_history parameter to ensure that the size of the history does not exceed the specified limit.
Arguments
conv_history: A vector that stores the history of conversations. Typically, this is PT.CONV_HISTORY.
conversation: The new conversation to be added. It should be a vector of AbstractMessage objects.
max_history: The maximum number of conversations to retain in the history. If Nothing, the history is not resized.
Split a given string text into chunks recursively using a series of separators, with each chunk having a maximum length of max_length (if it's achievable given the separators provided). This function is useful for splitting large documents or texts into smaller segments that are more manageable for processing, particularly for models or systems with limited context windows.
It was previously known as split_by_length.
This is similar to Langchain's RecursiveCharacterTextSplitter. To achieve the same behavior, use separators=["\n\n", "\n", " ", ""].
Arguments
text::AbstractString: The text to be split.
separators::Vector{String}: An ordered list of separators used to split the text. The function iteratively applies these separators to split the text. Recommend to use ["\n\n", ". ", "\n", " "]
max_length::Int: The maximum length of each chunk. Defaults to 35,000 characters. This length is considered after each iteration of splitting, ensuring chunks fit within specified constraints.
Returns
Vector{String}: A vector of strings, where each string is a chunk of the original text that is smaller than or equal to max_length.
Usage Tips
I tend to prefer splitting on sentences (". ") before splitting on newline characters ("\n") to preserve the structure of the text.
What's the difference between separators=["\n"," ",""] and separators=["\n"," "]? The former will split down to character level (""), so it will always achieve the max_length but it will split words (bad for context!) I prefer to instead set slightly smaller max_length but not split words.
How It Works
The function processes the text iteratively with each separator in the provided order. It then measures the length of each chunk and splits it further if it exceeds the max_length. If the chunks is "short enough", the subsequent separators are not applied to it.
Each chunk is as close to max_length as possible (unless we cannot split it any further, eg, if the splitters are "too big" / there are not enough of them)
If the text is empty, the function returns an empty array.
Separators are re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible. Apply strip if you do not need them.
The function provides separators as the second argument to distinguish itself from its single-separator counterpart dispatch.
Examples
Splitting text using multiple separators:
julia
text = "Paragraph 1\n\nParagraph 2. Sentence 1. Sentence 2.\nParagraph 3"
+separators = ["\n\n", ". ", "\n"] # split by paragraphs, sentences, and newlines (not by words)
+chunks = recursive_splitter(text, separators, max_length=20)
Splitting text using multiple separators - with splitting on words:
julia
text = "Paragraph 1\n\nParagraph 2. Sentence 1. Sentence 2.\nParagraph 3"
+separators = ["\n\n", ". ", "\n", " "] # split by paragraphs, sentences, and newlines, words
+chunks = recursive_splitter(text, separators, max_length=10)
Split a given string text into chunks of a specified maximum length max_length. This is particularly useful for splitting larger documents or texts into smaller segments, suitable for models or systems with smaller context windows.
There is a method for dispatching on multiple separators, recursive_splitter(text::String, separators::Vector{String}; max_length::Int=35000) -> Vector{String} that mimics the logic of Langchain's RecursiveCharacterTextSplitter.
Arguments
text::String: The text to be split.
separator::String=" ": The separator used to split the text into minichunks. Defaults to a space character.
max_length::Int=35000: The maximum length of each chunk. Defaults to 35,000 characters, which should fit within 16K context window.
Returns
Vector{String}: A vector of strings, each representing a chunk of the original text that is smaller than or equal to max_length.
Notes
The function ensures that each chunk is as close to max_length as possible without exceeding it.
If the text is empty, the function returns an empty array.
The separator is re-added to the text chunks after splitting, preserving the original structure of the text as closely as possible.
Examples
Splitting text with the default separator (" "):
julia
text = "Hello world. How are you?"
+chunks = recursive_splitter(text; max_length=13)
+length(chunks) # Output: 2
Register a new AI model with name and its associated schema.
Registering a model helps with calculating the costs and automatically selecting the right prompt schema.
Arguments
name: The name of the model. This is the name that will be used to refer to the model in the ai* functions.
schema: The schema of the model. This is the schema that will be used to generate prompts for the model, eg, OpenAISchema().
cost_of_token_prompt: The cost of a token in the prompt for this model. This is used to calculate the cost of a prompt. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
cost_of_token_generation: The cost of a token generated by this model. This is used to calculate the cost of a generation. Note: It is often provided online as cost per 1000 tokens, so make sure to convert it correctly!
description: A description of the model. This is used to provide more information about the model when it is queried.
Builds a history of the conversation to provide the prompt to the API. All unspecified kwargs are passed as replacements such that =>value in the template.
Keyword Arguments
conversation: Past conversation to be included in the beginning of the prompt (for continued conversations).
Builds a history of the conversation to provide the prompt to the API. All unspecified kwargs are passed as replacements such that =>value in the template.
Keyword Arguments
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
Builds a history of the conversation to provide the prompt to the API. All unspecified kwargs are passed as replacements such that =>value in the template.
Note: Due to its "managed" nature, at most 2 messages can be provided (system and prompt inputs in the API).
Keyword Arguments
conversation: Not allowed for this schema. Provided only for compatibility.
Builds a history of the conversation to provide the prompt to the API. All unspecified kwargs are passed as replacements such that =>value in the template.
Keyword Arguments
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
Builds a history of the conversation to provide the prompt to the API. All unspecified kwargs are passed as replacements such that =>value in the template.
Keyword Arguments
image_detail: Only for UserMessageWithImages. It represents the level of detail to include for images. Can be "auto", "high", or "low".
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
Renders a conversation history from a vector of messages with all replacement variables specified in replacement_kwargs.
It is the first pass of the prompt rendering system, and is used by all other schemas.
Keyword Arguments
image_detail: Only for UserMessageWithImages. It represents the level of detail to include for images. Can be "auto", "high", or "low".
conversation: An optional vector of AbstractMessage objects representing the conversation history. If not provided, it is initialized as an empty vector.
Notes
All unspecified kwargs are passed as replacements such that =>value in the template.
If a SystemMessage is missing, we inject a default one at the beginning of the conversation.
Only one SystemMessage is allowed (ie, cannot mix two conversations different system prompts).
Resize the conversation history to a specified maximum length.
This function trims the conv_history to ensure that its size does not exceed max_history. It removes the oldest conversations first if the length of conv_history is greater than max_history.
Arguments
conv_history: A vector that stores the history of conversations. Typically, this is PT.CONV_HISTORY.
max_history: The maximum number of conversations to retain in the history. If Nothing, the history is not resized.
Asynchronous version of @ai_str macro, which will log the result once it's ready.
See also aai!"" if you want an asynchronous reply to the provided message / continue the conversation.
Example
Send asynchronous request to GPT-4, so we don't have to wait for the response: Very practical with slow models, so you can keep working in the meantime.
julia
+**...with some delay...**
+
+**[ Info: Tokens: 29 @ Cost: 0.0011
+ in 2.7 seconds**
+
+**[ Info: AIMessage> Hello! How can I assist you today?**
+
+
+[source](https://github.com/svilupp/PromptingTools.jl/blob/90b690d4509e7792dd80f18b7a97800399f51f28/src/macros.jl#L99-L116)
+
+</div>
+<br>
+<div style='border-width:1px; border-style:solid; border-color:black; padding: 1em; border-radius: 25px;'>
+<a id='PromptingTools.@ai!_str-Tuple{Any, Vararg{Any}}' href='#PromptingTools.@ai!_str-Tuple{Any, Vararg{Any}}'>#</a> <b><u>PromptingTools.@ai!_str</u></b> — <i>Macro</i>.
+
+
+
+
+```julia
+ai!"user_prompt"[model_alias] -> AIMessage
The ai!"" string macro is used to continue a previous conversation with the AI model.
It appends the new user prompt to the last conversation in the tracked history (in PromptingTools.CONV_HISTORY) and generates a response based on the entire conversation context. If you want to see the previous conversation, you can access it via PromptingTools.CONV_HISTORY, which keeps at most last PromptingTools.MAX_HISTORY_LENGTH conversations.
Arguments
user_prompt (String): The new input prompt to be added to the existing conversation.
model_alias (optional, any): Specify the model alias of the AI model to be used (see MODEL_ALIASES). If not provided, the default model is used.
Returns
AIMessage corresponding to the new user prompt, considering the entire conversation history.
Example
To continue a conversation:
julia
# start conversation as normal
+ai"Say hi."
+
+# ... wait for reply and then react to it:
+
+# continue the conversation (notice that you can change the model, eg, to more powerful one for better answer)
+ai!"What do you think about that?"gpt4t
+# AIMessage("Considering our previous discussion, I think that...")
Usage Notes
This macro should be used when you want to maintain the context of an ongoing conversation (ie, the last ai"" message).
It automatically accesses and updates the global conversation history.
If no conversation history is found, it raises an assertion error, suggesting to initiate a new conversation using ai"" instead.
Important
Ensure that the conversation history is not too long to maintain relevancy and coherence in the AI's responses. The history length is managed by MAX_HISTORY_LENGTH.
The ai"" string macro generates an AI response to a given prompt by using aigenerate under the hood.
See also ai!"" if you want to reply to the provided message / continue the conversation.
Arguments
user_prompt (String): The input prompt for the AI model.
model_alias (optional, any): Provide model alias of the AI model (see MODEL_ALIASES).
Returns
AIMessage corresponding to the input prompt.
Example
julia
result = ai"Hello, how are you?"
+# AIMessage("Hello! I'm an AI assistant, so I don't have feelings, but I'm here to help you. How can I assist you today?")
If you want to interpolate some variables or additional context, simply use string interpolation:
julia
a=1
+result = ai"What is `$a+$a`?"
+# AIMessage("The sum of `1+1` is `2`.")
If you want to use a different model, eg, GPT-4, you can provide its alias as a flag:
julia
result = ai"What is `1.23 * 100 + 1`?"gpt4t
+# AIMessage("The answer is 124.")
A lazy call wrapper for AI functions in the PromptingTools module, such as aigenerate.
The AICall struct is designed to facilitate a deferred execution model (lazy evaluation) for AI functions that interact with a Language Learning Model (LLM). It stores the necessary information for an AI call and executes the underlying AI function only when supplied with a UserMessage or when the run! method is applied. This approach allows for more flexible and efficient handling of AI function calls, especially in interactive environments.
Seel also: run!, AICodeFixer
Fields
func::F: The AI function to be called lazily. This should be a function like aigenerate or other ai* functions.
schema::Union{Nothing, PT.AbstractPromptSchema}: Optional schema to structure the prompt for the AI function.
conversation::Vector{PT.AbstractMessage}: A vector of messages that forms the conversation context for the AI call.
kwargs::NamedTuple: Keyword arguments to be passed to the AI function.
success::Union{Nothing, Bool}: Indicates whether the last call was successful (true) or not (false). Nothing if the call hasn't been made yet.
error::Union{Nothing, Exception}: Stores any exception that occurred during the last call. Nothing if no error occurred or if the call hasn't been made yet.
Example
Initiate an AICall like any ai* function, eg, AIGenerate:
julia
aicall = AICall(aigenerate)
+
+# With arguments and kwargs like ai* functions
+# from `aigenerate(schema, conversation; model="abc", api_kwargs=(; temperature=0.1))`
+# to
+aicall = AICall(aigenerate, schema, conversation; model="abc", api_kwargs=(; temperature=0.1)
+
+# Or with a template
+aicall = AIGenerate(:JuliaExpertAsk; ask="xyz", model="abc", api_kwargs=(; temperature=0.1))
Trigger the AICall with run! (it returns the update AICall struct back):
julia
aicall |> run!
+````
+
+You can also use `AICall` as a functor to trigger the AI call with a `UserMessage` or simply the text to send:
julia aicall(UserMessage("Hello, world!")) # Triggers the lazy call result = run!(aicall) # Explicitly runs the AI call ``` This can be used to "reply" to previous message / continue the stored conversation
Notes
The AICall struct is a key component in building flexible and efficient Agentic pipelines
The lazy evaluation model allows for setting up the call parameters in advance and deferring the actual execution until it is explicitly triggered.
This struct is particularly useful in scenarios where the timing of AI function execution needs to be deferred or where multiple potential calls need to be prepared and selectively executed.
An AIAgent that iteratively evaluates any received Julia code and provides feedback back to the AI model if num_rounds>0. AICodeFixer manages the lifecycle of a code fixing session, including tracking conversation history, rounds of interaction, and applying user feedback through a specialized feedback function.
It integrates with lazy AI call structures like AIGenerate.
The operation is "lazy", ie, the agent is only executed when needed, eg, when run! is called.
Fields
call::AICall: The AI call that is being used for code generation or processing, eg, AIGenerate (same as aigenerate but "lazy", ie, called only when needed
templates::Union{Symbol, AITemplate, Vector{PT.UserMessage}}: A set of user messages or templates that guide the AI's code fixing process. The first UserMessage is used in the first round of code fixing, the second UserMessage is used for every subsequent iteration.
num_rounds::Int: The number of rounds for the code fixing session. Defaults to 3.
round_counter::Int: Counter to track the current round of interaction.
feedback_func::Function: Function to generate feedback based on the AI's proposed code, defaults to aicodefixer_feedback (modular thanks to type dispatch on AbstractOutcomes)
kwargs::NamedTuple: Additional keyword arguments for customizing the AI call.
Note: Any kwargs provided to run!() will be passed to the underlying AICall.
Example
Let's create an AIGenerate call and then pipe it to AICodeFixer to run a few rounds of the coding fixing:
julia
# Create an AIGenerate call
+lazy_call = AIGenerate("Write a function to do XYZ...")
+
+# the action starts only when `run!` is called
+result = lazy_call |> AICodeFixer |> run!
+
+# Access the result of the code fixing session
+# result.call refers to the AIGenerate lazy call above
+conversation = result.call.conversation
+fixed_code = last(conversation) # usually in the last message
+
+# Preview the conversation history
+preview(conversation)
You can change the template used to provide user feedback and number of counds via arguments:
julia
# Setup an AIGenerate call
+lazy_call = AIGenerate(aigenerate, "Write code to do XYZ...")
+
+# Custom template and 2 fixing rounds
+result = AICodeFixer(lazy_call, [PT.UserMessage("Please fix the code.
+
+Feedback: {{feedback}}")]; num_rounds = 2) |> run!
+
+# The result now contains the AI's attempts to fix the code
+preview(result.call.conversation)
Notes
AICodeFixer is particularly useful when code is hard to get right in one shot (eg, smaller models, complex syntax)
The structure leverages the lazy evaluation model of AICall (/AIGenerate) to efficiently manage AI interactions and be able to repeatedly call it.
The run! function executes the AI call and applies the feedback loop for the specified number of rounds, enabling an interactive code fixing process.
Configuration for self-fixing the AI calls. It includes the following fields:
Fields
retries::Int: The number of retries ("fixing rounds") that have been attempted so far.
calls::Int: The total number of SUCCESSFULLY generated ai* function calls made so far (across all samples/retry rounds). Ie, if a call fails, because of an API error, it's not counted, because it didn't reach the LLM.
max_retries::Int: The maximum number of retries ("fixing rounds") allowed for the AI call. Defaults to 10.
max_calls::Int: The maximum number of ai* function calls allowed for the AI call. Defaults to 99.
retry_delay::Int: The delay (in seconds) between retry rounds. Defaults to 0s.
n_samples::Int: The number of samples to generate in each ai* call round (to increase changes of successful pass). Defaults to 1.
scoring::AbstractScoringMethod: The scoring method to use for generating multiple samples. Defaults to UCT(sqrt(2)).
ordering::Symbol: The ordering to use for select the best samples. With :PostOrderDFS we prioritize leaves, with :PreOrderDFS we prioritize the root. Defaults to :PostOrderDFS.
feedback_inplace::Bool: Whether to provide feedback in previous UserMessage (and remove the past AIMessage) or to create a new UserMessage. Defaults to false.
feedback_template::Symbol: Template to use for feedback in place. Defaults to :FeedbackFromEvaluator.
temperature::Float64: The temperature to use for sampling. Relevant only if not defined in api_kwargs provided. Defaults to 0.7.
catch_errors::Bool: Whether to catch errors during run! of AICall. Saves them in aicall.error. Defaults to false.
It's used to hold the data we're trying to optimize/discover (eg, a conversation), the scores from evaluation (wins, visits) and the results of the evaluations upon failure (feedback).
Fields
id::UInt16: Unique identifier for the node
parent::Union{SampleNode, Nothing}: Parent node that current node was built on
children::Vector{SampleNode}: Children nodes
wins::Int: Number of successful outcomes
visits::Int: Number of condition checks done (eg, losses are checks - wins)
data::T: eg, the conversation or some parameter to be optimized
feedback::String: Feedback from the evaluation, always a string! Defaults to empty string.
success::Union{Nothing, Bool}: Success of the generation and subsequent evaluations, proxy for whether it should be further evaluated. Defaults to nothing.
Adds formatted feedback to the conversation based on the sample node feedback (and its ancestors).
Arguments
conversation::AbstractVector{<:PT.AbstractMessage}: The conversation to add the feedback to.
sample::SampleNode: The sample node to extract the feedback from.
feedback_inplace::Bool=false: If true, it will add the feedback to the last user message inplace (and pop the last AIMessage). Otherwise, it will append the feedback as a new message.
feedback_template::Symbol=:FeedbackFromEvaluator: The template to use for the feedback message. It must be a valid AITemplate name.
Example
julia
sample = SampleNode(; data = nothing, feedback = "Feedback X")
+conversation = [PT.UserMessage("I say hi!"), PT.AIMessage(; content = "I say hi!")]
+conversation = AT.add_feedback!(conversation, sample)
+conversation[end].content == "### Feedback from Evaluator\nFeedback X\n"
+
+Inplace feedback:
julia conversation = [PT.UserMessage("I say hi!"), PT.AIMessage(; content = "I say hi!")] conversation = AT.add_feedback!(conversation, sample; feedback_inplace = true) conversation[end].content == "I say hi!\n\n### Feedback from Evaluator\nFeedback X\n"
+Sample with ancestors with feedback:
julia sample_p = SampleNode(; data = nothing, feedback = "\nFeedback X") sample = expand!(sample_p, nothing) sample.feedback = "\nFeedback Y" conversation = [PT.UserMessage("I say hi!"), PT.AIMessage(; content = "I say hi!")] conversation = AT.add_feedback!(conversation, sample)
conversation[end].content == "### Feedback from Evaluator\n\nFeedback X\n–––––\n\nFeedback Y\n" ```
Generate feedback for an AI code fixing session based on the conversation history. Function is designed to be extensible for different types of feedback and code evaluation outcomes.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
Individual feedback functions are dispatched on different subtypes of AbstractCodeOutcome and can be extended/overwritten to provide more detailed feedback.
See also: AIGenerate, AICodeFixer
Arguments
conversation::AbstractVector{<:PT.AbstractMessage}: A vector of messages representing the conversation history, where the last message is expected to contain the code to be analyzed.
max_length::Int=512: An optional argument that specifies the maximum length of the feedback message.
Returns
NamedTuple: A feedback message as a kwarg in NamedTuple based on the analysis of the code provided in the conversation.
Example
julia
new_kwargs = aicodefixer_feedback(conversation)
Notes
This function is part of the AI code fixing system, intended to interact with code in AIMessage and provide feedback on improving it.
The highlevel wrapper accepts a conversation and returns new kwargs for the AICall.
It dispatches for the code feedback based on the subtypes of AbstractCodeOutcome below:
CodeEmpty: No code found in the message.
CodeFailedParse: Code parsing error.
CodeFailedEval: Runtime evaluation error.
CodeFailedTimeout: Code execution timed out.
CodeSuccess: Successful code execution.
You can override the individual methods to customize the feedback.
Evaluates the condition f_cond on the aicall object. If the condition is not met, it will return the best sample to retry from and provide feedback (string or function) to aicall. That's why it's mutating. It will retry maximum max_retries times, with throw=true, an error will be thrown if the condition is not met after max_retries retries.
Function signatures
f_cond(aicall::AICallBlock) -> Bool, ie, it must accept the aicall object and return a boolean value.
feedback can be a string or feedback(aicall::AICallBlock) -> String, ie, it must accept the aicall object and return a string.
You can leverage the last_message, last_output, and AICode functions to access the last message, last output and execute code blocks in the conversation, respectively. See examples below.
Good Use Cases
Retry with API failures/drops (add retry_delay=2 to wait 2s between retries)
Check the output format / type / length / etc
Check the output with aiclassify call (LLM Judge) to catch unsafe/NSFW/out-of-scope content
Provide hints to the model to guide it to the correct answer
Gotchas
If controlling keyword arguments are set to nothing, they will fall back to the default values in aicall.config. You can override them by passing the keyword arguments explicitly.
If there multiple airetry! checks, they are evaluted sequentially. As long as throw==false, they will be all evaluated even if they failed previous checks.
Only samples which passed previous evaluations are evaluated (sample.success is true). If there are no successful samples, the function will evaluate only the active sample (aicall.active_sample_id) and nothing else.
Feedback from all "ancestor" evaluations is added upon retry, not feedback from the "sibblings" or other branches. To have only ONE long BRANCH (no sibblings), make sure to keep RetryConfig(; n_samples=1). That way the model will always see ALL previous feedback.
We implement a version of Monte Carlo Tree Search (MCTS) to always pick the most promising sample to restart from (you can tweak the options in RetryConfig to change the behaviour).
For large number of parallel branches (ie, "shallow and wide trees"), you might benefit from switching scoring to scoring=ThompsonSampling() (similar to how Bandit algorithms work).
Open-source/local models can struggle with too long conversation, you might want to experiment with in-place feedback (set RetryConfig(; feedback_inplace=true)).
Arguments
f_cond::Function: A function that accepts the aicall object and returns a boolean value. Retry will be attempted if the condition is not met (f_cond -> false).
aicall::AICallBlock: The aicall object to evaluate the condition on.
feedback::Union{AbstractString, Function}: Feedback to provide if the condition is not met. If a function is provided, it must accept the aicall object as the only argument and return a string.
verbose::Integer=1: A verbosity level for logging the retry attempts and warnings. A higher value indicates more detailed logging.
throw::Bool=false: If true, it will throw an error if the function f_cond does not return true after max_retries retries.
evaluate_all::Bool=false: If true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample.
feedback_expensive::Bool=false: If false, it will provide feedback to all samples that fail the condition. If feedback function is expensive to call (eg, another ai* function), set this to true and feedback will be provided only to the sample we will retry from.
max_retries::Union{Nothing, Int}=nothing: Maximum number of retries. If not provided, it will fall back to the max_retries in aicall.config.
retry_delay::Union{Nothing, Int}=nothing: Delay between retries in seconds. If not provided, it will fall back to the retry_delay in aicall.config.
Returns
The aicall object with the updated conversation, and samples (saves the evaluations and their scores/feedback).
Example
You can use airetry! to catch API errors in run! and auto-retry the call. RetryConfig is how you influence all the subsequent retry behaviours - see ?RetryConfig for more details.
julia
# API failure because of a non-existent model
+out = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),
+ model = "NOTEXIST")
+run!(out) # fails
+
+# we ask to wait 2s between retries and retry 2 times (can be set in `config` in aicall as well)
+airetry!(isvalid, out; retry_delay = 2, max_retries = 2)
If you provide arguments to the aicall, we try to honor them as much as possible in the following calls, eg, set low verbosity
julia
out = AIGenerate("say hi!"; config = RetryConfig(; catch_errors = true),
+model = "NOTEXIST", verbose=false)
+run!(out)
+# No info message, you just see `success = false` in the properties of the AICall
Let's show a toy example to demonstrate the runtime checks / guardrails for the model output. We'll play a color guessing game (I'm thinking "yellow"):
julia
# Notice that we ask for two samples (`n_samples=2`) at each attempt (to improve our chances).
+# Both guesses are scored at each time step, and the best one is chosen for the next step.
+# And with OpenAI, we can set `api_kwargs = (;n=2)` to get both samples simultaneously (cheaper and faster)!
+out = AIGenerate(
+ "Guess what color I'm thinking. It could be: blue, red, black, white, yellow. Answer with 1 word only";
+ verbose = false,
+ config = RetryConfig(; n_samples = 2), api_kwargs = (; n = 2))
+run!(out)
+
+
+## Check that the output is 1 word only, third argument is the feedback that will be provided if the condition fails
+## Notice: functions operate on `aicall` as the only argument. We can use utilities like `last_output` and `last_message` to access the last message and output in the conversation.
+airetry!(x -> length(split(last_output(x), r" |\.")) == 1, out,
+ "You must answer with 1 word only.")
+
+
+## Let's ensure that the output is in lowercase - simple and short
+airetry!(x -> all(islowercase, last_output(x)), out, "You must answer in lowercase.")
+# [ Info: Condition not met. Retrying...
+
+
+## Let's add final hint - it took us 2 retries
+airetry!(x -> startswith(last_output(x), "y"), out, "It starts with "y"")
+# [ Info: Condition not met. Retrying...
+# [ Info: Condition not met. Retrying...
+
+
+## We end up with the correct answer
+last_output(out)
+# Output: "yellow"
Let's explore how we got here. We save the various attempts in a "tree" (SampleNode object) You can access it in out.samples, which is the ROOT of the tree (top level). Currently "active" sample ID is out.active_sample_id -> that's the same as conversation field in your AICall.
julia
# Root node:
+out.samples
+# Output: SampleNode(id: 46839, stats: 6/12, length: 2)
+
+# Active sample (our correct answer):
+out.active_sample_id
+# Output: 50086
+
+# Let's obtain the active sample node with this ID - use getindex notation or function find_node
+out.samples[out.active_sample_id]
+# Output: SampleNode(id: 50086, stats: 1/1, length: 7)
+
+# The SampleNode has two key fields: data and feedback. Data is where the conversation is stored:
+active_sample = out.samples[out.active_sample_id]
+active_sample.data == out.conversation # Output: true -> This is the winning guess!
We also get a clear view of the tree structure of all samples with print_samples:
We can also iterate through all samples and extract whatever information we want with PostOrderDFS or PreOrderDFS (exported from AbstractTrees.jl)
julia
for sample in PostOrderDFS(out.samples)
+ # Data is the universal field for samples, we put `conversation` in there
+ # Last item in data is the last message in coversation
+ msg = sample.data[end]
+ if msg isa PT.AIMessage # skip feedback
+ # get only the message content, ie, the guess
+ println("ID: $(sample.id), Answer: $(msg.content)")
+ end
+end
+
+# ID: 20493, Answer: yellow
+# ID: 50086, Answer: yellow
+# ID: 2733, Answer: red
+# ID: 30088, Answer: blue
+# ID: 44816, Answer: blue
Note: airetry! will attempt to fix the model max_retries times. If you set throw=true, it will throw an ErrorException if the condition is not met after max_retries retries.
Let's define a mini program to guess the number and use airetry! to guide the model to the correct answer:
julia
"""
+ llm_guesser()
+
+Mini program to guess the number provided by the user (betwee 1-100).
+"""
+function llm_guesser(user_number::Int)
+ @assert 1 <= user_number <= 100
+ prompt = """
+I'm thinking a number between 1-100. Guess which one it is.
+You must respond only with digits and nothing else.
+Your guess:"""
+ ## 2 samples at a time, max 5 fixing rounds
+ out = AIGenerate(prompt; config = RetryConfig(; n_samples = 2, max_retries = 5),
+ api_kwargs = (; n = 2)) |> run!
+ ## Check the proper output format - must parse to Int, use do-syntax
+ ## We can provide feedback via a function!
+ function feedback_f(aicall)
+ "Output: $(last_output(aicall))
+Feedback: You must respond only with digits!!"
+ end
+ airetry!(out, feedback_f) do aicall
+ !isnothing(tryparse(Int, last_output(aicall)))
+ end
+ ## Give a hint on bounds
+ lower_bound = (user_number ÷ 10) * 10
+ upper_bound = lower_bound + 10
+ airetry!(
+ out, "The number is between or equal to $lower_bound to $upper_bound.") do aicall
+ guess = tryparse(Int, last_output(aicall))
+ lower_bound <= guess <= upper_bound
+ end
+ ## You can make at most 3x guess now -- if there is max_retries in `config.max_retries` left
+ max_retries = out.config.retries + 3
+ function feedback_f2(aicall)
+ guess = tryparse(Int, last_output(aicall))
+ "Your guess of $(guess) is wrong, it's $(abs(guess-user_number)) numbers away."
+ end
+ airetry!(out, feedback_f2; max_retries) do aicall
+ tryparse(Int, last_output(aicall)) == user_number
+ end
+
+ ## Evaluate the best guess
+ @info "Results: Guess: $(last_output(out)) vs User: $user_number (Number of calls made: $(out.config.calls))"
+ return out
+end
+
+# Let's play the game
+out = llm_guesser(33)
+[ Info: Condition not met. Retrying...
+[ Info: Condition not met. Retrying...
+[ Info: Condition not met. Retrying...
+[ Info: Condition not met. Retrying...
+[ Info: Results: Guess: 33 vs User: 33 (Number of calls made: 10)
Yay! We got it 😃
Now, we could explore different samples (eg, print_samples(out.samples)) or see what the model guessed at each step:
Note that if there are multiple "branches" the model will see only the feedback of its own and its ancestors not the other "branches". If you wanted to provide ALL feedback, set RetryConfig(; n_samples=1) to remove any "branching". It fixing will be done sequentially in one conversation and the model will see all feedback (less powerful if the model falls into a bad state). Alternatively, you can tweak the feedback function.
Evalutes the condition f_cond (must return Bool) on the aicall object. If the condition is not met, it will return the best sample to retry from and provide feedback.
Mutating as the results are saved in aicall.samples
If evaluate_all is true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample..
For f_cond and feedback functions, you can use the last_message and last_output utilities to access the last message and last output in the conversation, respectively.
Arguments
f_cond::Function: A function that accepts the aicall object and returns a boolean value. Retry will be attempted if the condition is not met (f_cond -> false).
aicall::AICallBlock: The aicall object to evaluate the condition on.
feedback::Union{AbstractString, Function}: Feedback to provide if the condition is not met. If a function is provided, it must accept the aicall object as the only argument and return a string.
evaluate_all::Bool=false: If true, it will evaluate all the "successful" samples in the aicall object. Otherwise, it will only evaluate the active sample.
feedback_expensive::Bool=false: If false, it will provide feedback to all samples that fail the condition. If feedback function is expensive to call (eg, another ai* function), set this to true and feedback will be provided only to the sample we will retry from.
Returns
a tuple (condition_passed, sample), where condition_passed is a boolean indicating whether the condition was met, and sample is the best sample to retry from.
Example
julia
# Mimic AIGenerate run!
+aicall = AIGenerate("Say hi!"; config = RetryConfig(; n_samples = 2))
+sample = expand!(aicall.samples, aicall.conversation; success = true)
+aicall.active_sample_id = sample.id
+
+# Return whether it passed and node to take the next action from
+cond, node = AT.evaluate_condition!(x -> occursin("hi", last_output(x)), aicall)
+
+# Checks:
+cond == true
+node == sample
+node.wins == 1
With feedback: ```julia
Mimic AIGenerate run with feedback
aicall = AIGenerate( :BlankSystemUser; system = "a", user = "b") sample = expand!(aicall.samples, aicall.conversation; success = true) aicall.active_sample_id = sample.id
Evaluate
cond, node = AT.evaluate_condition!( x -> occursin("NOTFOUND", last_output(x)), aicall, "Feedback X") cond == false # fail sample == node # same node (no other choice) node.wins == 0 node.feedback == " Feedback X"
Executes the code fixing process encapsulated by the AICodeFixer instance. This method iteratively refines and fixes code by running the AI call in a loop for a specified number of rounds, using feedback from the code evaluation (aicodefixer_feedback) to improve the outcome in each iteration.
Arguments
codefixer::AICodeFixer: An instance of AICodeFixer containing the AI call, templates, and settings for the code fixing session.
verbose::Int=1: Verbosity level for logging. A higher value indicates more detailed logging.
max_conversation_length::Int=32000: Maximum length in characters for the conversation history to keep it within manageable limits, especially for large code fixing sessions.
num_rounds::Union{Nothing, Int}=nothing: Number of additional rounds for the code fixing session. If nothing, the value from the AICodeFixer instance is used.
run_kwargs...: Additional keyword arguments that are passed to the AI function.
Returns
AICodeFixer: The updated AICodeFixer instance with the results of the code fixing session.
The run! method drives the core logic of the AICodeFixer, iterating through rounds of AI interactions to refine and fix code.
In each round, it applies feedback based on the current state of the conversation, allowing the AI to respond more effectively.
The conversation history is managed to ensure it stays within the specified max_conversation_length, keeping the AI's focus on relevant parts of the conversation.
This iterative process is essential for complex code fixing tasks where multiple interactions and refinements are required to achieve the desired outcome.
Executes the AI call wrapped by an AICallBlock instance. This method triggers the actual communication with the AI model and processes the response based on the provided conversation context and parameters.
Note: Currently return_all must always be set to true.
Arguments
aicall::AICallBlock: An instance of AICallBlock which encapsulates the AI function call along with its context and parameters (eg, AICall, AIGenerate)
verbose::Integer=1: A verbosity level for logging. A higher value indicates more detailed logging.
catch_errors::Union{Nothing, Bool}=nothing: A flag to indicate whether errors should be caught and saved to aicall.error. If nothing, it defaults to aicall.config.catch_errors.
return_all::Bool=true: A flag to indicate whether the whole conversation from the AI call should be returned. It should always be true.
kwargs...: Additional keyword arguments that are passed to the AI function.
Returns
AICallBlock: The same AICallBlock instance, updated with the results of the AI call. This includes updated conversation, success status, and potential error information.
Example
julia
aicall = AICall(aigenerate)
+run!(aicall)
Alternatively, you can trigger the run! call by using the AICall as a functor and calling it with a string or a UserMessage:
julia
aicall = AICall(aigenerate)
+aicall("Say hi!")
Notes
The run! method is a key component of the lazy evaluation model in AICall. It allows for the deferred execution of AI function calls, providing flexibility in how and when AI interactions are conducted.
The method updates the AICallBlock instance with the outcome of the AI call, including any generated responses, success or failure status, and error information if an error occurred.
This method is essential for scenarios where AI interactions are based on dynamic or evolving contexts, as it allows for real-time updates and responses based on the latest information.
Selects the best node from the tree using the given scoring (UCT or ThompsonSampling). Defaults to UCT. Thompson Sampling is more random with small samples, while UCT stabilizes much quicker thanks to looking at parent nodes as well.
Ordering can be either :PreOrderDFS or :PostOrderDFS. Defaults to :PostOrderDFS, which favors the leaves (end points of the tree).
Example
Compare the different scoring methods:
julia
# Set up mock samples and scores
+data = PT.AbstractMessage[]
+root = SampleNode(; data)
+child1 = expand!(root, data)
+backpropagate!(child1; wins = 1, visits = 1)
+child2 = expand!(root, data)
+backpropagate!(child2; wins = 0, visits = 1)
+child11 = expand!(child1, data)
+backpropagate!(child11; wins = 1, visits = 1)
+
+# Select with UCT
+n = select_best(root, UCT())
+SampleNode(id: 29826, stats: 1/1, length: 0)
+
+# Show the tree:
+print_samples(root; scoring = UCT())
+## SampleNode(id: 13184, stats: 2/3, score: 0.67, length: 0)
+## ├─ SampleNode(id: 26078, stats: 2/2, score: 2.05, length: 0)
+## │ └─ SampleNode(id: 29826, stats: 1/1, score: 2.18, length: 0)
+## └─ SampleNode(id: 39931, stats: 0/1, score: 1.48, length: 0)
+
+# Select with ThompsonSampling - much more random with small samples
+n = select_best(root, ThompsonSampling())
+SampleNode(id: 26078, stats: 2/2, length: 0)
+
+# Show the tree (run it a few times and see how the scores jump around):
+print_samples(root; scoring = ThompsonSampling())
+## SampleNode(id: 13184, stats: 2/3, score: 0.6, length: 0)
+## ├─ SampleNode(id: 26078, stats: 2/2, score: 0.93, length: 0)
+## │ └─ SampleNode(id: 29826, stats: 1/1, score: 0.22, length: 0)
+## └─ SampleNode(id: 39931, stats: 0/1, score: 0.84, length: 0)
Truncates a given conversation to a max_conversation_length characters by removing messages "in the middle". It tries to retain the original system+user message and also the most recent messages.
Practically, if a conversation is too long, it will start by removing the most recent message EXCEPT for the last two (assumed to be the last AIMessage with the code and UserMessage with the feedback
Arguments
max_conversation_length is in characters; assume c. 2-3 characters per LLM token, so 32000 should correspond to 16K context window.
api_key::AbstractString: The API key to use for the search. Get an API key from Tavily.
search_depth::AbstractString: The depth of the search. Can be either "basic" or "advanced". Default is "basic". Advanced search calls equal to 2 requests.
include_answer::Bool: Whether to include the answer in the search results. Default is false.
include_raw_content::Bool: Whether to include the raw content in the search results. Default is false.
max_results::Integer: The maximum number of results to return. Default is 5.
include_images::Bool: Whether to include images in the search results. Default is false.
include_domains::AbstractVector{<:AbstractString}: A list of domains to include in the search results. Default is an empty list.
exclude_domains::AbstractVector{<:AbstractString}: A list of domains to exclude from the search results. Default is an empty list.
Example
julia
r = create_websearch("Who is King Charles?")
Even better, you can get not just the results but also the answer:
julia
r = create_websearch("Who is King Charles?"; include_answer = true)
Note: This module is experimental and may change in future releases. The intention is for the functionality to be moved to separate packages over time.
embeddings::Union{Nothing, Matrix{<:Real}}: for semantic search
tags::Union{Nothing, AbstractMatrix{<:Bool}}: for exact search, filtering, etc. This is often a sparse matrix indicating which chunks have the given tag (see tag_vocab for the position lookup)
tags_vocab::Union{Nothing, Vector{<:AbstractString}}: vocabulary for the tags matrix (each column in tags is one item in tags_vocab and rows are the chunks)
sources::Vector{<:AbstractString}: sources of the chunks
extras::Union{Nothing, AbstractVector}: additional data, eg, metadata, source code, etc.
Dispatch for retrieve with advanced retrieval methods to improve result quality. Compared to SimpleRetriever, it adds rephrasing the query and reranking the results.
Fields
rephraser::AbstractRephraser: the rephrasing method, dispatching rephrase - uses HyDERephraser
embedder::AbstractEmbedder: the embedding method, dispatching get_embeddings (see Preparation Stage for more details) - uses BatchEmbedder
finder::AbstractSimilarityFinder: the similarity search method, dispatching find_closest - uses CosineSimilarity
tagger::AbstractTagger: the tag generating method, dispatching get_tags (see Preparation Stage for more details) - uses NoTagger
filter::AbstractTagFilter: the tag matching method, dispatching find_tags - uses NoTagFilter
reranker::AbstractReranker: the reranking method, dispatching rerank - uses CohereReranker
A struct for storing references to chunks in the given index (identified by index_id) called positions and scores holding the strength of similarity (=1 is the highest, most similar). It's the result of the retrieval stage of RAG.
Fields
index_id::Symbol: the id of the index from which the candidates are drawn
positions::Vector{Int}: the positions of the candidates in the index (ie, 5 refers to the 5th chunk in the index - chunks(index)[5])
scores::Vector{Float32}: the similarity scores of the candidates from the query (higher is better)
embeddings::Union{Nothing, Matrix{<:Real}}: for semantic search
tags::Union{Nothing, AbstractMatrix{<:Bool}}: for exact search, filtering, etc. This is often a sparse matrix indicating which chunks have the given tag (see tag_vocab for the position lookup)
tags_vocab::Union{Nothing, Vector{<:AbstractString}}: vocabulary for the tags matrix (each column in tags is one item in tags_vocab and rows are the chunks)
sources::Vector{<:AbstractString}: sources of the chunks
extras::Union{Nothing, AbstractVector}: additional data, eg, metadata, source code, etc.
Default method for build_context! method. It simply enumerates the context snippets around each position in candidates. When possibly, it will add surrounding chunks (from the same source).
Rephraser implemented using the provided AI Template (eg, ...) and standard chat model.
It uses a prompt-based rephrasing method called HyDE (Hypothetical Document Embedding), where instead of looking for an embedding of the question, we look for the documents most similar to a synthetic passage that would be a good answer to our question.
Tagger for get_tags functions, which generates possible tags for each chunk via aiextract. You can customize it via prompt template (default: :RAGExtractMetadataShort), but it's quite open-ended (ie, AI decides the possible tags).
Default configuration for RAG. It uses SimpleIndexer, SimpleRetriever, and SimpleGenerator as default components.
To customize the components, replace corresponding fields for each step of the RAG pipeline (eg, use subtypes(AbstractIndexBuilder) to find the available options).
A struct for debugging RAG answers. It contains the question, answer, context, and the candidate chunks at each step of the RAG pipeline.
Think of the flow as question -> rephrased_questions -> answer -> final_answer with the context and candidate chunks helping along the way.
Fields
question::AbstractString: the original question
rephrased_questions::Vector{<:AbstractString}: a vector of rephrased questions (eg, HyDe, Multihop, etc.)
answer::AbstractString: the generated answer
final_answer::AbstractString: the refined final answer (eg, after CorrectiveRAG), also considered the FINAL answer (it must be always available)
context::Vector{<:AbstractString}: the context used for retrieval (ie, the vector of chunks and their surrounding window if applicable)
sources::Vector{<:AbstractString}: the sources of the context (for the original matched chunks)
emb_candidates::CandidateChunks: the candidate chunks from the embedding index (from find_closest)
tag_candidates::Union{Nothing, CandidateChunks}: the candidate chunks from the tag index (from find_tags)
filtered_candidates::CandidateChunks: the filtered candidate chunks (intersection of emb_candidates and tag_candidates)
reranked_candidates::CandidateChunks: the reranked candidate chunks (from rerank)
conversations::Dict{Symbol,Vector{<:AbstractMessage}}: the conversation history for AI steps of the RAG pipeline, use keys that correspond to the function names, eg, :answer or :refine
See also: pprint (pretty printing), annotate_support (for annotating the answer)
Annotation method where we score answer versus each context based on word-level trigrams that match.
It's very simple method (and it can loose some semantic meaning in longer sequences like negative), but it works reasonably well for both text and code.
High-level wrapper for Retrieval-Augmented Generation (RAG), it combines together the retrieve and generate! steps which you can customize if needed.
The simplest version first finds the relevant chunks in index for the question and then sends these chunks to the AI model to help with generating a response to the question.
To customize the components, replace the types (retriever, generator) of the corresponding step of the RAG pipeline - or go into sub-routines within the steps. Eg, use subtypes(AbstractRetriever) to find the available options.
Arguments
cfg::AbstractRAGConfig: The configuration for the RAG pipeline. Defaults to RAGConfig(), where you can swap sub-types to customize the pipeline.
index::AbstractChunkIndex: The chunk index to search for relevant text.
question::AbstractString: The question to be answered.
return_all::Bool: If true, returns the details used for RAG along with the response.
verbose::Integer: If >0, enables verbose logging. The higher the number, the more nested functions will log.
api_kwargs: API parameters that will be forwarded to ALL of the API calls (aiembed, aigenerate, and aiextract).
retriever::AbstractRetriever: The retriever to use for finding relevant chunks. Defaults to cfg.retriever, eg, SimpleRetriever (with no question rephrasing).
retriever_kwargs::NamedTuple: API parameters that will be forwarded to the retriever call. Examples of important ones:
top_k::Int: Number of top candidates to retrieve based on embedding similarity.
top_n::Int: Number of candidates to return after reranking.
tagger::AbstractTagger: Tagger to use for tagging the chunks. Defaults to NoTagger().
tagger_kwargs::NamedTuple: API parameters that will be forwarded to the tagger call. You could provide the explicit tags directly with PassthroughTagger and tagger_kwargs = (; tags = ["tag1", "tag2"]).
generator::AbstractGenerator: The generator to use for generating the answer. Defaults to cfg.generator, eg, SimpleGenerator.
generator_kwargs::NamedTuple: API parameters that will be forwarded to the generator call. Examples of important ones:
answerer_kwargs::NamedTuple: API parameters that will be forwarded to the answerer call. Examples:
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerFromContext.
refiner::AbstractRefiner: The method to use for refining the answer. Defaults to generator.refiner, eg, NoRefiner.
refiner_kwargs::NamedTuple: API parameters that will be forwarded to the refiner call.
model: The model to use for generating the answer. Defaults to PT.MODEL_CHAT.
template: The template to use for the aigenerate function. Defaults to :RAGAnswerRefiner.
cost_tracker: An atomic counter to track the total cost of the operations (if you want to track the cost of multiple pipeline runs - it passed around in the pipeline).
Returns
If return_all is false, returns the generated message (msg).
If return_all is true, returns the detail of the full pipeline in RAGResult (see the docs).
See also build_index, retrieve, generate!, RAGResult
Examples
Using airag to get a response for a question:
julia
index = build_index(...) # create an index
+question = "How to make a barplot in Makie.jl?"
+msg = airag(index; question)
To understand the details of the RAG process, use return_all=true
julia
msg, details = airag(index; question, return_all = true)
+# details is a RAGDetails object with all the internal steps of the `airag` function
You can also pretty-print details to highlight generated text vs text that is supported by context. It also includes annotations of which context was used for each part of the response (where available).
julia
PT.pprint(details)
Example with advanced retrieval (with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n). In addition, it will be done with a "custom" locally-hosted model.
julia
cfg = RAGConfig(; retriever = AdvancedRetriever())
+
+# kwargs will be big and nested, let's prepare them upfront
+# we specify "custom" model for each component that calls LLM
+kwargs = (
+ retriever_kwargs = (;
+ top_k = 100,
+ top_n = 5,
+ rephraser_kwargs = (;
+ model = "custom"),
+ embedder_kwargs = (;
+ model = "custom"),
+ tagger_kwargs = (;
+ model = "custom")),
+ generator_kwargs = (;
+ answerer_kwargs = (;
+ model = "custom"),
+ refiner_kwargs = (;
+ model = "custom")),
+ api_kwargs = (;
+ url = "http://localhost:8080"))
+
+result = airag(cfg, index, question; kwargs...)
Annotates the answer with the overlap/what's supported in context and returns the annotated tree of nodes representing the answer
Returns a "root" node with children nodes representing the sentences/code blocks in the answer. Only the "leaf" nodes are to be printed (to avoid duplication), "leaf" nodes are those with NO children.
Default logic:
Split into sentences/code blocks, then into tokens (~words).
Then match each token (~word) exactly.
If no exact match found, count trigram-based match (include the surrounding tokens for better contextual awareness).
If the match is higher than min_score, it's recorded in the score of the node.
Arguments
annotater::TrigramAnnotater: Annotater to use
answer::AbstractString: Text to annotate
context::AbstractVector: Context to annotate against, ie, look for "support" in the texts in context
min_score::Float64: Minimum score to consider a match. Default: 0.5, which means that half of the trigrams of each word should match
skip_trigrams::Bool: Whether to potentially skip trigram matching if exact full match is found. Default: true
hashed::Bool: Whether to use hashed trigrams. It's harder to debug, but it's much faster for larger texts (hashed text are held in a Set to deduplicate). Default: true
sources::Union{Nothing, AbstractVector{<:AbstractString}}: Sources to add at the end of the context. Default: nothing
min_source_score::Float64: Minimum score to consider/to display a source. Default: 0.25, which means that at least a quarter of the trigrams of each word should match to some context. The threshold is lower than min_score, because it's average across ALL words in a block, so it's much harder to match fully with generated text.
add_sources::Bool: Whether to add sources at the end of each code block/sentence. Sources are addded in the square brackets like "[1]". Default: true
add_scores::Bool: Whether to add source-matching scores at the end of each code block/sentence. Scores are added in the square brackets like "[0.75]". Default: true
kwargs: Additional keyword arguments to pass to trigram_support! and set_node_style!. See their documentation for more details (eg, customize the colors of the nodes based on the score)
Example
julia
annotater = TrigramAnnotater()
+context = [
+ "This is a test context.", "Another context sentence.", "Final piece of context."]
+answer = "This is a test context. Another context sentence."
+
+annotated_root = annotate_support(annotater, answer, context)
+pprint(annotated_root) # pretty print the annotated tree
Dispatch for annotate_support for AbstractRAGResult type. It extracts the final_answer and context from the result and calls annotate_support with them.
See annotate_support for more details.
Example
julia
res = RAGResult(; question = "", final_answer = "This is a test.",
+ context = ["Test context.", "Completely different"])
+annotated_root = annotate_support(annotater, res)
+PT.pprint(annotated_root)
Build context strings for each position in candidates considering a window margin around each position. If mutating version is used (build_context!), it will use result.reranked_candidates to update the result.context field.
Arguments
contexter::ContextEnumerator: The method to use for building the context. Enumerates the snippets.
index::ChunkIndex: The index containing chunks and sources.
candidates::CandidateChunks: Candidate chunks which contain positions to extract context from.
verbose::Bool: If true, enables verbose logging.
chunks_window_margin::Tuple{Int, Int}: A tuple indicating the margin (before, after) around each position to include in the context. Defaults to (1,1), which means 1 preceding and 1 suceeding chunk will be included. With (0,0), only the matching chunks will be included.
Returns
Vector{String}: A vector of context strings, each corresponding to a position in reranked_candidates.
Examples
julia
index = ChunkIndex(...) # Assuming a proper index is defined
+candidates = CandidateChunks(index.id, [2, 4], [0.1, 0.2])
+context = build_context(ContextEnumerator(), index, candidates; chunks_window_margin=(0, 1)) # include only one following chunk for each matching chunk
Build an INDEX for RAG (Retriever-Augmented Generation) applications from the provided file paths. INDEX is a object storing the document chunks and their embeddings (and potentially other information).
The function processes each file or document (depending on chunker), splits its content into chunks, embeds these chunks, optionally extracts metadata, and then combines this information into a retrievable index.
Define your own methods via indexer and its subcomponents (chunker, embedder, tagger).
Arguments
indexer::AbstractIndexBuilder: The indexing logic to use. Default is SimpleIndexer().
files_or_docs: A vector of valid file paths OR string documents to be indexed (chunked and embedded). Specify which mode to use via chunker.
verbose: An Integer specifying the verbosity of the logs. Default is 1 (high-level logging). 0 is disabled.
extras: An optional vector of extra information to be stored with each chunk. Default is nothing.
index_id: A unique identifier for the index. Default is a generated symbol.
chunker: The chunker logic to use for splitting the documents. Default is TextChunker().
chunker_kwargs: Parameters to be provided to the get_chunks function. Useful to change the separators or max_length.
sources: A vector of strings indicating the source of each chunk. Default is equal to files_or_docs.
embedder: The embedder logic to use for embedding the chunks. Default is BatchEmbedder().
embedder_kwargs: Parameters to be provided to the get_embeddings function. Useful to change the target_batch_size_length or reduce asyncmap tasks ntasks.
model: The model to use for embedding. Default is PT.MODEL_EMBEDDING.
tagger: The tagger logic to use for extracting tags from the chunks. Default is NoTagger(), ie, skip tag extraction. There are also PassthroughTagger and OpenTagger.
tagger_kwargs: Parameters to be provided to the get_tags function.
model: The model to use for tags extraction. Default is PT.MODEL_CHAT.
template: A template to be used for tags extraction. Default is :RAGExtractMetadataShort.
tags: A vector of vectors of strings directly providing the tags for each chunk. Applicable for tagger::PasstroughTagger.
api_kwargs: Parameters to be provided to the API endpoint. Shared across all API calls if provided.
cost_tracker: A Threads.Atomic{Float64} object to track the total cost of the API calls. Useful to pass the total cost to the parent call.
Returns
ChunkIndex: An object containing the compiled index of chunks, embeddings, tags, vocabulary, and sources.
# Default is loading a vector of strings and chunking them (`TextChunker()`)
+index = build_index(SimpleIndexer(), texts; chunker_kwargs = (; max_length=10))
+
+# Another example with tags extraction, splitting only sentences and verbose output
+# Assuming `test_files` is a vector of file paths
+indexer = SimpleIndexer(chunker=FileChunker(), tagger=OpenTagger())
+index = build_index(indexer, test_files;
+ chunker_kwargs(; separators=[". "]), verbose=true)
Notes
If you get errors about exceeding embedding input sizes, first check the max_length in your chunks. If that does NOT resolve the issue, try changing the embedding_kwargs. In particular, reducing the target_batch_size_length parameter (eg, 10_000) and number of tasks ntasks=1. Some providers cannot handle large batch sizes (eg, Databricks).
Create a collection of question and answer evaluations (QAEvalItem) from document chunks and sources. This function generates Q&A pairs based on the provided document chunks, using a specified AI model and template.
Arguments
doc_chunks::Vector{<:AbstractString}: A vector of document chunks, each representing a segment of text.
sources::Vector{<:AbstractString}: A vector of source identifiers corresponding to each chunk in doc_chunks (eg, filenames or paths).
model: The AI model used for generating Q&A pairs. Default is PT.MODEL_CHAT.
instructions::String: Additional instructions or context to provide to the model generating QA sets. Defaults to "None.".
qa_template::Symbol: A template symbol that dictates the AITemplate that will be used. It must have placeholder context. Default is :CreateQAFromContext.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API endpoint.
verbose::Bool: If true, additional information like costs will be logged. Defaults to true.
Returns
Vector{QAEvalItem}: A vector of QAEvalItem structs, each containing a source, context, question, and answer. Invalid or empty items are filtered out.
Notes
The function internally uses aiextract to generate Q&A pairs based on the provided qa_template. So you can use any kwargs that you want.
Each QAEvalItem includes the context (document chunk), the generated question and answer, and the source.
The function tracks and reports the cost of AI calls if verbose is enabled.
Items where the question, answer, or context is empty are considered invalid and are filtered out.
Examples
Creating Q&A evaluations from a set of document chunks:
julia
doc_chunks = ["Text from document 1", "Text from document 2"]
+sources = ["source1", "source2"]
+qa_evals = build_qa_evals(doc_chunks, sources)
Finds the indices of chunks (represented by embeddings in emb) that are closest (in cosine similarity for CosineSimilarity()) to query embedding (query_emb).
finder is the logic used for the similarity search. Default is CosineSimilarity.
If minimum_similarity is provided, only indices with similarity greater than or equal to it are returned. Similarity can be between -1 and 1 (-1 = completely opposite, 1 = exactly the same).
Assume we already have `index`
+
+question = "What are the best practices for parallel computing in Julia?"
+
+# Retrieve the relevant chunks - returns RAGResult
+result = retrieve(index, question)
+
+# Generate the answer using the default generator, mutates the same result
+result = generate!(index, result)
Chunks the provided files_or_docs into chunks of maximum length max_length (if possible with provided separators).
Supports two modes of operation:
chunker = FileChunker(): The function opens each file in files_or_docs and reads its contents.
chunker = TextChunker(): The function assumes that files_or_docs is a vector of strings to be chunked, you MUST provide corresponding sources.
Arguments
files_or_docs: A vector of valid file paths OR string documents to be chunked.
separators: A list of strings used as separators for splitting the text in each file into chunks. Default is [\n\n", ". ", "\n", " "]. See recursive_splitter for more details.
max_length: The maximum length of each chunk (if possible with provided separators). Default is 256.
sources: A vector of strings indicating the source of each chunk. Default is equal to files_or_docs (for reader=:files)
Embeds a vector of docs using the provided model (kwarg model) in a batched manner - BatchEmbedder.
BatchEmbedder tries to batch embedding calls for roughly 80K characters per call (to avoid exceeding the API rate limit) to reduce network latency.
Notes
docs are assumed to be already chunked to the reasonable sizes that fit within the embedding context limit.
If you get errors about exceeding input sizes, first check the max_length in your chunks. If that does NOT resolve the issue, try reducing the target_batch_size_length parameter (eg, 10_000) and number of tasks ntasks=1. Some providers cannot handle large batch sizes.
Arguments
docs: A vector of strings to be embedded.
verbose: A boolean flag for verbose output. Default is true.
model: The model to use for embedding. Default is PT.MODEL_EMBEDDING.
cost_tracker: A Threads.Atomic{Float64} object to track the total cost of the API calls. Useful to pass the total cost to the parent call.
target_batch_size_length: The target length (in characters) of each batch of document chunks sent for embedding. Default is 80_000 characters. Speeds up embedding process.
ntasks: The number of tasks to use for asyncmap. Default is 4 * Threads.nthreads().
Pass tags directly as Vector of Vectors of strings (ie, tags[i] is the tags for docs[i]). It then builds the vocabulary from the tags and returns both the tags in matrix form and the vocabulary.
Pretty-prints the annotation parent_node (or RAGResult) to the io stream (or returns the string) in HTML format (assumes node is styled with styler HTMLStyler).
It wraps each "token" into a span with requested styling (HTMLStyler's properties classes and styles). It also replaces new lines with <br> for better HTML formatting.
For any non-HTML styler, it prints the content as plain text.
Returns
nothing if io is provided
or the string with HTML-formatted text (if io is not provided, we print the result out)
See also HTMLStyler, annotate_support, and set_node_style! for how the styling is applied and what the arguments mean.
Examples
Note: RT is an alias for PromptingTools.Experimental.RAGTools
Simple start directly with the RAGResult:
julia
# set up the text/RAGResult
+context = [
+ "This is a test context.", "Another context sentence.", "Final piece of context."]
+answer = "This is a test answer. It has multiple sentences."
+rag = RT.RAGResult(; context, final_answer=answer, question="")
+
+# print the HTML
+print_html(rag)
Low-level control by creating our AnnotatedNode:
julia
# prepare your HTML styling
+styler_kwargs = (;
+ default_styler=RT.HTMLStyler(),
+ low_styler=RT.HTMLStyler(styles="color:magenta", classes=""),
+ medium_styler=RT.HTMLStyler(styles="color:blue", classes=""),
+ high_styler=RT.HTMLStyler(styles="", classes=""))
+
+# annotate the text
+context = [
+ "This is a test context.", "Another context sentence.", "Final piece of context."]
+answer = "This is a test answer. It has multiple sentences."
+
+parent_node = RT.annotate_support(
+ RT.TrigramAnnotater(), answer, context; add_sources=false, add_scores=false, styler_kwargs...)
+
+# print the HTML
+print_html(parent_node)
+
+# or to accumulate more nodes
+io = IOBuffer()
+print_html(io, parent_node)
Rephrases the question using the provided rephraser template = RAGQueryHyDE.
Special flavor of rephrasing using HyDE (Hypothetical Document Embedding) method, which aims to find the documents most similar to a synthetic passage that would be a good answer to our question.
Returns both the original and the rephrased question.
Arguments
rephraser: Type that dictates the logic of rephrasing step.
question: The question to be rephrased.
model: The model to use for rephrasing. Default is PT.MODEL_CHAT.
template: The rephrasing template to use. Default is :RAGQueryHyDE. Find more with aitemplates("rephrase").
verbose: A boolean flag indicating whether to print verbose logging. Default is true.
Retrieves the most relevant chunks from the index for the given question and returns them in the RAGResult object.
This is the main entry point for the retrieval stage of the RAG pipeline. It is often followed by generate! step.
Notes:
The default flow is build_context! -> answer! -> refine! -> postprocess!.
The arguments correspond to the steps of the retrieval process (rephrasing, embedding, finding similar docs, tagging, filtering by tags, reranking). You can customize each step by providing a new custom type that dispatches the corresponding function, eg, create your own type struct MyReranker<:AbstractReranker end and define the custom method for it rerank(::MyReranker,...) = ....
Note: Discover available retrieval sub-types for each step with subtypes(AbstractRephraser) and similar for other abstract types.
If you're using locally-hosted models, you can pass the api_kwargs with the url field set to the model's URL and make sure to provide corresponding model kwargs to rephraser, embedder, and tagger to use the custom models (they make AI calls).
Arguments
retriever: The retrieval method to use. Default is SimpleRetriever but could be AdvancedRetriever for more advanced retrieval.
index: The index that holds the chunks and sources to be retrieved from.
question: The question to be used for the retrieval.
verbose: If >0, it prints out verbose logging. Default is 1. If you set it to 2, it will print out logs for each sub-function.
top_k: The TOTAL number of closest chunks to return from find_closest. Default is 100. If there are multiple rephrased questions, the number of chunks per each item will be top_k ÷ number_of_rephrased_questions.
top_n: The TOTAL number of most relevant chunks to return for the context (from rerank step). Default is 5.
api_kwargs: Additional keyword arguments to be passed to the API calls (shared by all ai* calls).
rephraser: Transform the question into one or more questions. Default is retriever.rephraser.
rephraser_kwargs: Additional keyword arguments to be passed to the rephraser.
model: The model to use for rephrasing. Default is PT.MODEL_CHAT.
template: The rephrasing template to use. Default is :RAGQueryOptimizer or :RAGQueryHyDE (depending on the rephraser selected).
embedder: The embedding method to use. Default is retriever.embedder.
embedder_kwargs: Additional keyword arguments to be passed to the embedder.
finder: The similarity search method to use. Default is retriever.finder, often CosineSimilarity.
finder_kwargs: Additional keyword arguments to be passed to the similarity finder.
tagger: The tag generating method to use. Default is retriever.tagger.
tagger_kwargs: Additional keyword arguments to be passed to the tagger. Noteworthy arguments:
tags: Directly provide the tags to use for filtering (can be String, Regex, or Vector{String}). Useful for tagger = PassthroughTagger.
filter: The tag matching method to use. Default is retriever.filter.
filter_kwargs: Additional keyword arguments to be passed to the tag filter.
reranker: The reranking method to use. Default is retriever.reranker.
reranker_kwargs: Additional keyword arguments to be passed to the reranker.
model: The model to use for reranking. Default is rerank-english-v2.0 if you use reranker = CohereReranker().
cost_tracker: An atomic counter to track the cost of the retrieval. Default is Threads.Atomic{Float64}(0.0).
Find the 5 most relevant chunks from the index for the given question.
julia
# assumes you have an existing index `index`
+retriever = SimpleRetriever()
+
+result = retrieve(retriever,
+ index,
+ "What is the capital of France?",
+ top_n = 5)
+
+# or use the default retriever (same as above)
+result = retrieve(retriever,
+ index,
+ "What is the capital of France?",
+ top_n = 5)
Apply more advanced retrieval with question rephrasing and reranking (requires COHERE_API_KEY). We will obtain top 100 chunks from embeddings (top_k) and top 5 chunks from reranking (top_n).
Evaluates a vector of QAEvalItems and returns a vector QAEvalResult. This function assesses the relevance and accuracy of the answers generated in a QA evaluation context.
See ?run_qa_evals for more details.
Arguments
qa_items::AbstractVector{<:QAEvalItem}: The vector of QA evaluation items containing the questions and their answers.
verbose::Bool: If true, enables verbose logging. Defaults to true.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API calls. See ?aiextract for details.
airag_kwargs::NamedTuple: Parameters that will be forwarded to airag calls. See ?airag for details.
qa_evals_kwargs::NamedTuple: Parameters that will be forwarded to run_qa_evals calls. See ?run_qa_evals for details.
parameters_dict::Dict{Symbol, Any}: Track any parameters used for later evaluations. Keys must be Symbols.
Returns
Vector{QAEvalResult}: Vector of evaluation results that includes various scores and metadata related to the QA evaluation.
Example
julia
index = "..." # Assuming a proper index is defined
+qa_items = [QAEvalItem(question="What is the capital of France?", answer="Paris", context="France is a country in Europe."),
+ QAEvalItem(question="What is the capital of Germany?", answer="Berlin", context="Germany is a country in Europe.")]
+
+# Let's run a test with `top_k=5`
+results = run_qa_evals(index, qa_items; airag_kwargs=(;top_k=5), parameters_dict=Dict(:top_k => 5))
+
+# Filter out the "failed" calls
+results = filter(x->!isnothing(x.answer_score), results);
+
+# See average judge score
+mean(x->x.answer_score, results)
Evaluates a single QAEvalItem using RAG details (RAGResult) and returns a QAEvalResult structure. This function assesses the relevance and accuracy of the answers generated in a QA evaluation context.
Arguments
qa_item::QAEvalItem: The QA evaluation item containing the question and its answer.
ctx::RAGResult: The RAG result used for generating the QA pair, including the original context and the answers. Comes from airag(...; return_context=true)
verbose::Bool: If true, enables verbose logging. Defaults to true.
parameters_dict::Dict{Symbol, Any}: Track any parameters used for later evaluations. Keys must be Symbols.
judge_template::Symbol: The template symbol for the AI model used to judge the answer. Defaults to :RAGJudgeAnswerFromContext.
model_judge::AbstractString: The AI model used for judging the answer's quality. Defaults to standard chat model, but it is advisable to use more powerful model GPT-4.
api_kwargs::NamedTuple: Parameters that will be forwarded to the API endpoint.
Returns
QAEvalResult: An evaluation result that includes various scores and metadata related to the QA evaluation.
Notes
The function computes a retrieval score and rank based on how well the context matches the QA context.
It then uses the judge_template and model_judge to score the answer's accuracy and relevance.
In case of errors during evaluation, the function logs a warning (if verbose is true) and the answer_score will be set to nothing.
Examples
Evaluating a QA pair using a specific context and model:
julia
qa_item = QAEvalItem(question="What is the capital of France?", answer="Paris", context="France is a country in Europe.")
+ctx = RAGResult(source="Wikipedia", context="France is a country in Europe.", answer="Paris")
+parameters_dict = Dict("param1" => "value1", "param2" => "value2")
+
+eval_result = run_qa_evals(qa_item, ctx, parameters_dict=parameters_dict, model_judge="MyAIJudgeModel")
Splits text block into code or text and sub-splits into units.
If code block, it splits by newline but keep the group_id the same (to have the same source) If text block, splits into sentences, bullets, etc., provides different group_id (to have different source)
Joins the three tokens together. Useful to add boundary tokens (like spaces vs brackets) to the curr_token to improve the matched context (ie, separate partial matches from exact match)