From c9f9889305cddaf26b6463f8f3a80ef3f30766ac Mon Sep 17 00:00:00 2001 From: MaryamZi Date: Mon, 17 Mar 2025 10:34:03 +0100 Subject: [PATCH] Add the blog sample --- examples/blog-service/.devcontainer.json | 8 + examples/blog-service/.gitignore | 11 + examples/blog-service/Ballerina.toml | 8 + examples/blog-service/Dependencies.toml | 386 +++++++++++++++++++++++ examples/blog-service/README.md | 11 + examples/blog-service/main.bal | 78 +++++ examples/blog-service/review_blog.bal | 38 +++ examples/blog-service/types.bal | 37 +++ 8 files changed, 577 insertions(+) create mode 100644 examples/blog-service/.devcontainer.json create mode 100644 examples/blog-service/.gitignore create mode 100644 examples/blog-service/Ballerina.toml create mode 100644 examples/blog-service/Dependencies.toml create mode 100644 examples/blog-service/README.md create mode 100644 examples/blog-service/main.bal create mode 100644 examples/blog-service/review_blog.bal create mode 100644 examples/blog-service/types.bal diff --git a/examples/blog-service/.devcontainer.json b/examples/blog-service/.devcontainer.json new file mode 100644 index 0000000..8acab64 --- /dev/null +++ b/examples/blog-service/.devcontainer.json @@ -0,0 +1,8 @@ +{ + "image": "ballerina/ballerina-devcontainer:2201.12.0-20250310-080700-94702e9e", + "customizations": { + "vscode": { + "extensions": ["WSO2.ballerina"] + } + } +} diff --git a/examples/blog-service/.gitignore b/examples/blog-service/.gitignore new file mode 100644 index 0000000..2d54267 --- /dev/null +++ b/examples/blog-service/.gitignore @@ -0,0 +1,11 @@ +# Ballerina generates this directory during the compilation of a package. +# It contains compiler-generated artifacts and the final executable if this is an application package. +target/ + +# Ballerina maintains the compiler-generated source code here. +# Remove this if you want to commit generated sources. +generated/ + +# Contains configuration values used during development time. +# See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. +Config.toml diff --git a/examples/blog-service/Ballerina.toml b/examples/blog-service/Ballerina.toml new file mode 100644 index 0000000..687f6cf --- /dev/null +++ b/examples/blog-service/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "demo" +name = "blog_service" +version = "0.1.0" +distribution = "2201.12.0" + +[build-options] +observabilityIncluded = true diff --git a/examples/blog-service/Dependencies.toml b/examples/blog-service/Dependencies.toml new file mode 100644 index 0000000..c979a18 --- /dev/null +++ b/examples/blog-service/Dependencies.toml @@ -0,0 +1,386 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.12.0-20250310-080700-94702e9e" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.10.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.9.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "data.jsondata" +version = "1.1.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "data.jsondata"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.15.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.5.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "sql" +version = "1.16.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] +modules = [ + {org = "ballerinai", packageName = "observe", moduleName = "observe"} +] + +[[package]] +org = "ballerinax" +name = "azure.openai.chat" +version = "3.0.1" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] + +[[package]] +org = "ballerinax" +name = "mysql" +version = "1.15.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "sql"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerinax", packageName = "mysql", moduleName = "mysql"} +] + +[[package]] +org = "ballerinax" +name = "mysql.driver" +version = "1.8.0" +modules = [ + {org = "ballerinax", packageName = "mysql.driver", moduleName = "mysql.driver"} +] + +[[package]] +org = "ballerinax" +name = "np" +version = "0.2.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "azure.openai.chat"}, + {org = "ballerinax", name = "openai.chat"} +] +modules = [ + {org = "ballerinax", packageName = "np", moduleName = "np"} +] + +[[package]] +org = "ballerinax" +name = "openai.chat" +version = "4.0.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "url"}, + {org = "ballerinai", name = "observe"} +] + +[[package]] +org = "demo" +name = "blog_service" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "log"}, + {org = "ballerinai", name = "observe"}, + {org = "ballerinax", name = "mysql"}, + {org = "ballerinax", name = "mysql.driver"}, + {org = "ballerinax", name = "np"} +] +modules = [ + {org = "demo", packageName = "blog_service", moduleName = "blog_service"} +] + diff --git a/examples/blog-service/README.md b/examples/blog-service/README.md new file mode 100644 index 0000000..a7ba785 --- /dev/null +++ b/examples/blog-service/README.md @@ -0,0 +1,11 @@ +# Blog service + +A sample blog service supporting the following functionality: + +1. Retrieve posts by category, sorted by a rating identified when the blog post is created - implemented entirely in Ballerina. + +2. Submit a new post + + a. LLM call to identify the most suitable category (from a specified list) and a rating out of 1 - 10 (based on a defined criteria) - [implemented as a natural function](./review_blog.bal) with the requirement specified in natural language + + b. If there is a suitable category and the rating is greater than 3, accept the submission and persist the data. If not, reject the post - implemented entirely in Ballerina. diff --git a/examples/blog-service/main.bal b/examples/blog-service/main.bal new file mode 100644 index 0000000..b24d051 --- /dev/null +++ b/examples/blog-service/main.bal @@ -0,0 +1,78 @@ +// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org). +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/http; +import ballerina/log; +import ballerinax/mysql; +import ballerinax/mysql.driver as _; + +type DbConfig record {| + string host; + string user; + string password; + string database; + int port = 3306; +|}; + +configurable DbConfig dbConfig = ?; +configurable string apiKey = ?; +configurable string serviceUrl = ?; +configurable string deploymentId = ?; +configurable string apiVersion = ?; + +final mysql:Client db = check new (...dbConfig); + +final readonly & string[] categories = [ + "Tech Innovations & Software Development", + "Programming Languages & Frameworks", + "DevOps, Cloud Computing & Automation", + "Career Growth in Tech", + "Open Source & Community-Driven Development" +]; + +service on new http:Listener(8080) { + + resource function post blog(Blog blog) returns http:Created|http:BadRequest|http:InternalServerError { + do { + Blog {title, content} = blog; + Review {suggestedCategory, rating} = check reviewBlog(blog); + + if suggestedCategory is () || rating < 4 { + return { + body: "Blog rejected due to low rating or no matching category"}; + } + + _ = check db->execute(`INSERT INTO Blog (title, content, rating, category) VALUES (${ + title}, ${content}, ${rating}, ${suggestedCategory})`); + return {body: "Blog accepted"}; + } on fail error e { + log:printError("Blog submission failed", e); + return {body: "Blog submission failed"}; + } + } + + resource function get blogs/[string category]() returns Blog[]|http:InternalServerError { + do { + stream result = + db->query( + `SELECT title, content, rating, category FROM Blog WHERE category = ${ + category} ORDER BY rating DESC LIMIT 10`); + return check from BlogRecord blog in result select {title: blog.title, content: blog.content}; + } on fail { + return {body: "Failed to retrieve blogs"}; + } + } +} diff --git a/examples/blog-service/review_blog.bal b/examples/blog-service/review_blog.bal new file mode 100644 index 0000000..6949986 --- /dev/null +++ b/examples/blog-service/review_blog.bal @@ -0,0 +1,38 @@ +// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org). +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerinax/np; + +public isolated function reviewBlog( + Blog blog, + np:Prompt prompt = `You are an expert content reviewer for a blog site that + categorizes posts under the following categories: ${categories} + + Your tasks are: + 1. Suggest a suitable category for the blog from exactly the specified categories. + If there is no match, use null. + + 2. Rate the blog post on a scale of 1 to 10 based on the following criteria: + - **Relevance**: How well the content aligns with the chosen category. + - **Depth**: The level of detail and insight in the content. + - **Clarity**: How easy it is to read and understand. + - **Originality**: Whether the content introduces fresh perspectives or ideas. + - **Language Quality**: Grammar, spelling, and overall writing quality. + + Here is the blog post content: + + Title: ${blog.title} + Content: ${blog.content}`) returns Review|error = @np:NaturalFunction external; diff --git a/examples/blog-service/types.bal b/examples/blog-service/types.bal new file mode 100644 index 0000000..f3e43fb --- /dev/null +++ b/examples/blog-service/types.bal @@ -0,0 +1,37 @@ +// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org). +// +// WSO2 Inc. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +# Represents a blog entry with title and content. +public type Blog record {| + # The title of the blog + string title; + # The content of the blog + string content; +|}; + +# Review of a blog entry. +type Review record {| + # Suggested category + string? suggestedCategory; + # Rating out of 10 + int rating; +|}; + +type BlogRecord record {| + *Blog; + string category; + int rating; +|};