diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 0000000..bc116dd --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,4 @@ +self-hosted-runner: + # Labels of self-hosted runner in array of string + labels: + - warp-ubuntu-latest-x64-4x diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 336cc94..29f46de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,12 +8,12 @@ on: - reopened - ready_for_review paths: - - '**.go' - - '**.ts' - - '**.json' + - "**.go" + - "**.ts" + - "**.json" - .github/workflows/* schedule: - - cron: "0 5 * * *" + - cron: 0 5 * * * permissions: contents: read @@ -30,7 +30,9 @@ jobs: - name: Search for go.mod and package.json files id: get-dirs - run: echo "dirs=$(find . \( -name 'go.mod' -o -name 'package.json' \) -exec dirname {} \; | sed 's|^\./||' | jq -Rsc 'split("\n")[:-1]')" >> ${GITHUB_OUTPUT} + run: + echo "dirs=$(find . \( -name 'go.mod' -o -name 'package.json' \) -exec dirname {} \; | sed + 's|^\./||' | jq -Rsc 'split("\n")[:-1]')" >> ${GITHUB_OUTPUT} build: needs: get-dirs @@ -57,7 +59,7 @@ jobs: - name: Setup TinyGo uses: acifani/setup-tinygo@v2 with: - tinygo-version: "0.34.0" + tinygo-version: 0.34.0 - name: Build project run: npx -p @hypermode/modus-cli -y modus build diff --git a/dgraph-101/api-as/.gitignore b/.gitignore similarity index 72% rename from dgraph-101/api-as/.gitignore rename to .gitignore index f9d29f7..98dcb05 100644 --- a/dgraph-101/api-as/.gitignore +++ b/.gitignore @@ -13,3 +13,8 @@ node_modules/ # Ignore logs generated by as-test logs/ + +# Ignore Go debuger and generated files +__debug_bin* +*_generated.go +*.generated.go \ No newline at end of file diff --git a/.trunk/.gitignore b/.trunk/.gitignore new file mode 100644 index 0000000..15966d0 --- /dev/null +++ b/.trunk/.gitignore @@ -0,0 +1,9 @@ +*out +*logs +*actions +*notifications +*tools +plugins +user_trunk.yaml +user.yaml +tmp diff --git a/.trunk/configs/.markdownlint.json b/.trunk/configs/.markdownlint.json new file mode 100644 index 0000000..3d219a4 --- /dev/null +++ b/.trunk/configs/.markdownlint.json @@ -0,0 +1,6 @@ +{ + "line-length": { "line_length": 150, "tables": false }, + "no-inline-html": false, + "no-bare-urls": false, + "no-space-in-emphasis": false +} diff --git a/.trunk/configs/.prettierrc b/.trunk/configs/.prettierrc new file mode 100644 index 0000000..7497b18 --- /dev/null +++ b/.trunk/configs/.prettierrc @@ -0,0 +1,6 @@ +{ + "plugins": ["assemblyscript-prettier"], + "semi": false, + "proseWrap": "always", + "printWidth": 100 +} diff --git a/.trunk/configs/.shellcheckrc b/.trunk/configs/.shellcheckrc new file mode 100644 index 0000000..8c7b1ad --- /dev/null +++ b/.trunk/configs/.shellcheckrc @@ -0,0 +1,7 @@ +enable=all +source-path=SCRIPTDIR +disable=SC2154 + +# If you're having issues with shellcheck following source, disable the errors via: +# disable=SC1090 +# disable=SC1091 diff --git a/.trunk/configs/.yamllint.yaml b/.trunk/configs/.yamllint.yaml new file mode 100644 index 0000000..184e251 --- /dev/null +++ b/.trunk/configs/.yamllint.yaml @@ -0,0 +1,7 @@ +rules: + quoted-strings: + required: only-when-needed + extra-allowed: ["{|}"] + key-duplicates: {} + octal-values: + forbid-implicit-octal: true diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml new file mode 100644 index 0000000..9eb5a38 --- /dev/null +++ b/.trunk/trunk.yaml @@ -0,0 +1,43 @@ +# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml +version: 0.1 + +cli: + version: 1.22.8 + +plugins: + sources: + - id: trunk + ref: v1.6.6 + uri: https://github.com/trunk-io/plugins + +runtimes: + enabled: + - go@1.23.0 + - node@18.20.5 + - python@3.10.8 + +lint: + enabled: + - actionlint@1.7.4 + - checkov@3.2.334 + - git-diff-check + - gofmt@1.20.4 + - golangci-lint@1.62.2 + - markdownlint@0.43.0 + - osv-scanner@1.9.1 + - oxipng@9.1.3 + - prettier@3.4.2: + packages: + - assemblyscript-prettier@3.0.1 + - renovate@39.62.2 + - shellcheck@0.10.0 + - shfmt@3.6.0 + - trufflehog@3.86.0 + - yamllint@1.35.1 + +actions: + enabled: + - trunk-announce + - trunk-check-pre-push + - trunk-fmt-pre-commit + - trunk-upgrade-available diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..29d4338 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["trunk.io"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..96f2446 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "trunk.io", + "editor.trimAutoWhitespace": true +} diff --git a/README.md b/README.md index 4be6eee..b68680a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # Modus Recipes -Code recipes for cooking with [Modus](https://github.com/hypermodeinc/modus), the open source serverless framework for building intelligent APIs, powered by WebAssembly. +Code recipes for cooking with [Modus](https://github.com/hypermodeinc/modus), the open source +serverless framework for building intelligent APIs, powered by WebAssembly. ## 🚀 Getting started with Modus -For more information on getting started with Modus, check out [the docs](https://docs.hypermode.com/modus/overview). +For more information on getting started with Modus, check out +[the docs](https://docs.hypermode.com/modus/overview). ## 📺 Video walkthroughs diff --git a/dgraph-101/README.md b/dgraph-101/README.md index 35141bb..3c617af 100644 --- a/dgraph-101/README.md +++ b/dgraph-101/README.md @@ -1,6 +1,6 @@ # dgraph with modus 101 -From ModusHack livestream - 11/19/24 . +From ModusHack livestream - 11/19/24 . ## Dgraph @@ -11,16 +11,17 @@ Start a local instance using Docker. ``` Start Ratel, a Graphical data explorer for Dgraph: + ```sh docker run --name ratel -d -p "8000:8000" dgraph/ratel:latest ``` ## Data model -Modus is code first, simply define you data using classes. -In this example we are using `Product` and `Category` defined in `classes.ts` file. +Modus is code first, simply define you data using classes. In this example we are using `Product` +and `Category` defined in `classes.ts` file. -## Define your API. +## Define your API index.ts is where we export the functions that are exposed as GraphQL operations. @@ -33,12 +34,9 @@ This project defines the following operations: Note that Modus is exposing `getProduct` as `product` to follow common coding and GraphQL practices. Modus also exposes `upsertProduct` and `deleteProduct` as a GraphQL Mutation automatically. -- getProductsByCategory -The data is saved in Dgraph as a graph. -This function shows how to easily use the relationships in the graph. - -- searchProducts -Finally we are using an AI model to create text embeddings for our Products: as the embeddings are stored in Dgraph for each Product entity, we can easily expose an API to search by natural language and similarity, leveraging DQL `similar_to` function. - - +- getProductsByCategory The data is saved in Dgraph as a graph. This function shows how to easily + use the relationships in the graph. +- searchProducts Finally we are using an AI model to create text embeddings for our Products: as the + embeddings are stored in Dgraph for each Product entity, we can easily expose an API to search by + natural language and similarity, leveraging DQL `similar_to` function. diff --git a/dgraph-101/api-as/.prettierrc b/dgraph-101/api-as/.prettierrc deleted file mode 100644 index 64cb35c..0000000 --- a/dgraph-101/api-as/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["assemblyscript-prettier"] -} diff --git a/dgraph-101/api-as/assembly/classes.ts b/dgraph-101/api-as/assembly/classes.ts index 930d441..de4f256 100644 --- a/dgraph-101/api-as/assembly/classes.ts +++ b/dgraph-101/api-as/assembly/classes.ts @@ -1,4 +1,3 @@ - /* This file contains the classes that are used in our App. The classes are annotated with the @json decorator @@ -6,26 +5,30 @@ @alias is used to rename the properties in the json string to match Dgraph best practices. */ - @json export class Product { - + @alias("Product.id") - id!: string; - + id!: string + + @alias("Product.title") - title: string = ""; + title: string = "" + @alias("Product.description") - description: string = ""; + description: string = "" + @alias("Product.category") @omitnull() - category: Category | null = null; + category: Category | null = null } + @json export class Category { + @alias("Category.name") - name: string = ""; -} \ No newline at end of file + name: string = "" +} diff --git a/dgraph-101/api-as/assembly/dgraph-utils.ts b/dgraph-101/api-as/assembly/dgraph-utils.ts index 0fbf2e3..74208d1 100644 --- a/dgraph-101/api-as/assembly/dgraph-utils.ts +++ b/dgraph-101/api-as/assembly/dgraph-utils.ts @@ -3,46 +3,54 @@ */ import { dgraph } from "@hypermode/modus-sdk-as" import { JSON } from "json-as" -import { JSON as JSON_TREE} from "assemblyscript-json/assembly/index" +import { JSON as JSON_TREE } from "assemblyscript-json/assembly/index" + @json class Uid { - uid: string = ""; + uid: string = "" } -@json + + +@json class UidResult { - uids: Uid[] = []; + uids: Uid[] = [] } -@json + + +@json export class ListOf { - list: T[] = []; + list: T[] = [] } export class Relationship { - predicate!: string; - type!: string; + predicate!: string + type!: string } export class NodeType { - id_field: string = "" - relationships : Relationship[] = []; + id_field: string = "" + relationships: Relationship[] = [] } export class GraphSchema { - node_types: Map = new Map(); + node_types: Map = new Map() } - /** * modify the json payload to add the uid of the root and the nested entities when they exist in Dgraph * for non existing entities, a blank uid is generated helping the interpretation of mutation response */ -export function injectNodeUid(connection:string, payload: string, root_type:string, schema: GraphSchema): string { - - const root_node_type = schema.node_types.get(root_type); +export function injectNodeUid( + connection: string, + payload: string, + root_type: string, + schema: GraphSchema, +): string { + const root_node_type = schema.node_types.get(root_type) - const root = (JSON_TREE.parse(payload)); - injectNodeType(connection, root, root_type, schema); + const root = JSON_TREE.parse(payload) + injectNodeType(connection, root, root_type, schema) - console.log(root.toString()) + console.log(root.toString()) return root.toString() } @@ -50,74 +58,91 @@ export function injectNodeUid(connection:string, payload: string, root_type:stri /** * recursively inject the dgraph.type and uid of the entities in the json tree */ -function injectNodeType(connection: string, entity: JSON_TREE.Obj | null, type: string, schema: GraphSchema): void { - - if (entity != null) { - entity.set("dgraph.type", type); - const node_type = schema.node_types.get(type); - for (var i = 0; i < node_type.relationships.length; i++) { - const predicate = node_type.relationships[i].predicate; - const type = node_type.relationships[i].type; - injectNodeType(connection, entity.getObj(predicate), type, schema); - } +function injectNodeType( + connection: string, + entity: JSON_TREE.Obj | null, + type: string, + schema: GraphSchema, +): void { + if (entity != null) { + entity.set("dgraph.type", type) + const node_type = schema.node_types.get(type) + for (var i = 0; i < node_type.relationships.length; i++) { + const predicate = node_type.relationships[i].predicate + const type = node_type.relationships[i].type + injectNodeType(connection, entity.getObj(predicate), type, schema) + } - const id_field = entity.getString(node_type.id_field) - if (id_field != null) { - const id_value = entity.getString(node_type.id_field)!.toString(); - if (id_value != null) { - const node_uid = getEntityUid(connection,`${node_type.id_field}`, id_value); - if (node_uid != null) { - entity.set("uid", node_uid); - } else { - entity.set("uid", `_:${type}-${id_value}`); - } + const id_field = entity.getString(node_type.id_field) + if (id_field != null) { + const id_value = entity.getString(node_type.id_field)!.toString() + if (id_value != null) { + const node_uid = getEntityUid(connection, `${node_type.id_field}`, id_value) + if (node_uid != null) { + entity.set("uid", node_uid) + } else { + entity.set("uid", `_:${type}-${id_value}`) } } } + } } -export function getEntityById(connection: string, predicate: string, id: string, body: string): T | null{ - const query = new dgraph.Query(`{ +export function getEntityById( + connection: string, + predicate: string, + id: string, + body: string, +): T | null { + const query = new dgraph.Query(`{ list(func: eq(${predicate}, "${id}")) { ${body} } - }`); - const response = dgraph.execute(connection, new dgraph.Request(query)); - const data = JSON.parse>(response.Json); - if (data.list.length > 0) { - return data.list[0]; - } - return null; + }`) + const response = dgraph.execute(connection, new dgraph.Request(query)) + const data = JSON.parse>(response.Json) + if (data.list.length > 0) { + return data.list[0] } + return null +} -function getEntityUid(connection: string,predicate:string, value: string): string | null{ - - const query = new dgraph.Query(`{uids(func: eq(${predicate}, "${value}")) {uid}}`); - const response = dgraph.execute(connection, new dgraph.Request(query )) - const data = JSON.parse(response.Json) - if (data.uids.length == 0) { - return null - } - console.log(`${predicate} Uid: ${data.uids[0].uid}`) - return data.uids[0].uid +function getEntityUid(connection: string, predicate: string, value: string): string | null { + const query = new dgraph.Query(`{uids(func: eq(${predicate}, "${value}")) {uid}}`) + const response = dgraph.execute(connection, new dgraph.Request(query)) + const data = JSON.parse(response.Json) + if (data.uids.length == 0) { + return null } + console.log(`${predicate} Uid: ${data.uids[0].uid}`) + return data.uids[0].uid +} - export function deleteNodePredicates(connection:string, criteria: string, predicates: string[]): void { - - const query = new dgraph.Query(`{ +export function deleteNodePredicates( + connection: string, + criteria: string, + predicates: string[], +): void { + const query = new dgraph.Query(`{ node as var(func: ${criteria}) - }`); - predicates.push("dgraph.type"); - const del_nquads = predicates.map((predicate) => `uid(node) <${predicate}> * .`).join("\n"); - const mutation = new dgraph.Mutation("","","",del_nquads); - - dgraph.execute(connection, new dgraph.Request(query, [mutation])); - + }`) + predicates.push("dgraph.type") + const del_nquads = predicates + .map((predicate) => `uid(node) <${predicate}> * .`) + .join("\n") + const mutation = new dgraph.Mutation("", "", "", del_nquads) + + dgraph.execute(connection, new dgraph.Request(query, [mutation])) } -export function searchBySimilarity(connection:string, embedding: f32[],predicate:string, body:string, topK: i32): T[]{ - - const query = ` +export function searchBySimilarity( + connection: string, + embedding: f32[], + predicate: string, + body: string, + topK: i32, +): T[] { + const query = ` query search($vector: float32vector) { var(func: similar_to(${predicate},${topK},$vector)) { vemb as Product.embedding @@ -129,20 +154,19 @@ export function searchBySimilarity(connection:string, embedding: f32[],predic ${body} } }` - const vars = new dgraph.Variables(); - vars.set("$vector", JSON.stringify(embedding)); - - const dgraph_query = new dgraph.Query(query,vars); - - const response = dgraph.execute(connection, new dgraph.Request(dgraph_query)); - console.log(response.Json) - return JSON.parse>(response.Json).list - } + const vars = new dgraph.Variables() + vars.set("$vector", JSON.stringify(embedding)) + const dgraph_query = new dgraph.Query(query, vars) - export function addEmbeddingToJson(payload: string, predicate:string, embedding: f32[]): string { - // Add the embedding to the payload at root level - // TO DO: extend to nested entities and use JSONpath - payload = payload.replace("{", `{ \"${predicate}\":\"${JSON.stringify(embedding)}\",`) - return payload; - } \ No newline at end of file + const response = dgraph.execute(connection, new dgraph.Request(dgraph_query)) + console.log(response.Json) + return JSON.parse>(response.Json).list +} + +export function addEmbeddingToJson(payload: string, predicate: string, embedding: f32[]): string { + // Add the embedding to the payload at root level + // TO DO: extend to nested entities and use JSONpath + payload = payload.replace("{", `{ \"${predicate}\":\"${JSON.stringify(embedding)}\",`) + return payload +} diff --git a/dgraph-101/api-as/assembly/embeddings.ts b/dgraph-101/api-as/assembly/embeddings.ts index e68dc5d..5d30193 100644 --- a/dgraph-101/api-as/assembly/embeddings.ts +++ b/dgraph-101/api-as/assembly/embeddings.ts @@ -1,12 +1,11 @@ -import { models } from "@hypermode/modus-sdk-as"; -import { EmbeddingsModel } from "@hypermode/modus-sdk-as/models/experimental/embeddings"; - -const EMBEDDING_MODEL = "minilm"; +import { models } from "@hypermode/modus-sdk-as" +import { EmbeddingsModel } from "@hypermode/modus-sdk-as/models/experimental/embeddings" +const EMBEDDING_MODEL = "minilm" export function embedText(content: string[]): f32[][] { - const model = models.getModel(EMBEDDING_MODEL); - const input = model.createInput(content); - const output = model.invoke(input); - return output.predictions; + const model = models.getModel(EMBEDDING_MODEL) + const input = model.createInput(content) + const output = model.invoke(input) + return output.predictions } diff --git a/dgraph-101/api-as/assembly/index.ts b/dgraph-101/api-as/assembly/index.ts index 74425a7..64ba328 100644 --- a/dgraph-101/api-as/assembly/index.ts +++ b/dgraph-101/api-as/assembly/index.ts @@ -3,30 +3,35 @@ import { dgraph } from "@hypermode/modus-sdk-as" import { Product } from "./classes" import { embedText } from "./embeddings" import { buildProductMutationJson } from "./product-helpers" -import { deleteNodePredicates, ListOf, searchBySimilarity, getEntityById , addEmbeddingToJson} from "./dgraph-utils" +import { + deleteNodePredicates, + ListOf, + searchBySimilarity, + getEntityById, + addEmbeddingToJson, +} from "./dgraph-utils" -const DGRAPH_CONNECTION="dgraph-grpc" +const DGRAPH_CONNECTION = "dgraph-grpc" /** * Add or update a new product to the database */ export function upsertProduct(product: Product): Map | null { - - var payload = buildProductMutationJson(DGRAPH_CONNECTION,product); + var payload = buildProductMutationJson(DGRAPH_CONNECTION, product) - const embedding = embedText([product.description])[0]; - payload = addEmbeddingToJson(payload, "Product.embedding",embedding); - - const mutations: dgraph.Mutation[] = [new dgraph.Mutation(payload)]; - const uids = dgraph.execute(DGRAPH_CONNECTION, new dgraph.Request(null, mutations)).Uids; - - return uids; + const embedding = embedText([product.description])[0] + payload = addEmbeddingToJson(payload, "Product.embedding", embedding) + + const mutations: dgraph.Mutation[] = [new dgraph.Mutation(payload)] + const uids = dgraph.execute(DGRAPH_CONNECTION, new dgraph.Request(null, mutations)).Uids + + return uids } /** * Get a product info by its id */ -export function getProduct(id: string): Product | null{ +export function getProduct(id: string): Product | null { const body = ` Product.id Product.description @@ -34,14 +39,18 @@ export function getProduct(id: string): Product | null{ Product.category { Category.name }` - return getEntityById(DGRAPH_CONNECTION, "Product.id", id, body); + return getEntityById(DGRAPH_CONNECTION, "Product.id", id, body) } -/** +/** * Delete a product by its id */ export function deleteProduct(id: string): void { - deleteNodePredicates(DGRAPH_CONNECTION, `eq(Product.id, "${id}")`, ["Product.id", "Product.description", "Product.category"]); + deleteNodePredicates(DGRAPH_CONNECTION, `eq(Product.id, "${id}")`, [ + "Product.id", + "Product.description", + "Product.category", + ]) } /** @@ -59,22 +68,21 @@ export function getProductsByCategory(category: string): Product[] { } } } - }`); - const response = dgraph.execute(DGRAPH_CONNECTION, new dgraph.Request(query)); - const data = JSON.parse>>(response.Json); + }`) + const response = dgraph.execute(DGRAPH_CONNECTION, new dgraph.Request(query)) + const data = JSON.parse>>(response.Json) if (data.list.length > 0) { - return data.list[0].list; - } - return []; + return data.list[0].list + } + return [] } - /** * Search products by similarity to a given text */ -export function searchProducts(search: string): Product[]{ - const embedding = embedText([search])[0]; - const topK = 3; +export function searchProducts(search: string): Product[] { + const embedding = embedText([search])[0] + const topK = 3 const body = ` Product.id Product.description @@ -83,10 +91,5 @@ export function searchProducts(search: string): Product[]{ Category.name } ` - return searchBySimilarity(DGRAPH_CONNECTION,embedding,"Product.embedding",body, topK); - + return searchBySimilarity(DGRAPH_CONNECTION, embedding, "Product.embedding", body, topK) } - - - - diff --git a/dgraph-101/api-as/assembly/product-helpers.ts b/dgraph-101/api-as/assembly/product-helpers.ts index abae8c6..2f5fa92 100644 --- a/dgraph-101/api-as/assembly/product-helpers.ts +++ b/dgraph-101/api-as/assembly/product-helpers.ts @@ -3,26 +3,23 @@ */ import { JSON } from "json-as" import { Product } from "./classes" -import { injectNodeUid, GraphSchema } from "./dgraph-utils" +import { injectNodeUid, GraphSchema } from "./dgraph-utils" -const product_schema : GraphSchema = new GraphSchema(); +const product_schema: GraphSchema = new GraphSchema() product_schema.node_types.set("Product", { id_field: "Product.id", - relationships: [ - {predicate: "Product.category", type: "Category"} - ] -}); + relationships: [{ predicate: "Product.category", type: "Category" }], +}) product_schema.node_types.set("Category", { id_field: "Category.name", - relationships: [] -}); + relationships: [], +}) -export function buildProductMutationJson(connection:string, product: Product): string { - var payload = JSON.stringify(product); - - payload = injectNodeUid(connection,payload, "Product", product_schema); - - return payload; - - } \ No newline at end of file +export function buildProductMutationJson(connection: string, product: Product): string { + var payload = JSON.stringify(product) + + payload = injectNodeUid(connection, payload, "Product", product_schema) + + return payload +} diff --git a/dgraph-101/api-as/eslint.config.js b/dgraph-101/api-as/eslint.config.js index 7ad50ae..820e0f6 100644 --- a/dgraph-101/api-as/eslint.config.js +++ b/dgraph-101/api-as/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, aseslint.config, -); +) diff --git a/dgraph-101/api-as/modus.json b/dgraph-101/api-as/modus.json index af515de..4d0d07f 100644 --- a/dgraph-101/api-as/modus.json +++ b/dgraph-101/api-as/modus.json @@ -7,11 +7,11 @@ "auth": "bearer-token" } }, - "models" : { - "minilm":{ - "sourceModel": "sentence-transformers/all-MiniLM-L6-v2", - "connection": "hypermode", - "provider": "hugging-face" + "models": { + "minilm": { + "sourceModel": "sentence-transformers/all-MiniLM-L6-v2", + "connection": "hypermode", + "provider": "hugging-face" } }, "connections": { diff --git a/dgraph-101/extras/deployschema.sh b/dgraph-101/extras/deployschema.sh index 9dfba3f..6383cb6 100755 --- a/dgraph-101/extras/deployschema.sh +++ b/dgraph-101/extras/deployschema.sh @@ -1 +1,2 @@ -curl -X POST localhost:8080/alter --silent --data-binary '@dqlschema.txt' +#!/bin/bash +curl -X POST localhost:8080/alter --silent --data-binary '@dqlschema.txt' diff --git a/dgraph-101/extras/ratel-queries.md b/dgraph-101/extras/ratel-queries.md index 7444427..d4ae810 100644 --- a/dgraph-101/extras/ratel-queries.md +++ b/dgraph-101/extras/ratel-queries.md @@ -1,8 +1,6 @@ +# Queries to copy/paste in ratel - -# Queries to copy/paste in ratel - -``` +```json { product(func:type(Product)) { id:Product.id @@ -16,13 +14,13 @@ vector search -``` +```json query search($vector: float32vector) { - var(func: similar_to(Product.embedding,3,$vector)) { - vemb as Product.embedding + var(func: similar_to(Product.embedding,3,$vector)) { + vemb as Product.embedding dist as math((vemb - $vector) dot (vemb - $vector)) - } - list(func:uid(dist),orderdesc:val(dist)) { + } + list(func:uid(dist),orderdesc:val(dist)) { score:math(1 - (dist / 2.0)) Product.id Product.description @@ -30,7 +28,7 @@ query search($vector: float32vector) { Product.category { Category.name } - + } } -``` \ No newline at end of file +``` diff --git a/function-calling/README.md b/function-calling/README.md index ba0e198..69f5d20 100644 --- a/function-calling/README.md +++ b/function-calling/README.md @@ -1,55 +1,56 @@ # Function Calling With Modus -LLM apis such as OpenAI, have a feature called **function calling** or **tool use**. With this feature the LLM response to a chat message could be a request to invoke a function, usually in order to collect information necessary to generate a response. +LLM apis such as OpenAI, have a feature called **function calling** or **tool use**. With this +feature the LLM response to a chat message could be a request to invoke a function, usually in order +to collect information necessary to generate a response. -This project demonstrates how to setup function calling within [Modus](https://docs.hypermode.com/modus), the open source framework for building intelligent APIs. +This project demonstrates how to setup function calling within +[Modus](https://docs.hypermode.com/modus), the open source framework for building intelligent APIs. -The example implements a function `askQuestionToWarehouse` accepting an query in natural language about prices or stock of goods in the warehouse. +The example implements a function `askQuestionToWarehouse` accepting an query in natural language +about prices or stock of goods in the warehouse. The API uses 2 tools available for the LLM -- get_product_types: provide the list of product types we have in the warehouse -- get_product_info: return an info (qty or price) about one product type - +- get_product_types: provide the list of product types we have in the warehouse +- get_product_info: return an info (qty or price) about one product type ## Get started 1- Set your credentials Create the file `.env.dev.local` in `api-as` folder, containing your OpenAI API key: -``` + +```bash MODUS_OPENAI_API_KEY="sk-...." ``` 2- launch the API From `api-as` folder launch -``` + +```bash modus dev ``` -3- Test the GraphQL operation -From a GraphQL client (Postman), -Introspect the GraphQL endpoint `http://localhost:8686/graphql` -Invoke the operation `askQuestionToWarehouse` +3- Test the GraphQL operation From a GraphQL client (Postman), Introspect the GraphQL endpoint +`http://localhost:8686/graphql` Invoke the operation `askQuestionToWarehouse` ```graphql # example query using tool calling query AskQuestion { - askQuestionToWarehouse( - question: "What is the most expensive product?") { - response - logs - } + askQuestionToWarehouse(question: "What is the most expensive product?") { + response + logs + } } - ``` -The operation returns the final response and an array of strings showing showing the tool calls and messages exchanged with the LLM API. - -Experiment with some queries to see the function calling at work. +The operation returns the final response and an array of strings showing showing the tool calls and +messages exchanged with the LLM API. +Experiment with some queries to see the function calling at work. ```text # example of questions @@ -62,27 +63,29 @@ What is the most expensive product in stock? ``` - - ## Details The logic is as follow: -- Instruct the LLM to use function calls (tools) with the correct parameters to get the data necessary to reply to the provided question. +- Instruct the LLM to use function calls (tools) with the correct parameters to get the data + necessary to reply to the provided question. - Execute the identified function calls in Modus to build an additional context (tool messages) -- Re-invoke the LLM API with the additional tool messages. +- Re-invoke the LLM API with the additional tool messages. Return the generated responses based on the data retrieved by the function calls. ## Discussion + Correct prompt helps to address questions that are out of scope. Descriptions of function and parameters are also part of the prompt engineering! -Enums parameter can help. Try replacing the `product_name` parameter by an Enum type and see that the LLM can skip a function call. +Enums parameter can help. Try replacing the `product_name` parameter by an Enum type and see that +the LLM can skip a function call. Need a way to avoid loops. That's why we have a limit to 3 calls. -Need to experiment more to understand what are good functions in terms of abstraction, number of parameters etc ... +Need to experiment more to understand what are good functions in terms of abstraction, number of +parameters etc ... diff --git a/function-calling/api-as/.gitignore b/function-calling/api-as/.gitignore deleted file mode 100644 index f9d29f7..0000000 --- a/function-calling/api-as/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Ignore macOS system files -.DS_Store - -# Ignore environment variable files -.env -.env.* - -# Ignore build output directories -build/ - -# Ignore node_modules folders -node_modules/ - -# Ignore logs generated by as-test -logs/ diff --git a/function-calling/api-as/.prettierrc b/function-calling/api-as/.prettierrc deleted file mode 100644 index 64cb35c..0000000 --- a/function-calling/api-as/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["assemblyscript-prettier"] -} diff --git a/function-calling/api-as/assembly/index.ts b/function-calling/api-as/assembly/index.ts index 292b73f..6112f29 100644 --- a/function-calling/api-as/assembly/index.ts +++ b/function-calling/api-as/assembly/index.ts @@ -7,14 +7,14 @@ import { ToolMessage, ResponseFormat, CompletionMessage, -} from "@hypermode/modus-sdk-as/models/openai/chat"; -import { EnumParam, StringParam, ObjectParam } from "./params"; -import { get_product_info, get_product_types } from "./warehouse"; -import { models } from "@hypermode/modus-sdk-as"; -import { llmWithTools, ResponseWithLogs } from "./tool-helper"; -import { JSON } from "json-as"; +} from "@hypermode/modus-sdk-as/models/openai/chat" +import { EnumParam, StringParam, ObjectParam } from "./params" +import { get_product_info, get_product_types } from "./warehouse" +import { models } from "@hypermode/modus-sdk-as" +import { llmWithTools, ResponseWithLogs } from "./tool-helper" +import { JSON } from "json-as" -const MODEL_NAME: string = "llm"; // refer to modus.json for the model specs +const MODEL_NAME: string = "llm" // refer to modus.json for the model specs const DEFAULT_PROMPT = ` You are a warehouse manager only answering questions about the stock and price of products in the warehouse. @@ -24,13 +24,13 @@ const DEFAULT_PROMPT = ` Reply to the user question using only the data provided by tools. If you have a doubt about a product, use the tool to get the list of product names. - `; + ` /** * Ask a natural language question to the warehouse, for example try asking about items that are in stock in the warehouse */ export function askQuestionToWarehouse(question: string): ResponseWithLogs { - const model = models.getModel(MODEL_NAME); - const loop_limit: u8 = 3; // maximum number of loops + const model = models.getModel(MODEL_NAME) + const loop_limit: u8 = 3 // maximum number of loops return llmWithTools( model, [tool_get_product_list(), tool_get_product_info()], @@ -38,16 +38,16 @@ export function askQuestionToWarehouse(question: string): ResponseWithLogs { question, executeToolCall, loop_limit, - ); + ) } function executeToolCall(toolCall: ToolCall): string { if (toolCall.function.name == "get_product_list") { - return get_product_types(); + return get_product_types() } else if (toolCall.function.name == "get_product_info") { - return get_product_info(toolCall.function.arguments); + return get_product_info(toolCall.function.arguments) } else { - return ""; + return "" } } @@ -57,21 +57,19 @@ function executeToolCall(toolCall: ToolCall): string { * set good function and parameter description to help the LLM understand the tool */ function tool_get_product_info(): Tool { - const get_product_info = new Tool(); - const param = new ObjectParam(); + const get_product_info = new Tool() + const param = new ObjectParam() //param.addRequiredProperty("product_name", new EnumParam(["Shoe", "Hat", "Trouser", "Shirt"],"One of the product in the warehouse.")); param.addRequiredProperty( "product_name", - new StringParam( - "One of the product in the warehouse like 'Shoe' or 'Hat'.", - ), - ); + new StringParam("One of the product in the warehouse like 'Shoe' or 'Hat'."), + ) param.addRequiredProperty( "attribute", new EnumParam(["qty", "price"], "The product information to return"), - ); + ) get_product_info.function = { name: "get_product_info", @@ -83,9 +81,9 @@ function tool_get_product_info(): Tool { // meaning openai expects all fields to be required parameters: param.toString(), strict: true, - }; + } - return get_product_info; + return get_product_info } /** @@ -94,14 +92,14 @@ function tool_get_product_info(): Tool { * set good function and parameter description to help the LLM understand the tool */ function tool_get_product_list(): Tool { - const get_product_list = new Tool(); + const get_product_list = new Tool() /* this function has no parameters */ get_product_list.function = { name: "get_product_list", description: `Get the list of product names in the warehouse. Call this whenever you need to know which product you are able to get information about.`, parameters: null, strict: false, - }; + } - return get_product_list; + return get_product_list } diff --git a/function-calling/api-as/assembly/params.ts b/function-calling/api-as/assembly/params.ts index 9bb442c..fdd136e 100644 --- a/function-calling/api-as/assembly/params.ts +++ b/function-calling/api-as/assembly/params.ts @@ -1,68 +1,79 @@ -/* +/* * Helper classes to create parameter json for function calling API */ -import { JSON } from "json-as"; +import { JSON } from "json-as" + @json class Param { - constructor(type: string, description: string| null = null) { - this._type = type; - this._description = description - } - toString(): string { - return JSON.stringify(this); - } - - @alias("type") - protected _type: string; - - @alias("description") - @omitnull() - protected _description: string | null; - - get type(): string { - return this._type; - } + constructor(type: string, description: string | null = null) { + this._type = type + this._description = description + } + toString(): string { + return JSON.stringify(this) + } + + @alias("type") + protected _type: string + + + @alias("description") + @omitnull() + protected _description: string | null + + get type(): string { + return this._type } +} + + @json export class ObjectParam extends Param { + constructor(description: string | null = null) { + super("object", description) + this.additionalProperties = false + } - constructor(description: string| null = null) { - super("object", description); - this.additionalProperties = false; + addRequiredProperty(name: string, param: Param): void { + if (this.properties == null) { + this.properties = new Map() } + this.properties!.set(name, param) - addRequiredProperty(name: string, param: Param): void { - if (this.properties == null) { - this.properties = new Map(); - } - this.properties!.set(name, param); - - if (this.required == null) { - this.required = []; - } - this.required!.push(name); + if (this.required == null) { + this.required = [] } - @omitnull() - properties: Map | null = null; - @omitnull() - required: string[] | null = null; - protected additionalProperties: boolean; + this.required!.push(name) + } + + + @omitnull() + properties: Map | null = null + + + @omitnull() + required: string[] | null = null + protected additionalProperties: boolean } + + @json export class EnumParam extends Param { - enum: string[]; - constructor(enumValues: string[], description: string| null = null) { - super("string", description); - this.enum = enumValues; - } + enum: string[] + constructor(enumValues: string[], description: string | null = null) { + super("string", description) + this.enum = enumValues + } } + + @json export class StringParam extends Param { - constructor(description: string| null = null) { - super("string", description); - } + constructor(description: string | null = null) { + super("string", description) + } } /* example of parameters value in JSON format @@ -85,4 +96,4 @@ export class StringParam extends Param { "required": ["product_name", "attribute"], "additionalProperties": false }`, - */ \ No newline at end of file + */ diff --git a/function-calling/api-as/assembly/tool-helper.ts b/function-calling/api-as/assembly/tool-helper.ts index 46d5368..e4c31df 100644 --- a/function-calling/api-as/assembly/tool-helper.ts +++ b/function-calling/api-as/assembly/tool-helper.ts @@ -7,15 +7,15 @@ import { ToolMessage, ResponseFormat, CompletionMessage, -} from "@hypermode/modus-sdk-as/models/openai/chat"; +} from "@hypermode/modus-sdk-as/models/openai/chat" /** * Final response and log of each prompt iteration with tool use */ @json export class ResponseWithLogs { - response: string = ""; - logs: string[] = []; + response: string = "" + logs: string[] = [] } export function llmWithTools( @@ -26,39 +26,32 @@ export function llmWithTools( toolCallBack: (toolCall: ToolCall) => string, limit: u8 = 3, ): ResponseWithLogs { - var logs: string[] = []; - var final_response = ""; - var tool_messages: ToolMessage[] = []; - var message: CompletionMessage | null = null; - var loops: u8 = 0; + var logs: string[] = [] + var final_response = "" + var tool_messages: ToolMessage[] = [] + var message: CompletionMessage | null = null + var loops: u8 = 0 // we loop until we get a response or we reach the maximum number of loops (3) do { - message = getLLMResponse( - model, - tools, - system_prompt, - question, - message, - tool_messages, - ); + message = getLLMResponse(model, tools, system_prompt, question, message, tool_messages) /* do we have a tool call to execute */ if (message.toolCalls.length > 0) { for (let i = 0; i < message.toolCalls.length; i++) { logs.push( `Calling function : ${message.toolCalls[i].function.name} with ${message.toolCalls[i].function.arguments}`, - ); + ) } - tool_messages = aggregateToolsResponse(message.toolCalls, toolCallBack); + tool_messages = aggregateToolsResponse(message.toolCalls, toolCallBack) for (let i = 0; i < tool_messages.length; i++) { - logs.push(`Tool response : ${tool_messages[i].content}`); + logs.push(`Tool response : ${tool_messages[i].content}`) } } else { - final_response = message.content; - break; + final_response = message.content + break } - } while (loops++ < limit - 1); + } while (loops++ < limit - 1) - return { response: final_response, logs: logs }; + return { response: final_response, logs: logs } } function getLLMResponse( @@ -69,28 +62,25 @@ function getLLMResponse( last_message: CompletionMessage | null = null, tools_messages: ToolMessage[] = [], ): CompletionMessage { - const input = model.createInput([ - new SystemMessage(system_prompt), - new UserMessage(question), - ]); + const input = model.createInput([new SystemMessage(system_prompt), new UserMessage(question)]) /* * adding tools messages (response from tools) to the input * first we need to add the last completion message so the LLM can match the tool messages with the tool call */ if (last_message != null) { - input.messages.push(last_message); + input.messages.push(last_message) } for (var i = 0; i < tools_messages.length; i++) { - input.messages.push(tools_messages[i]); + input.messages.push(tools_messages[i]) } - input.responseFormat = ResponseFormat.Text; - input.tools = tools; + input.responseFormat = ResponseFormat.Text + input.tools = tools - input.toolChoice = "auto"; // "auto "required" or "none" or a function in json format + input.toolChoice = "auto" // "auto "required" or "none" or a function in json format - const message = model.invoke(input).choices[0].message; - return message; + const message = model.invoke(input).choices[0].message + return message } /** @@ -101,11 +91,11 @@ function aggregateToolsResponse( toolCalls: ToolCall[], toolCallBack: (toolCall: ToolCall) => string, ): ToolMessage[] { - var messages: ToolMessage[] = []; + var messages: ToolMessage[] = [] for (let i = 0; i < toolCalls.length; i++) { - const content = toolCallBack(toolCalls[i]); - const toolCallResponse = new ToolMessage(content, toolCalls[i].id); - messages.push(toolCallResponse); + const content = toolCallBack(toolCalls[i]) + const toolCallResponse = new ToolMessage(content, toolCalls[i].id) + messages.push(toolCallResponse) } - return messages; + return messages } diff --git a/function-calling/api-as/assembly/warehouse.ts b/function-calling/api-as/assembly/warehouse.ts index 5ca489b..7691d4a 100644 --- a/function-calling/api-as/assembly/warehouse.ts +++ b/function-calling/api-as/assembly/warehouse.ts @@ -2,44 +2,46 @@ * A simple warehouse fake DB with product information. */ -import { JSON } from "json-as"; +import { JSON } from "json-as" class Product { - qty: u32 = 0; - price: string = ""; + qty: u32 = 0 + price: string = "" } /** * Get the list of available products. */ export function get_product_types(): string { - const product_list = productInfo.keys(); - - return `The available products are: ${product_list.join(", ")}` -} + const product_list = productInfo.keys() + + return `The available products are: ${product_list.join(", ")}` +} /** * Get the product information for a given product name. */ export function get_product_info(string_args: string): string { - const args = JSON.parse(string_args) - if (productInfo.has(args.product_name)) { - const product = productInfo.get(args.product_name) - const value = args.attribute == "qty" ? product.qty.toString() : product.price - return `The ${args.attribute} of ${args.product_name} is ${value}. ` - } - return `The product ${args.product_name} is not available. `+ get_product_types(); + const args = JSON.parse(string_args) + if (productInfo.has(args.product_name)) { + const product = productInfo.get(args.product_name) + const value = args.attribute == "qty" ? product.qty.toString() : product.price + return `The ${args.attribute} of ${args.product_name} is ${value}. ` + } + return `The product ${args.product_name} is not available. ` + get_product_types() } + + @json -export class GetProductArguments { - product_name: string=""; - attribute: string=""; +export class GetProductArguments { + product_name: string = "" + attribute: string = "" } /** * Our fake warehouse DB is a map of product name to product information. */ -const productInfo: Map = new Map(); -productInfo.set("Shoe", {qty: 10, price: "100"}); -productInfo.set("Hat", {qty: 20, price: "200"}); -productInfo.set("Trouser", {qty: 30, price: "300"}); -productInfo.set("Shirt", {qty: 40, price: "400"}); \ No newline at end of file +const productInfo: Map = new Map() +productInfo.set("Shoe", { qty: 10, price: "100" }) +productInfo.set("Hat", { qty: 20, price: "200" }) +productInfo.set("Trouser", { qty: 30, price: "300" }) +productInfo.set("Shirt", { qty: 40, price: "400" }) diff --git a/function-calling/api-as/eslint.config.js b/function-calling/api-as/eslint.config.js index 7ad50ae..820e0f6 100644 --- a/function-calling/api-as/eslint.config.js +++ b/function-calling/api-as/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, aseslint.config, -); +) diff --git a/instant-vector-search/.gitignore b/instant-vector-search/.gitignore deleted file mode 100644 index f9d29f7..0000000 --- a/instant-vector-search/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Ignore macOS system files -.DS_Store - -# Ignore environment variable files -.env -.env.* - -# Ignore build output directories -build/ - -# Ignore node_modules folders -node_modules/ - -# Ignore logs generated by as-test -logs/ diff --git a/instant-vector-search/.prettierrc b/instant-vector-search/.prettierrc deleted file mode 100644 index 64cb35c..0000000 --- a/instant-vector-search/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["assemblyscript-prettier"] -} diff --git a/instant-vector-search/README.md b/instant-vector-search/README.md index 4a5ae58..8db0183 100644 --- a/instant-vector-search/README.md +++ b/instant-vector-search/README.md @@ -1,6 +1,8 @@ # instant-vector-search -A simplified example demonstrating how to build instant vector search using Hypermode and Modus. This project showcases how to configure collections, embed data, and deploy APIs to perform real-time vector search with minimal setup. +A simplified example demonstrating how to build instant vector search using Hypermode and Modus. +This project showcases how to configure collections, embed data, and deploy APIs to perform +real-time vector search with minimal setup. ## Guide @@ -9,7 +11,8 @@ Check out the blog post on how to create a project using this example: ## Semantic Search in Action -This example demonstrates how instant vector search can enhance your application by generating embeddings from user queries and performing vector searches in less than 200ms. +This example demonstrates how instant vector search can enhance your application by generating +embeddings from user queries and performing vector searches in less than 200ms. ## How to Use the Template @@ -55,9 +58,11 @@ Once your app is deployed, upload your data to the texts collection to enable ve From the Hypermode Console: Upload a CSV directly using the dashboard. -Using the `upsertTexts` function: Upload data programmatically by calling the function from your code. +Using the `upsertTexts` function: Upload data programmatically by calling the function from your +code. -Once your data is in place, the embeddings will be generated automatically, and your system will be ready to handle real-time vector searches. +Once your data is in place, the embeddings will be generated automatically, and your system will be +ready to handle real-time vector searches. ## Making API Calls @@ -73,7 +78,9 @@ query { This template leverages Hypermode and Modus to power your backend functions: -[Hypermode](https://docs.hypermode.com/introduction) is a managed service that provides the infrastructure and tools for creating AI-powered applications, including assistants, APIs, and backend services. It offers: +[Hypermode](https://docs.hypermode.com/introduction) is a managed service that provides the +infrastructure and tools for creating AI-powered applications, including assistants, APIs, and +backend services. It offers: - Automatic building and deployment of functions with each git push - A live, scalable API for previewing, testing, and production @@ -81,7 +88,8 @@ This template leverages Hypermode and Modus to power your backend functions: - Integrated console for observability and control - GraphQL API generation for easy integration -[Modus](https://docs.hypermode.com/modus/overview) is an open-source, serverless framework that’s part of the Hypermode ecosystem. It focuses on: +[Modus](https://docs.hypermode.com/modus/overview) is an open-source, serverless framework that’s +part of the Hypermode ecosystem. It focuses on: - Building functions and APIs using WebAssembly - Supporting multiple programming languages (currently Go and AssemblyScript) diff --git a/instant-vector-search/assembly/index.ts b/instant-vector-search/assembly/index.ts index 7dc17fb..0c5f5db 100644 --- a/instant-vector-search/assembly/index.ts +++ b/instant-vector-search/assembly/index.ts @@ -1,66 +1,60 @@ -import { collections } from "@hypermode/modus-sdk-as"; -import { models } from "@hypermode/modus-sdk-as"; -import { EmbeddingsModel } from "@hypermode/modus-sdk-as/models/experimental/embeddings"; +import { collections } from "@hypermode/modus-sdk-as" +import { models } from "@hypermode/modus-sdk-as" +import { EmbeddingsModel } from "@hypermode/modus-sdk-as/models/experimental/embeddings" -const textsCollection = "texts"; -const searchMethod = "searchMethod1"; -const embeddingModelName = "minilm"; +const textsCollection = "texts" +const searchMethod = "searchMethod1" +const embeddingModelName = "minilm" /** * Add text(s) to the collection */ export function upsertTexts(ids: string[], texts: string[]): string[] { - const errors: string[] = []; + const errors: string[] = [] if (ids.length !== texts.length) { - errors.push("Length of all arrays must be the same"); - return errors; + errors.push("Length of all arrays must be the same") + return errors } - let result = collections.upsertBatch(textsCollection, ids, texts); + let result = collections.upsertBatch(textsCollection, ids, texts) if (!result.isSuccessful) { - errors.push(result.error); - return errors; + errors.push(result.error) + return errors } - return ids; + return ids } /** * Perform a vector search using an embedding of the input string */ export function search(query: string): string[] { - const searchResults = collections.search( - textsCollection, - searchMethod, - query, - 10, - true, - ); + const searchResults = collections.search(textsCollection, searchMethod, query, 10, true) if (!searchResults.isSuccessful) { - return [searchResults.error]; + return [searchResults.error] } - const searchTexts: string[] = []; + const searchTexts: string[] = [] for (let i = 0; i < searchResults.objects.length; i++) { - const obj = searchResults.objects[i]; - const text = collections.getText(textsCollection, obj.key); + const obj = searchResults.objects[i] + const text = collections.getText(textsCollection, obj.key) if (text) { - searchTexts.push(text); + searchTexts.push(text) } } - return searchTexts; + return searchTexts } /** * Embed the input text(s) using the miniLM embedding model */ export function miniLMEmbed(texts: string[]): f32[][] { - const model = models.getModel(embeddingModelName); - const input = model.createInput(texts); - const output = model.invoke(input); + const model = models.getModel(embeddingModelName) + const input = model.createInput(texts) + const output = model.invoke(input) - return output.predictions; + return output.predictions } diff --git a/instant-vector-search/eslint.config.js b/instant-vector-search/eslint.config.js index 7ad50ae..820e0f6 100644 --- a/instant-vector-search/eslint.config.js +++ b/instant-vector-search/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, aseslint.config, -); +) diff --git a/modus-getting-started/.gitignore b/modus-getting-started/.gitignore deleted file mode 100644 index f9d29f7..0000000 --- a/modus-getting-started/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Ignore macOS system files -.DS_Store - -# Ignore environment variable files -.env -.env.* - -# Ignore build output directories -build/ - -# Ignore node_modules folders -node_modules/ - -# Ignore logs generated by as-test -logs/ diff --git a/modus-getting-started/.prettierrc b/modus-getting-started/.prettierrc deleted file mode 100644 index 64cb35c..0000000 --- a/modus-getting-started/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["assemblyscript-prettier"] -} diff --git a/modus-getting-started/assembly/index.ts b/modus-getting-started/assembly/index.ts index 4c4783a..62498dd 100644 --- a/modus-getting-started/assembly/index.ts +++ b/modus-getting-started/assembly/index.ts @@ -1,26 +1,23 @@ -import { http, models } from "@hypermode/modus-sdk-as"; +import { http, models } from "@hypermode/modus-sdk-as" import { OpenAIChatModel, ResponseFormat, SystemMessage, UserMessage, -} from "@hypermode/modus-sdk-as/models/openai/chat"; +} from "@hypermode/modus-sdk-as/models/openai/chat" /** * Generate text from a LLM model */ export function generateText(instruction: string, prompt: string): string { - const model = models.getModel("text-generator"); + const model = models.getModel("text-generator") - const input = model.createInput([ - new SystemMessage(instruction), - new UserMessage(prompt), - ]); + const input = model.createInput([new SystemMessage(instruction), new UserMessage(prompt)]) - input.temperature = 0.7; - const output = model.invoke(input); + input.temperature = 0.7 + const output = model.invoke(input) - return output.choices[0].message.content.trim(); + return output.choices[0].message.content.trim() } /** @@ -30,32 +27,30 @@ export function generateText(instruction: string, prompt: string): string { class Quote { @alias("q") - quote!: string; + quote!: string @alias("a") - author!: string; + author!: string } /** * Fetch random quote from the Zenquotes API */ export function getRandomQuote(): Quote { - const request = new http.Request("https://zenquotes.io/api/random"); + const request = new http.Request("https://zenquotes.io/api/random") - const response = http.fetch(request); + const response = http.fetch(request) if (!response.ok) { - throw new Error( - `Failed to fetch quote. Received: ${response.status} ${response.statusText}`, - ); + throw new Error(`Failed to fetch quote. Received: ${response.status} ${response.statusText}`) } - return response.json()[0]; + return response.json()[0] } /** * Basic Hello World example */ export function sayHello(name: string | null = null): string { - return `Hello, ${name || "World"}!`; + return `Hello, ${name || "World"}!` } diff --git a/modus-getting-started/eslint.config.js b/modus-getting-started/eslint.config.js index 7ad50ae..820e0f6 100644 --- a/modus-getting-started/eslint.config.js +++ b/modus-getting-started/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, aseslint.config, -); +) diff --git a/modus-press/.gitignore b/modus-press/.gitignore deleted file mode 100644 index f9d29f7..0000000 --- a/modus-press/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Ignore macOS system files -.DS_Store - -# Ignore environment variable files -.env -.env.* - -# Ignore build output directories -build/ - -# Ignore node_modules folders -node_modules/ - -# Ignore logs generated by as-test -logs/ diff --git a/modus-press/.prettierrc b/modus-press/.prettierrc deleted file mode 100644 index 64cb35c..0000000 --- a/modus-press/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["assemblyscript-prettier"] -} diff --git a/modus-press/README.md b/modus-press/README.md index 0064d64..a9b55b1 100644 --- a/modus-press/README.md +++ b/modus-press/README.md @@ -1,58 +1,66 @@ # ModusPress -This example shows how to add LLM-backed features to a fictitious web-based blog platform using [Modus](https://docs.hypermode.com/modus), the open source framework for building intelligent APIs. Specifically, this example is a Modus app that exposes a GraphQL endpoint with the +This example shows how to add LLM-backed features to a fictitious web-based blog platform using +[Modus](https://docs.hypermode.com/modus), the open source framework for building intelligent APIs. +Specifically, this example is a Modus app that exposes a GraphQL endpoint with the - generate suggested blog post titles based on the blog post content, in the style of the author - generate HTML meta tags optimized for SEO based on the blog post content -![](img/ModusPressArch.png) +![solution architecture](img/ModusPressArch.png) ## Setup Install the Modus CLI (if not already installed): -``` +```bash npm i -g @hypermode/modus-cli ``` -**Seed Postgres DB** +### Seed Postgres DB -This example pulls author biography data from a Postgres database that represents the backend database for our blog platform. You'll need to create a Postgres database and seed it with author data using the following schema: +This example pulls author biography data from a Postgres database that represents the backend +database for our blog platform. You'll need to create a Postgres database and seed it with author +data using the following schema: -![](img/ModusPressPostgres.png) +![Postgres client](img/ModusPressPostgres.png) -**Set database credentials in .env** +### Set database credentials in .env -If using a local Postgres database with defaults your conection credentials will look something like this: +If using a local Postgres database with defaults your conection credentials will look something like +this: -``` +```text MODUS_MODUSPRESSDB_USERNAME= MODUS_MODUSPRESSDB_HOST=localhost MODUS_MODUSPRESSDB_PORT=5432 MODUS_MODUSPRESSDB_DBNAME= ``` -**Connect to Hypermode model hosting** +### Connect to Hypermode model hosting -This example uses the LLaMa open source LLM hosted on Hypermode. You can create a free Hypermode account to leverage model hosting using the Hypermode Platform with the `hyp` cli. +This example uses the LLaMa open source LLM hosted on Hypermode. You can create a free Hypermode +account to leverage model hosting using the Hypermode Platform with the `hyp` cli. Install `hyp` cli: -``` +```bash npm i -g @hypermode/hyp-cli ``` Login to Hypermode: -``` +```bash hyp login ``` -This command will open a web browser and prompt you to sign in or create a free Hypermode account, then select an organization. Once complete you will be able to use Hypermode hosted models in your Modus app. +This command will open a web browser and prompt you to sign in or create a free Hypermode account, +then select an organization. Once complete you will be able to use Hypermode hosted models in your +Modus app. ## Run -``` +```bash modus dev ``` @@ -60,6 +68,8 @@ This will build your Modus app and launch a GraphQL API at `localhost:8686/graph ## Query -You can query the GraphQL endpoint using any GraphQL client or cURL. Here we use Postman to query for suggested titles based on the blog post content and author bio retrived from Postres and then passed to the LLM. +You can query the GraphQL endpoint using any GraphQL client or cURL. Here we use Postman to query +for suggested titles based on the blog post content and author bio retrived from Postres and then +passed to the LLM. -![](img/ModusPressGraphQL.png) +![GraphQL API client](img/ModusPressGraphQL.png) diff --git a/modus-press/assembly/index.ts b/modus-press/assembly/index.ts index b7d54b7..a0e7065 100644 --- a/modus-press/assembly/index.ts +++ b/modus-press/assembly/index.ts @@ -5,14 +5,14 @@ meta tags optimized for SEO based on the blog post content in the style of the blog post author. */ -import { models, postgresql } from "@hypermode/modus-sdk-as"; +import { models, postgresql } from "@hypermode/modus-sdk-as" import { OpenAIChatModel, ResponseFormat, SystemMessage, UserMessage, -} from "@hypermode/modus-sdk-as/models/openai/chat"; +} from "@hypermode/modus-sdk-as/models/openai/chat" /** * Generate HTML meta description tag content optimized for SEO based on the blog post content @@ -21,21 +21,17 @@ export function genSEO(postContent: string): string { const suggestedTag = generateText( "You are an SEO expert", `Create the HTML meta description tag for a blog post with the following content, only return the meta tag value: ${postContent}`, - ); + ) - return suggestedTag; + return suggestedTag } /** * Generate a suggested blog post title using the blog post content and category, leveraging * the author's biography data retrieved from a Postgres database to match the author's style */ -export function genTitle( - postContent: string, - postCategory: string, - authorName: string, -): string { - const author = getAuthorByName(authorName); +export function genTitle(postContent: string, postCategory: string, authorName: string): string { + const author = getAuthorByName(authorName) const suggestedTitle = generateText( "You are a copyeditor", @@ -48,50 +44,47 @@ export function genTitle( Author biography: ${author.bio} `, - ); + ) - return suggestedTitle; + return suggestedTitle } /** * Use our LLM to generate text based on an instruction and prompt */ function generateText(instruction: string, prompt: string): string { - const model = models.getModel("llama"); + const model = models.getModel("llama") - const input = model.createInput([ - new SystemMessage(instruction), - new UserMessage(prompt), - ]); + const input = model.createInput([new SystemMessage(instruction), new UserMessage(prompt)]) - input.temperature = 0.7; - const output = model.invoke(input); + input.temperature = 0.7 + const output = model.invoke(input) - return output.choices[0].message.content.trim(); + return output.choices[0].message.content.trim() } // The connection host for our Postgres database, as defined in modus.json -const host = "moduspressdb"; +const host = "moduspressdb" /** * The author information */ @json class Author { - id: i32 = 0; - name!: string; - bio!: string; + id: i32 = 0 + name!: string + bio!: string } /** * Query our database to find author information */ function getAuthorByName(name: string): Author { - const query = "select * from authors where name = $1"; + const query = "select * from authors where name = $1" - const params = new postgresql.Params(); - params.push(name); + const params = new postgresql.Params() + params.push(name) - const response = postgresql.query(host, query, params); - return response.rows[0]; + const response = postgresql.query(host, query, params) + return response.rows[0] } diff --git a/modus-press/eslint.config.js b/modus-press/eslint.config.js index 7ad50ae..820e0f6 100644 --- a/modus-press/eslint.config.js +++ b/modus-press/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, aseslint.config, -); +) diff --git a/modus-press/img/ModusPressArch.png b/modus-press/img/ModusPressArch.png index 2d35264..7f00421 100644 Binary files a/modus-press/img/ModusPressArch.png and b/modus-press/img/ModusPressArch.png differ diff --git a/modus-press/img/ModusPressGraphQL.png b/modus-press/img/ModusPressGraphQL.png index 3c3deb9..9d05001 100644 Binary files a/modus-press/img/ModusPressGraphQL.png and b/modus-press/img/ModusPressGraphQL.png differ diff --git a/modus-press/img/ModusPressPostgres.png b/modus-press/img/ModusPressPostgres.png index 54d670d..4426cb0 100644 Binary files a/modus-press/img/ModusPressPostgres.png and b/modus-press/img/ModusPressPostgres.png differ diff --git a/modus101/.gitignore b/modus101/.gitignore deleted file mode 100644 index 95dd6d3..0000000 --- a/modus101/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Ignore macOS system files -.DS_Store - -# Ignore environment variable files -.env -.env.* - -# Ignore build output directories -build/ - -# Ignore Go debuger and generated files -__debug_bin* -*_generated.go -*.generated.go diff --git a/modus101/go.mod b/modus101/go.mod index c7f9f93..8d4c324 100644 --- a/modus101/go.mod +++ b/modus101/go.mod @@ -9,4 +9,4 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect -) +) \ No newline at end of file diff --git a/modus101/go.sum b/modus101/go.sum index c6b310c..67dab01 100644 --- a/modus101/go.sum +++ b/modus101/go.sum @@ -1,7 +1,3 @@ -github.com/hypermodeinc/modus/sdk/go v0.13.1 h1:i12jiBwAAhq/c9HwEvwA0ugXOCvg/wlP9kGtTsg7w9U= -github.com/hypermodeinc/modus/sdk/go v0.13.1/go.mod h1:0TaYvERi5P95n35uLCm7I+Dq7qjnoc95Bu2Yjqr+GIo= -github.com/hypermodeinc/modus/sdk/go v0.14.1 h1:CpjVvftENqIIrBtV1L7dLFY/RXcq5fjkNaZx9QXGmzk= -github.com/hypermodeinc/modus/sdk/go v0.14.1/go.mod h1:VDL2kAYHQtNEr7lxPynbchZ6HizLSSQ9cG4LrDnq3Vg= github.com/hypermodeinc/modus/sdk/go v0.14.3 h1:7lXJvLchg2T0iT3dWwVJlua1uODVoRN5BJIEKzzZ2K8= github.com/hypermodeinc/modus/sdk/go v0.14.3/go.mod h1:VDL2kAYHQtNEr7lxPynbchZ6HizLSSQ9cG4LrDnq3Vg= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= diff --git a/modushack-data-models/.gitignore b/modushack-data-models/.gitignore deleted file mode 100644 index f9d29f7..0000000 --- a/modushack-data-models/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Ignore macOS system files -.DS_Store - -# Ignore environment variable files -.env -.env.* - -# Ignore build output directories -build/ - -# Ignore node_modules folders -node_modules/ - -# Ignore logs generated by as-test -logs/ diff --git a/modushack-data-models/.prettierrc b/modushack-data-models/.prettierrc deleted file mode 100644 index 64cb35c..0000000 --- a/modushack-data-models/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["assemblyscript-prettier"] -} diff --git a/modushack-data-models/README.md b/modushack-data-models/README.md index 100ac1c..8e4b8a6 100644 --- a/modushack-data-models/README.md +++ b/modushack-data-models/README.md @@ -1,6 +1,7 @@ # ModusHack Livestream #2: Data and Models -This example shows how to fetch the most recent articles from the New York Times API and use the Meta LLama LLM model to copywrite additional article titles based on the description. +This example shows how to fetch the most recent articles from the New York Times API and use the +Meta LLama LLM model to copywrite additional article titles based on the description. Watch the recording here: @@ -41,4 +42,5 @@ Then login to Hypermode: hyp login ``` -You'll be prompted to login / create a Hypermode account after which you'll be able to invoke Hypermode hosted models via your Modus apps locally. +You'll be prompted to login / create a Hypermode account after which you'll be able to invoke +Hypermode hosted models via your Modus apps locally. diff --git a/modushack-data-models/assembly/classes.ts b/modushack-data-models/assembly/classes.ts index 7f39870..469630b 100644 --- a/modushack-data-models/assembly/classes.ts +++ b/modushack-data-models/assembly/classes.ts @@ -6,33 +6,33 @@ export class Article { /** * The unique URL of the article */ - url!: string; - adx_keywords!: string; - subsection!: string; - column!: string; - eta_id!: i32; - section!: string; - id!: i32; - asset_id!: i32; - nytdsection!: string; - byline!: string; - type!: string; - title!: string; + url!: string + adx_keywords!: string + subsection!: string + column!: string + eta_id!: i32 + section!: string + id!: i32 + asset_id!: i32 + nytdsection!: string + byline!: string + type!: string + title!: string /** * The alternative article title, generated by our AI copywriter */ - alt_title!: string; + alt_title!: string @alias("abstract") - description!: string; - published_date!: string; - source!: string; - updated!: string; - des_facet!: string[]; - org_facet!: string[]; - per_facet!: string[]; - geo_facet!: string[]; + description!: string + published_date!: string + source!: string + updated!: string + des_facet!: string[] + org_facet!: string[] + per_facet!: string[] + geo_facet!: string[] } /** @@ -40,8 +40,8 @@ export class Article { */ @json export class ArticleResult { - status!: string; - copyright!: string; - num_results!: i32; - results!: Article[]; + status!: string + copyright!: string + num_results!: i32 + results!: Article[] } diff --git a/modushack-data-models/assembly/index.ts b/modushack-data-models/assembly/index.ts index 2f9deed..da669cf 100644 --- a/modushack-data-models/assembly/index.ts +++ b/modushack-data-models/assembly/index.ts @@ -1,44 +1,39 @@ -import { http, models } from "@hypermode/modus-sdk-as"; -import { Article, ArticleResult } from "./classes"; +import { http, models } from "@hypermode/modus-sdk-as" +import { Article, ArticleResult } from "./classes" import { OpenAIChatModel, ResponseFormat, SystemMessage, UserMessage, -} from "@hypermode/modus-sdk-as/models/openai/chat"; +} from "@hypermode/modus-sdk-as/models/openai/chat" /** * Fetch most popular articles and use LLM to copywrite additional title options */ export function fetchNews(): Article { - const response = http.fetch( - "https://api.nytimes.com/svc/mostpopular/v2/emailed/7.json", - ); - const article_result = response.json(); + const response = http.fetch("https://api.nytimes.com/svc/mostpopular/v2/emailed/7.json") + const article_result = response.json() - const article = article_result.results[0]; + const article = article_result.results[0] article.alt_title = generateText( "You are a newspaper editor", `Please copywrite the title of a newspaper article based on this description, only respond with the article title text: ${article.description}`, - ); + ) - return article; + return article } /** * Use our LLM to generate text based on an instruction and prompt */ export function generateText(instruction: string, prompt: string): string { - const model = models.getModel("text-generator"); + const model = models.getModel("text-generator") - const input = model.createInput([ - new SystemMessage(instruction), - new UserMessage(prompt), - ]); + const input = model.createInput([new SystemMessage(instruction), new UserMessage(prompt)]) - input.temperature = 0.7; - const output = model.invoke(input); + input.temperature = 0.7 + const output = model.invoke(input) - return output.choices[0].message.content.trim(); + return output.choices[0].message.content.trim() } diff --git a/modushack-data-models/eslint.config.js b/modushack-data-models/eslint.config.js index 7ad50ae..820e0f6 100644 --- a/modushack-data-models/eslint.config.js +++ b/modushack-data-models/eslint.config.js @@ -1,11 +1,11 @@ // @ts-check -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint"; +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import aseslint from "@hypermode/modus-sdk-as/tools/assemblyscript-eslint" export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, aseslint.config, -); +)