diff --git a/cli/.gitignore b/cli/.gitignore index 5e0fc1d7..4f9e498f 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -1,3 +1,4 @@ hero compile compile_upload +vdo diff --git a/cli/vdo.v b/cli/vdo.v new file mode 100644 index 00000000..a2b5d8d0 --- /dev/null +++ b/cli/vdo.v @@ -0,0 +1,12 @@ +module main + +import freeflowuniverse.herolib.mcp.v_do + +fn main() { + // Create and start the MCP server + mut server := v_do.new_server() + server.start() or { + eprintln('Error starting server: $err') + exit(1) + } +} diff --git a/examples/clients/qdrant_example.vsh b/examples/clients/qdrant_example.vsh new file mode 100644 index 00000000..e7b8fef6 --- /dev/null +++ b/examples/clients/qdrant_example.vsh @@ -0,0 +1,209 @@ +#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.clients.qdrant +import os +import flag + +mut fp := flag.new_flag_parser(os.args) +fp.application('qdrant_example.vsh') +fp.version('v0.1.0') +fp.description('Example script demonstrating Qdrant client usage') +fp.skip_executable() + +help_requested := fp.bool('help', `h`, false, 'Show help message') + +if help_requested { + println(fp.usage()) + exit(0) +} + +additional_args := fp.finalize() or { + eprintln(err) + println(fp.usage()) + exit(1) +} + +// Initialize Qdrant client +mut client := qdrant.get(name: 'default') or { + // If client doesn't exist, create a new one + mut new_client := qdrant.QdrantClient{ + name: 'default' + url: 'http://localhost:6333' + } + qdrant.set(new_client) or { + eprintln('Failed to set Qdrant client: ${err}') + exit(1) + } + new_client +} + +println('Connected to Qdrant at ${client.url}') + +// Check if Qdrant is healthy +is_healthy := client.health_check() or { + eprintln('Failed to check Qdrant health: ${err}') + exit(1) +} + +if !is_healthy { + eprintln('Qdrant is not healthy') + exit(1) +} + +println('Qdrant is healthy') + +// Get service info +service_info := client.get_service_info() or { + eprintln('Failed to get service info: ${err}') + exit(1) +} + +println('Qdrant version: ${service_info.version}') + +// Collection name for our example +collection_name := 'example_collection' + +// Check if collection exists and delete it if it does +collections := client.list_collections() or { + eprintln('Failed to list collections: ${err}') + exit(1) +} + +if collection_name in collections.result { + println('Collection ${collection_name} already exists, deleting it...') + client.delete_collection(collection_name: collection_name) or { + eprintln('Failed to delete collection: ${err}') + exit(1) + } + println('Collection deleted') +} + +// Create a new collection +println('Creating collection ${collection_name}...') +vectors_config := qdrant.VectorsConfig{ + size: 4 // Small size for example purposes + distance: .cosine +} + +client.create_collection( + collection_name: collection_name + vectors: vectors_config +) or { + eprintln('Failed to create collection: ${err}') + exit(1) +} + +println('Collection created') + +// Upsert some points +println('Upserting points...') +points := [ + qdrant.PointStruct{ + id: '1' + vector: [f32(0.1), 0.2, 0.3, 0.4] + payload: { + 'color': 'red' + 'category': 'furniture' + 'name': 'chair' + } + }, + qdrant.PointStruct{ + id: '2' + vector: [f32(0.2), 0.3, 0.4, 0.5] + payload: { + 'color': 'blue' + 'category': 'electronics' + 'name': 'laptop' + } + }, + qdrant.PointStruct{ + id: '3' + vector: [f32(0.3), 0.4, 0.5, 0.6] + payload: { + 'color': 'green' + 'category': 'food' + 'name': 'apple' + } + } +] + +client.upsert_points( + collection_name: collection_name + points: points + wait: true +) or { + eprintln('Failed to upsert points: ${err}') + exit(1) +} + +println('Points upserted') + +// Get collection info to verify points were added +collection_info := client.get_collection(collection_name: collection_name) or { + eprintln('Failed to get collection info: ${err}') + exit(1) +} + +println('Collection has ${collection_info.vectors_count} points') + +// Search for points +println('Searching for points similar to [0.1, 0.2, 0.3, 0.4]...') +search_result := client.search( + collection_name: collection_name + vector: [f32(0.1), 0.2, 0.3, 0.4] + limit: 3 +) or { + eprintln('Failed to search points: ${err}') + exit(1) +} + +println('Search results:') +for i, point in search_result.result { + println(' ${i+1}. ID: ${point.id}, Score: ${point.score}') + if payload := point.payload { + println(' Name: ${payload['name']}') + println(' Category: ${payload['category']}') + println(' Color: ${payload['color']}') + } +} + +// Search with filter +println('\nSearching for points with category "electronics"...') +filter := qdrant.Filter{ + must: [ + qdrant.FieldCondition{ + key: 'category' + match: 'electronics' + } + ] +} + +filtered_search := client.search( + collection_name: collection_name + vector: [f32(0.1), 0.2, 0.3, 0.4] + filter: filter + limit: 3 +) or { + eprintln('Failed to search with filter: ${err}') + exit(1) +} + +println('Filtered search results:') +for i, point in filtered_search.result { + println(' ${i+1}. ID: ${point.id}, Score: ${point.score}') + if payload := point.payload { + println(' Name: ${payload['name']}') + println(' Category: ${payload['category']}') + println(' Color: ${payload['color']}') + } +} + +// Clean up - delete the collection +println('\nCleaning up - deleting collection...') +client.delete_collection(collection_name: collection_name) or { + eprintln('Failed to delete collection: ${err}') + exit(1) +} + +println('Collection deleted') +println('Example completed successfully') diff --git a/examples/installers/lang/python.vsh b/examples/installers/lang/python.vsh index 42bb041a..75acca39 100755 --- a/examples/installers/lang/python.vsh +++ b/examples/installers/lang/python.vsh @@ -3,5 +3,8 @@ import freeflowuniverse.herolib.installers.lang.python as python_module mut python_installer := python_module.get()! -// python_installer.install()! -python_installer.destroy()! +python_installer.install()! + + + +// python_installer.destroy()! diff --git a/lib/clients/jina/jina_client.v b/lib/clients/jina/jina_client.v index 5ef4c919..e7dacb5e 100644 --- a/lib/clients/jina/jina_client.v +++ b/lib/clients/jina/jina_client.v @@ -1,7 +1,7 @@ module jina import freeflowuniverse.herolib.core.httpconnection -import os +// import os import json @[params] @@ -242,4 +242,4 @@ pub fn (mut j Jina) rerank(params RerankParams) !RankingOutput { // // If we get a response, the API key is valid // return true -// } +// } \ No newline at end of file diff --git a/lib/clients/jina/jina_factory_.v b/lib/clients/jina/jina_factory_.v index 6a602a57..f3910483 100644 --- a/lib/clients/jina/jina_factory_.v +++ b/lib/clients/jina/jina_factory_.v @@ -2,7 +2,7 @@ module jina import freeflowuniverse.herolib.core.base import freeflowuniverse.herolib.core.playbook -import freeflowuniverse.herolib.ui.console +// import freeflowuniverse.herolib.ui.console __global ( jina_global map[string]&Jina diff --git a/lib/clients/jina/jina_model.v b/lib/clients/jina/jina_model.v index 8057e768..cbcd1861 100644 --- a/lib/clients/jina/jina_model.v +++ b/lib/clients/jina/jina_model.v @@ -2,7 +2,7 @@ module jina import freeflowuniverse.herolib.data.encoderhero import freeflowuniverse.herolib.core.httpconnection -import net.http +// import net.http import os pub const version = '0.0.0' diff --git a/lib/clients/jina/rank_api.v b/lib/clients/jina/rank_api.v index e17efd50..4112006b 100644 --- a/lib/clients/jina/rank_api.v +++ b/lib/clients/jina/rank_api.v @@ -1,6 +1,6 @@ module jina -import json +// import json pub enum JinaRerankModel { reranker_v2_base_multilingual // 278M diff --git a/lib/clients/qdrant/qdrant_client.v b/lib/clients/qdrant/qdrant_client.v new file mode 100644 index 00000000..b2047aca --- /dev/null +++ b/lib/clients/qdrant/qdrant_client.v @@ -0,0 +1,394 @@ +module qdrant + +import freeflowuniverse.herolib.core.httpconnection +import json +// import os + +// QdrantClient is the main client for interacting with the Qdrant API +pub struct QdrantClient { +pub mut: + name string = 'default' + secret string + url string = 'http://localhost:6333' +} + +// httpclient creates a new HTTP connection to the Qdrant API +fn (mut self QdrantClient) httpclient() !&httpconnection.HTTPConnection { + mut http_conn := httpconnection.new( + name: 'Qdrant_vclient' + url: self.url + )! + + // Add authentication header if API key is provided + if self.secret.len > 0 { + http_conn.default_header.add(.api_key, self.secret) + } + return http_conn +} + +// Collections API + +@[params] +pub struct CreateCollectionParams { +pub mut: + collection_name string @[required] + vectors VectorsConfig @[required] + shard_number ?int + replication_factor ?int + write_consistency_factor ?int + on_disk_payload ?bool + hnsw_config ?HnswConfig + optimizers_config ?OptimizersConfig + wal_config ?WalConfig + quantization_config ?QuantizationConfig + init_from ?InitFrom + timeout ?int +} + +// Create a new collection +pub fn (mut q QdrantClient) create_collection(params CreateCollectionParams) !bool { + mut collection_params := CollectionParams{ + vectors: params.vectors + } + + if v := params.shard_number { + collection_params.shard_number = v + } + + if v := params.replication_factor { + collection_params.replication_factor = v + } + + if v := params.write_consistency_factor { + collection_params.write_consistency_factor = v + } + + if v := params.on_disk_payload { + collection_params.on_disk_payload = v + } + + if v := params.hnsw_config { + collection_params.hnsw_config = v + } + + if v := params.optimizers_config { + collection_params.optimizers_config = v + } + + if v := params.wal_config { + collection_params.wal_config = v + } + + if v := params.quantization_config { + collection_params.quantization_config = v + } + + if v := params.init_from { + collection_params.init_from = v + } + + mut query_params := map[string]string{} + if v := params.timeout { + query_params['timeout'] = v.str() + } + + req := httpconnection.Request{ + method: .put + prefix: 'collections/${params.collection_name}' + dataformat: .json + data: json.encode(collection_params) + params: query_params + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + result := json.decode(OperationResponse, response.body)! + return result.result +} + +@[params] +pub struct ListCollectionsParams { +pub mut: + timeout ?int +} + +// List all collections +pub fn (mut q QdrantClient) list_collections(params ListCollectionsParams) !CollectionsResponse { + mut query_params := map[string]string{} + if v := params.timeout { + query_params['timeout'] = v.str() + } + + req := httpconnection.Request{ + method: .get + prefix: 'collections' + params: query_params + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + return json.decode(CollectionsResponse, response.body)! +} + +@[params] +pub struct DeleteCollectionParams { +pub mut: + collection_name string @[required] + timeout ?int +} + +// Delete a collection +pub fn (mut q QdrantClient) delete_collection(params DeleteCollectionParams) !bool { + mut query_params := map[string]string{} + if v := params.timeout { + query_params['timeout'] = v.str() + } + + req := httpconnection.Request{ + method: .delete + prefix: 'collections/${params.collection_name}' + params: query_params + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + result := json.decode(OperationResponse, response.body)! + return result.result +} + +@[params] +pub struct GetCollectionParams { +pub mut: + collection_name string @[required] + timeout ?int +} + +// Get collection info +pub fn (mut q QdrantClient) get_collection(params GetCollectionParams) !CollectionInfo { + mut query_params := map[string]string{} + if v := params.timeout { + query_params['timeout'] = v.str() + } + + req := httpconnection.Request{ + method: .get + prefix: 'collections/${params.collection_name}' + params: query_params + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + result := json.decode(CollectionInfoResponse, response.body)! + return result.result +} + +// Points API + +@[params] +pub struct UpsertPointsParams { +pub mut: + collection_name string @[required] + points []PointStruct @[required] + wait ?bool + ordering ?WriteOrdering +} + +// Upsert points +pub fn (mut q QdrantClient) upsert_points(params UpsertPointsParams) !PointsOperationResponse { + mut query_params := map[string]string{} + if v := params.wait { + query_params['wait'] = v.str() + } + + mut request_body := map[string]json.Any{} + request_body['points'] = params.points + + if v := params.ordering { + request_body['ordering'] = v + } + + req := httpconnection.Request{ + method: .put + prefix: 'collections/${params.collection_name}/points' + dataformat: .json + data: json.encode(request_body) + params: query_params + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + return json.decode(PointsOperationResponse, response.body)! +} + +@[params] +pub struct DeletePointsParams { +pub mut: + collection_name string @[required] + points_selector PointsSelector @[required] + wait ?bool + ordering ?WriteOrdering +} + +// Delete points +pub fn (mut q QdrantClient) delete_points(params DeletePointsParams) !PointsOperationResponse { + mut query_params := map[string]string{} + if v := params.wait { + query_params['wait'] = v.str() + } + + mut request_body := map[string]json.Any{} + + if params.points_selector.points != none { + request_body['points'] = params.points_selector.points + } else if params.points_selector.filter != none { + request_body['filter'] = params.points_selector.filter + } + + if v := params.ordering { + request_body['ordering'] = v + } + + req := httpconnection.Request{ + method: .post + prefix: 'collections/${params.collection_name}/points/delete' + dataformat: .json + data: json.encode(request_body) + params: query_params + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + return json.decode(PointsOperationResponse, response.body)! +} + +@[params] +pub struct GetPointParams { +pub mut: + collection_name string @[required] + id string @[required] + with_payload ?WithPayloadSelector + with_vector ?WithVector +} + +// Get a point by ID +pub fn (mut q QdrantClient) get_point(params GetPointParams) !GetPointResponse { + mut query_params := map[string]string{} + + if v := params.with_payload { + query_params['with_payload'] = json.encode(v) + } + + if v := params.with_vector { + query_params['with_vector'] = json.encode(v) + } + + req := httpconnection.Request{ + method: .get + prefix: 'collections/${params.collection_name}/points/${params.id}' + params: query_params + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + return json.decode(GetPointResponse, response.body)! +} + +@[params] +pub struct SearchParams { +pub mut: + collection_name string @[required] + vector []f32 @[required] + limit int = 10 + filter ?Filter + params ?SearchParamsConfig + with_payload ?WithPayloadSelector + with_vector ?WithVector + score_threshold ?f32 +} + +// Search for points +pub fn (mut q QdrantClient) search(params SearchParams) !SearchResponse { + // Create a struct to serialize to JSON + struct SearchRequest { + pub mut: + vector []f32 + limit int + filter ?Filter + params ?SearchParamsConfig + with_payload ?WithPayloadSelector + with_vector ?WithVector + score_threshold ?f32 + } + + mut request := SearchRequest{ + vector: params.vector + limit: params.limit + } + + if v := params.filter { + request.filter = v + } + + if v := params.params { + request.params = v + } + + if v := params.with_payload { + request.with_payload = v + } + + if v := params.with_vector { + request.with_vector = v + } + + if v := params.score_threshold { + request.score_threshold = v + } + + req := httpconnection.Request{ + method: .post + prefix: 'collections/${params.collection_name}/points/search' + dataformat: .json + data: json.encode(request) + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + return json.decode(SearchResponse, response.data)! +} + +// Service API + +// Get Qdrant service info +pub fn (mut q QdrantClient) get_service_info() !ServiceInfoResponse { + req := httpconnection.Request{ + method: .get + prefix: '' + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + return json.decode(ServiceInfoResponse, response.data)! +} + +// Check Qdrant health +pub fn (mut q QdrantClient) health_check() !bool { + req := httpconnection.Request{ + method: .get + prefix: 'healthz' + } + + mut httpclient := q.httpclient()! + response := httpclient.send(req)! + + return response.code == 200 +} diff --git a/lib/clients/qdrant/qdrant_client_test.v b/lib/clients/qdrant/qdrant_client_test.v new file mode 100644 index 00000000..b605b4a1 --- /dev/null +++ b/lib/clients/qdrant/qdrant_client_test.v @@ -0,0 +1,117 @@ +module qdrant + +fn test_qdrant_client() { + mut client := QDrantClient{ + name: 'test_client' + url: 'http://localhost:6333' + } + + // Test creating a collection + vectors_config := VectorsConfig{ + size: 128 + distance: .cosine + } + + // Create collection + create_result := client.create_collection( + collection_name: 'test_collection' + vectors: vectors_config + ) or { + assert false, 'Failed to create collection: ${err}' + return + } + assert create_result == true + + // List collections + collections := client.list_collections() or { + assert false, 'Failed to list collections: ${err}' + return + } + assert 'test_collection' in collections.result + + // Get collection info + collection_info := client.get_collection( + collection_name: 'test_collection' + ) or { + assert false, 'Failed to get collection info: ${err}' + return + } + assert collection_info.vectors_count == 0 + + // Upsert points + points := [ + PointStruct{ + id: '1' + vector: [f32(0.1), 0.2, 0.3, 0.4] + payload: { + 'color': 'red' + 'category': 'furniture' + } + }, + PointStruct{ + id: '2' + vector: [f32(0.2), 0.3, 0.4, 0.5] + payload: { + 'color': 'blue' + 'category': 'electronics' + } + } + ] + + upsert_result := client.upsert_points( + collection_name: 'test_collection' + points: points + wait: true + ) or { + assert false, 'Failed to upsert points: ${err}' + return + } + assert upsert_result.status == 'ok' + + // Search for points + search_result := client.search( + collection_name: 'test_collection' + vector: [f32(0.1), 0.2, 0.3, 0.4] + limit: 1 + ) or { + assert false, 'Failed to search points: ${err}' + return + } + assert search_result.result.len > 0 + + // Get a point + point := client.get_point( + collection_name: 'test_collection' + id: '1' + ) or { + assert false, 'Failed to get point: ${err}' + return + } + if result := point.result { + assert result.id == '1' + } else { + assert false, 'Point not found' + } + + // Delete a point + delete_result := client.delete_points( + collection_name: 'test_collection' + points_selector: PointsSelector{ + points: ['1'] + } + wait: true + ) or { + assert false, 'Failed to delete point: ${err}' + return + } + assert delete_result.status == 'ok' + + // Delete collection + delete_collection_result := client.delete_collection( + collection_name: 'test_collection' + ) or { + assert false, 'Failed to delete collection: ${err}' + return + } + assert delete_collection_result == true +} diff --git a/lib/clients/qdrant/qdrant_factory_.v b/lib/clients/qdrant/qdrant_factory_.v index 7bfb8ca8..b76fbc8d 100644 --- a/lib/clients/qdrant/qdrant_factory_.v +++ b/lib/clients/qdrant/qdrant_factory_.v @@ -2,7 +2,7 @@ module qdrant import freeflowuniverse.herolib.core.base import freeflowuniverse.herolib.core.playbook -import freeflowuniverse.herolib.ui.console +// import freeflowuniverse.herolib.ui.console __global ( qdrant_global map[string]&QDrantClient diff --git a/lib/clients/qdrant/qdrant_model.v b/lib/clients/qdrant/qdrant_model.v index df18c0ee..9edab578 100644 --- a/lib/clients/qdrant/qdrant_model.v +++ b/lib/clients/qdrant/qdrant_model.v @@ -1,8 +1,9 @@ module qdrant -import freeflowuniverse.herolib.data.paramsparser +// import freeflowuniverse.herolib.data.paramsparser import freeflowuniverse.herolib.data.encoderhero -import os +// import json +// import os pub const version = '0.0.0' const singleton = false @@ -13,9 +14,9 @@ const default = true @[heap] pub struct QDrantClient { pub mut: - name string = 'default' - secret string - url string = "http://localhost:6333/" + name string = 'default' + secret string + url string = 'http://localhost:6333/' } // your checking & initialization code if needed @@ -34,3 +35,363 @@ pub fn heroscript_loads(heroscript string) !QDrantClient { mut obj := encoderhero.decode[QDrantClient](heroscript)! return obj } + +// Base response structure +pub struct BaseResponse { +pub mut: + time f32 + status string +} + +// Operation response +pub struct OperationResponse { +pub mut: + time f32 + status string + result bool +} + +// Collections response +pub struct CollectionsResponse { +pub mut: + time f32 + status string + result []string +} + +// Collection info response +pub struct CollectionInfoResponse { +pub mut: + time f32 + status string + result CollectionInfo +} + +// Collection info +pub struct CollectionInfo { +pub mut: + status string + optimizer_status OptimizersStatus + vectors_count u64 + indexed_vectors_count ?u64 + points_count u64 + segments_count u64 + config CollectionConfig + payload_schema map[string]PayloadIndexInfo +} + +// Optimizers status +pub struct OptimizersStatus { +pub mut: + status string +} + +// Collection config +pub struct CollectionConfig { +pub mut: + params CollectionParams + hnsw_config ?HnswConfig + optimizer_config ?OptimizersConfig + wal_config ?WalConfig + quantization_config ?QuantizationConfig +} + +// Collection params +pub struct CollectionParams { +pub mut: + vectors VectorsConfig + shard_number ?int + replication_factor ?int + write_consistency_factor ?int + on_disk_payload ?bool + hnsw_config ?HnswConfig + optimizers_config ?OptimizersConfig + wal_config ?WalConfig + quantization_config ?QuantizationConfig + init_from ?InitFrom +} + +// Vectors config +pub struct VectorsConfig { +pub mut: + size int + distance Distance + hnsw_config ?HnswConfig + quantization_config ?QuantizationConfig + on_disk ?bool +} + +// Distance type +pub enum Distance { + cosine + euclid + dot +} + +// Convert Distance enum to string +pub fn (d Distance) str() string { + return match d { + .cosine { 'cosine' } + .euclid { 'euclid' } + .dot { 'dot' } + } +} + +// HNSW config +pub struct HnswConfig { +pub mut: + m int + ef_construct int + full_scan_threshold ?int + max_indexing_threads ?int + on_disk ?bool + payload_m ?int +} + +// Optimizers config +pub struct OptimizersConfig { +pub mut: + deleted_threshold f32 + vacuum_min_vector_number int + default_segment_number int + max_segment_size ?int + memmap_threshold ?int + indexing_threshold ?int + flush_interval_sec ?int + max_optimization_threads ?int +} + +// WAL config +pub struct WalConfig { +pub mut: + wal_capacity_mb ?int + wal_segments_ahead ?int +} + +// Quantization config +pub struct QuantizationConfig { +pub mut: + scalar ?ScalarQuantization + product ?ProductQuantization + binary ?BinaryQuantization +} + +// Scalar quantization +pub struct ScalarQuantization { +pub mut: + type_ string + quantile ?f32 + always_ram ?bool +} + +// Product quantization +pub struct ProductQuantization { +pub mut: + compression string + always_ram ?bool +} + +// Binary quantization +pub struct BinaryQuantization { +pub mut: + binary bool + always_ram ?bool +} + +// Init from +pub struct InitFrom { +pub mut: + collection string + shard ?int +} + +// Payload index info +pub struct PayloadIndexInfo { +pub mut: + data_type string + params ?map[string]string + points int +} + +// Points operation response +pub struct PointsOperationResponse { +pub mut: + time f32 + status string + result OperationInfo +} + +// Operation info +pub struct OperationInfo { +pub mut: + operation_id int + status string +} + +// Point struct +pub struct PointStruct { +pub mut: + id string + vector []f32 + payload ?map[string]string +} + +// Points selector +pub struct PointsSelector { +pub mut: + points ?[]string + filter ?Filter +} + +// Filter +pub struct Filter { +pub mut: + must ?[]Condition + must_not ?[]Condition + should ?[]Condition +} + +// Filter is serialized directly to JSON + +// Condition interface +pub interface Condition {} + +// Field condition +pub struct FieldCondition { +pub mut: + key string + match ?string @[json: match] + match_integer ?int @[json: match] + match_float ?f32 @[json: match] + match_bool ?bool @[json: match] + range ?Range + geo_bounding_box ?GeoBoundingBox + geo_radius ?GeoRadius + values_count ?ValuesCount +} + +// FieldCondition is serialized directly to JSON + +// Range +pub struct Range { +pub mut: + lt ?f32 + gt ?f32 + gte ?f32 + lte ?f32 +} + +// Range is serialized directly to JSON + +// GeoBoundingBox +pub struct GeoBoundingBox { +pub mut: + top_left GeoPoint + bottom_right GeoPoint +} + +// GeoBoundingBox is serialized directly to JSON + +// GeoPoint +pub struct GeoPoint { +pub mut: + lon f32 + lat f32 +} + +// GeoPoint is serialized directly to JSON + +// GeoRadius +pub struct GeoRadius { +pub mut: + center GeoPoint + radius f32 +} + +// GeoRadius is serialized directly to JSON + +// ValuesCount +pub struct ValuesCount { +pub mut: + lt ?int + gt ?int + gte ?int + lte ?int +} + +// ValuesCount is serialized directly to JSON + +// WithPayloadSelector +pub struct WithPayloadSelector { +pub mut: + include ?[]string + exclude ?[]string +} + +// WithPayloadSelector is serialized directly to JSON + +// WithVector +pub struct WithVector { +pub mut: + include ?[]string +} + +// WithVector is serialized directly to JSON + +// Get point response +pub struct GetPointResponse { +pub mut: + time f32 + status string + result ?PointStruct +} + +// Search params configuration +pub struct SearchParamsConfig { +pub mut: + hnsw_ef ?int + exact ?bool +} + +// SearchParamsConfig is serialized directly to JSON + +// Search response +pub struct SearchResponse { +pub mut: + time f32 + status string + result []ScoredPoint +} + +// Scored point +pub struct ScoredPoint { +pub mut: + id string + version int + score f32 + payload ?map[string]string + vector ?[]f32 +} + +// Write ordering +pub struct WriteOrdering { +pub mut: + type_ string +} + +// WriteOrdering is serialized directly to JSON + +// Service info response +pub struct ServiceInfoResponse { +pub mut: + time f32 + status string + result ServiceInfo +} + +// Service info +pub struct ServiceInfo { +pub mut: + version string + commit ?string +} diff --git a/lib/clients/qdrant/readme.md b/lib/clients/qdrant/readme.md index 50c4ab8d..05c9ce52 100644 --- a/lib/clients/qdrant/readme.md +++ b/lib/clients/qdrant/readme.md @@ -1,30 +1,169 @@ -# qdrant +# Qdrant Client for HeroLib +This is a V client for [Qdrant](https://qdrant.tech/), a high-performance vector database and similarity search engine. +## Features -To get started +- Collection management (create, list, delete, get info) +- Points management (upsert, delete, search, get) +- Service information and health checks +- Support for filters, payload management, and vector operations -```vlang - - -import freeflowuniverse.herolib.clients. qdrant - -mut client:= qdrant.get()! - -client... +## Usage +### Initialize Client +```v +// Create a new Qdrant client +import freeflowuniverse.herolib.clients.qdrant +mut client := qdrant.get()! +// Or create with custom configuration +mut custom_client := qdrant.QDrantClient{ + name: 'custom', + url: 'http://localhost:6333', + secret: 'your_api_key' // Optional +} +qdrant.set(custom_client)! ``` -## example heroscript +### Collection Management + +```v +// Create a collection +vectors_config := qdrant.VectorsConfig{ + size: 128, + distance: .cosine +} + +client.create_collection( + collection_name: 'my_collection', + vectors: vectors_config +)! + +// List all collections +collections := client.list_collections()! + +// Get collection info +collection_info := client.get_collection( + collection_name: 'my_collection' +)! + +// Delete a collection +client.delete_collection( + collection_name: 'my_collection' +)! +``` + +### Points Management + +```v +// Upsert points +points := [ + qdrant.PointStruct{ + id: '1', + vector: [f32(0.1), 0.2, 0.3, 0.4], + payload: { + 'color': 'red', + 'category': 'furniture' + } + }, + qdrant.PointStruct{ + id: '2', + vector: [f32(0.2), 0.3, 0.4, 0.5], + payload: { + 'color': 'blue', + 'category': 'electronics' + } + } +] + +client.upsert_points( + collection_name: 'my_collection', + points: points, + wait: true +)! + +// Search for points +search_result := client.search( + collection_name: 'my_collection', + vector: [f32(0.1), 0.2, 0.3, 0.4], + limit: 10 +)! + +// Get a point by ID +point := client.get_point( + collection_name: 'my_collection', + id: '1' +)! + +// Delete points +client.delete_points( + collection_name: 'my_collection', + points_selector: qdrant.PointsSelector{ + points: ['1', '2'] + }, + wait: true +)! +``` + +### Service Information + +```v +// Get service info +service_info := client.get_service_info()! + +// Check health +is_healthy := client.health_check()! +``` + +## Advanced Usage + +### Filtering + +```v +// Create a filter +filter := qdrant.Filter{ + must: [ + qdrant.FieldCondition{ + key: 'color', + match: 'red' + }, + qdrant.FieldCondition{ + key: 'price', + range: qdrant.Range{ + gte: 10.0, + lt: 100.0 + } + } + ] +} + +// Search with filter +search_result := client.search( + collection_name: 'my_collection', + vector: [f32(0.1), 0.2, 0.3, 0.4], + filter: filter, + limit: 10 +)! +``` + +## Example HeroScript ```hero !!qdrant.configure - secret: '...' - host: 'localhost' - port: 8888 + name: 'default' + secret: 'your_api_key' + url: 'http://localhost:6333' ``` +## Installation +Qdrant server can be installed using the provided installer script: + +```bash +~/code/github/freeflowuniverse/herolib/examples/installers/db/qdrant.vsh +``` + +This will install and start a Qdrant server locally. diff --git a/lib/installers/infra/coredns/readme.md b/lib/installers/infra/coredns/readme.md index 2cee25f9..15fa18a8 100644 --- a/lib/installers/infra/coredns/readme.md +++ b/lib/installers/infra/coredns/readme.md @@ -7,7 +7,7 @@ To get started ```vlang -import freeflowuniverse.herolib.lib.installers.infra.coredns as coredns_installer +import freeflowuniverse.herolib.installers.infra.coredns as coredns_installer heroscript:=" !!coredns.configure name:'test' diff --git a/lib/installers/lang/python/python_model.v b/lib/installers/lang/python/python_model.v index 3d4ec4d9..28f701b8 100644 --- a/lib/installers/lang/python/python_model.v +++ b/lib/installers/lang/python/python_model.v @@ -1,8 +1,6 @@ module python -import freeflowuniverse.herolib.data.paramsparser import freeflowuniverse.herolib.data.encoderhero -import os pub const version = '3.12.0' const singleton = true diff --git a/lib/mcp/aiprompt.md b/lib/mcp/aiprompt.md new file mode 100644 index 00000000..4f6802cb --- /dev/null +++ b/lib/mcp/aiprompt.md @@ -0,0 +1,44 @@ + +make an mcp server in @lib/mcp/v_do + +use the Standard Input/Output (stdio) transport as described in +https://modelcontextprotocol.io/docs/concepts/transports + +The tool has following methods + +## test +- args: $fullpath +- cmd: 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}' + +if the file is a dir then find the .v files (non recursive) and do it for each opf those + +collect the output and return + +## run +- args: $fullpath +- cmd: 'v -gc none -stats -enable-globals -n -w -cg -g -cc tcc run ${fullpath}' + +if the file is a dir then find the .v files (non recursive) and do it for each opf those + +collect the output and return + + +## compile +- args: $fullpath +- cmd: 'cd /tmp && v -gc none -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath}' + +if the file is a dir then find the .v files (non recursive) and do it for each opf those + +collect the output and return + + +## vet +- args: $fullpath +- cmd: 'v vet -v -w ${fullpath}' + +if the file is a dir then find the .v files (non recursive) and do it for each opf those + +collect the output and return + + + diff --git a/lib/mcp/v_do/README.md b/lib/mcp/v_do/README.md new file mode 100644 index 00000000..9ed623bd --- /dev/null +++ b/lib/mcp/v_do/README.md @@ -0,0 +1,92 @@ +# V-Do MCP Server + +An implementation of the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for V language operations. This server uses the Standard Input/Output (stdio) transport as described in the [MCP documentation](https://modelcontextprotocol.io/docs/concepts/transports). + +## Features + +The server supports the following operations: + +1. **test** - Run V tests on a file or directory +2. **run** - Execute V code from a file or directory +3. **compile** - Compile V code from a file or directory +4. **vet** - Run V vet on a file or directory + +## Usage + +### Building the Server + +```bash +v -gc none -stats -enable-globals -n -w -cg -g -cc tcc /Users/despiegk/code/github/freeflowuniverse/herolib/lib/mcp/v_do +``` + +### Using the Server + +The server communicates using the MCP protocol over stdio. To send a request, use the following format: + +``` +Content-Length: + +{"jsonrpc":"2.0","id":"","method":"","params":{"fullpath":""}} +``` + +Where: +- `` is the length of the JSON message in bytes +- `` is a unique identifier for the request +- `` is one of: `test`, `run`, `compile`, or `vet` +- `` is the absolute path to the V file or directory to process + +### Example + +Request: +``` +Content-Length: 85 + +{"jsonrpc":"2.0","id":"1","method":"test","params":{"fullpath":"/path/to/file.v"}} +``` + +Response: +``` +Content-Length: 245 + +{"jsonrpc":"2.0","id":"1","result":{"output":"Command: v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test /path/to/file.v\nExit code: 0\nOutput:\nAll tests passed!"}} +``` + +## Methods + +### test + +Runs V tests on the specified file or directory. + +Command used: +``` +v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath} +``` + +If a directory is specified, it will run tests on all `.v` files in the directory (non-recursive). + +### run + +Executes the specified V file or all V files in a directory. + +Command used: +``` +v -gc none -stats -enable-globals -n -w -cg -g -cc tcc run ${fullpath} +``` + +### compile + +Compiles the specified V file or all V files in a directory. + +Command used: +``` +cd /tmp && v -gc none -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc ${fullpath} +``` + +### vet + +Runs V vet on the specified file or directory. + +Command used: +``` +v vet -v -w ${fullpath} +``` diff --git a/lib/mcp/v_do/handlers/vcompile.v b/lib/mcp/v_do/handlers/vcompile.v new file mode 100644 index 00000000..dd91c85b --- /dev/null +++ b/lib/mcp/v_do/handlers/vcompile.v @@ -0,0 +1,4 @@ +module handlers + +import os +import freeflowuniverse.herolib.mcp.v_do.logger diff --git a/lib/mcp/v_do/handlers/vlist.v b/lib/mcp/v_do/handlers/vlist.v new file mode 100644 index 00000000..f3e3ab71 --- /dev/null +++ b/lib/mcp/v_do/handlers/vlist.v @@ -0,0 +1,21 @@ +module handlers + +import os +import freeflowuniverse.herolib.mcp.v_do.logger + +// list_v_files returns all .v files in a directory (non-recursive), excluding generated files ending with _.v +fn list_v_files(dir string) ![]string { + files := os.ls(dir) or { + return error('Error listing directory: $err') + } + + mut v_files := []string{} + for file in files { + if file.ends_with('.v') && !file.ends_with('_.v') { + filepath := os.join_path(dir, file) + v_files << filepath + } + } + + return v_files +} diff --git a/lib/mcp/v_do/handlers/vrun.v b/lib/mcp/v_do/handlers/vrun.v new file mode 100644 index 00000000..dd91c85b --- /dev/null +++ b/lib/mcp/v_do/handlers/vrun.v @@ -0,0 +1,4 @@ +module handlers + +import os +import freeflowuniverse.herolib.mcp.v_do.logger diff --git a/lib/mcp/v_do/handlers/vtest.v b/lib/mcp/v_do/handlers/vtest.v new file mode 100644 index 00000000..b10ad59a --- /dev/null +++ b/lib/mcp/v_do/handlers/vtest.v @@ -0,0 +1,31 @@ +module handlers + +import os +import freeflowuniverse.herolib.mcp.v_do.logger + +// test runs v test on the specified file or directory +pub fn vtest(fullpath string) !string { + logger.info('test $fullpath') + if !os.exists(fullpath) { + return error('File or directory does not exist: $fullpath') + } + if os.is_dir(fullpath) { + mut results:="" + for item in list_v_files(fullpath)!{ + results += vtest(item)! + results += '\n-----------------------\n' + } + return results + }else{ + cmd := 'v -gc none -stats -enable-globals -show-c-output -keepc -n -w -cg -o /tmp/tester.c -g -cc tcc test ${fullpath}' + logger.debug('Executing command: $cmd') + result := os.execute(cmd) + if result.exit_code != 0 { + return error('Test failed for $fullpath with exit code ${result.exit_code}\n${result.output}') + } else { + logger.info('Test completed for $fullpath') + } + return 'Command: $cmd\nExit code: ${result.exit_code}\nOutput:\n${result.output}' + } + +} diff --git a/lib/mcp/v_do/handlers/vvet.v b/lib/mcp/v_do/handlers/vvet.v new file mode 100644 index 00000000..a50afb75 --- /dev/null +++ b/lib/mcp/v_do/handlers/vvet.v @@ -0,0 +1,42 @@ +module handlers + +import os +import freeflowuniverse.herolib.mcp.v_do.logger + +// vvet runs v vet on the specified file or directory +pub fn vvet(fullpath string) !string { + logger.info('vet $fullpath') + if !os.exists(fullpath) { + return error('File or directory does not exist: $fullpath') + } + + if os.is_dir(fullpath) { + mut results := "" + files := list_v_files(fullpath) or { + return error('Error listing V files: $err') + } + for file in files { + results += vet_file(file) or { + logger.error('Failed to vet $file: $err') + return error('Failed to vet $file: $err') + } + results += '\n-----------------------\n' + } + return results + } else { + return vet_file(fullpath) + } +} + +// vet_file runs v vet on a single file +fn vet_file(file string) !string { + cmd := 'v vet -v -w ${file}' + logger.debug('Executing command: $cmd') + result := os.execute(cmd) + if result.exit_code != 0 { + return error('Vet failed for $file with exit code ${result.exit_code}\n${result.output}') + } else { + logger.info('Vet completed for $file') + } + return 'Command: $cmd\nExit code: ${result.exit_code}\nOutput:\n${result.output}' +} diff --git a/lib/mcp/v_do/logger/logger.v b/lib/mcp/v_do/logger/logger.v new file mode 100644 index 00000000..ab2a69d1 --- /dev/null +++ b/lib/mcp/v_do/logger/logger.v @@ -0,0 +1,50 @@ +module logger + +import os + +// LogLevel defines the severity of log messages +pub enum LogLevel { + debug + info + warn + error + fatal +} + +// log outputs a message to stderr with the specified log level +pub fn log(level LogLevel, message string) { + level_str := match level { + .debug { 'DEBUG' } + .info { 'INFO ' } + .warn { 'WARN ' } + .error { 'ERROR' } + .fatal { 'FATAL' } + } + eprintln('[$level_str] $message') +} + +// debug logs a debug message to stderr +pub fn debug(message string) { + log(.debug, message) +} + +// info logs an info message to stderr +pub fn info(message string) { + log(.info, message) +} + +// warn logs a warning message to stderr +pub fn warn(message string) { + log(.warn, message) +} + +// error logs an error message to stderr +pub fn error(message string) { + log(.error, message) +} + +// fatal logs a fatal error message to stderr and exits the program +pub fn fatal(message string) { + log(.fatal, message) + exit(1) +} diff --git a/lib/mcp/v_do/server.v b/lib/mcp/v_do/server.v new file mode 100644 index 00000000..5127c370 --- /dev/null +++ b/lib/mcp/v_do/server.v @@ -0,0 +1,188 @@ +module v_do + +import json +import os +import freeflowuniverse.herolib.mcp.v_do.handlers +import freeflowuniverse.herolib.mcp.v_do.logger + +// MCP server implementation using stdio transport +// Based on https://modelcontextprotocol.io/docs/concepts/transports + +// MCPRequest represents an MCP request message +struct MCPRequest { + id string + method string + params map[string]string + jsonrpc string = '2.0' +} + +// MCPResponse represents an MCP response +struct MCPResponse { + id string + result map[string]string + jsonrpc string = '2.0' +} + +// MCPErrorResponse represents an MCP error response +struct MCPErrorResponse { + id string + error MCPError + jsonrpc string = '2.0' +} + +// MCPError represents an error in an MCP response +struct MCPError { + code int + message string +} + +// Server is the main MCP server struct +pub struct Server {} + +// new_server creates a new MCP server +pub fn new_server() &Server { + return &Server{} +} + +// start starts the MCP server +pub fn (mut s Server) start() ! { + logger.info('Starting V-Do MCP server') + for { + message := s.read_message() or { + logger.error('Failed to parse message: $err') + s.send_error('0', -32700, 'Failed to parse message: $err') + continue + } + + logger.debug('Received message: ${message.method}') + s.handle_message(message) or { + logger.error('Internal error: $err') + s.send_error(message.id, -32603, 'Internal error: $err') + } + } +} + +// read_message reads an MCP message from stdin +fn (mut s Server) read_message() !MCPRequest { + mut content_length := 0 + + // Read headers + for { + line := read_line_from_stdin() or { + logger.error('Failed to read line: $err') + return error('Failed to read line: $err') + } + if line.len == 0 { + break + } + + if line.starts_with('Content-Length:') { + content_length_str := line.all_after('Content-Length:').trim_space() + content_length = content_length_str.int() + } + } + + if content_length == 0 { + logger.error('No Content-Length header found') + return error('No Content-Length header found') + } + + // Read message body + body := read_content_from_stdin(content_length) or { + logger.error('Failed to read content: $err') + return error('Failed to read content: $err') + } + + // Parse JSON + message := json.decode(MCPRequest, body) or { + logger.error('Failed to decode JSON: $err') + return error('Failed to decode JSON: $err') + } + + return message +} + +// read_line_from_stdin reads a line from stdin +fn read_line_from_stdin() !string { + line := os.get_line() + return line +} + +// read_content_from_stdin reads content from stdin with the specified length +fn read_content_from_stdin(length int) !string { + // For MCP protocol, we need to read exactly the content length + mut content := '' + mut reader := os.stdin() + mut buf := []u8{len: length} + n := reader.read(mut buf) or { + logger.error('Failed to read from stdin: $err') + return error('Failed to read from stdin: $err') + } + + if n < length { + logger.error('Expected to read $length bytes, but got $n') + return error('Expected to read $length bytes, but got $n') + } + + content = buf[..n].bytestr() + return content +} + +// handle_message handles an MCP message +fn (mut s Server) handle_message(message MCPRequest) ! { + match message.method { + 'test' { + fullpath := message.params['fullpath'] or { + logger.error('Missing fullpath parameter') + s.send_error(message.id, -32602, 'Missing fullpath parameter') + return error('Missing fullpath parameter') + } + logger.info('Running test on $fullpath') + result := handlers.vtest(fullpath) or { + logger.error('Test failed: $err') + s.send_error(message.id, -32000, 'Test failed: $err') + return err + } + s.send_response(message.id, {'output': result}) + } + else { + logger.error('Unknown method: ${message.method}') + s.send_error(message.id, -32601, 'Unknown method: ${message.method}') + return error('Unknown method: ${message.method}') + } + } +} + +// send_response sends an MCP response +fn (mut s Server) send_response(id string, result map[string]string) { + response := MCPResponse{ + id: id + result: result + } + + json_str := json.encode(response) + logger.debug('Sending response for id: $id') + s.write_message(json_str) +} + +// send_error sends an MCP error response +fn (mut s Server) send_error(id string, code int, message string) { + logger.error('Sending error response: $message (code: $code, id: $id)') + error_response := MCPErrorResponse{ + id: id + error: MCPError{ + code: code + message: message + } + } + + json_str := json.encode(error_response) + s.write_message(json_str) +} + +// write_message writes an MCP message to stdout +fn (mut s Server) write_message(content string) { + header := 'Content-Length: ${content.len}\r\n\r\n' + print(header) + print(content) +} diff --git a/lib/mcp/v_do/test_client.vsh b/lib/mcp/v_do/test_client.vsh new file mode 100644 index 00000000..75b0c495 --- /dev/null +++ b/lib/mcp/v_do/test_client.vsh @@ -0,0 +1,105 @@ +#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run + +import os +import flag +import json + +// Simple test client for the V-Do MCP server +// This script sends test requests to the MCP server and displays the responses + +struct MCPRequest { + id string + method string + params map[string]string + jsonrpc string = '2.0' +} + +fn send_request(method string, fullpath string) { + // Create the request + request := MCPRequest{ + id: '1' + method: method + params: { + 'fullpath': fullpath + } + } + + // Encode to JSON + json_str := json.encode(request) + + // Format the message with headers + message := 'Content-Length: ${json_str.len}\r\n\r\n${json_str}' + + // Write to a temporary file + os.write_file('/tmp/mcp_request.txt', message) or { + eprintln('Failed to write request to file: $err') + return + } + + // Execute the MCP server with the request + cmd := 'cat /tmp/mcp_request.txt | v run /Users/despiegk/code/github/freeflowuniverse/herolib/lib/mcp/v_do/main.v' + result := os.execute(cmd) + + if result.exit_code != 0 { + eprintln('Error executing MCP server: ${result.output}') + return + } + + // Parse and display the response + response := result.output + println('Raw response:') + println('-----------------------------------') + println(response) + println('-----------------------------------') + + // Try to extract the JSON part + if response.contains('{') && response.contains('}') { + json_start := response.index_after('{', 0) + json_end := response.last_index_of('}') + if json_start >= 0 && json_end >= 0 && json_end > json_start { + json_part := response[json_start-1..json_end+1] + println('Extracted JSON:') + println(json_part) + } + } +} + +// Parse command line arguments +mut fp := flag.new_flag_parser(os.args) +fp.application('test_client.vsh') +fp.version('v0.1.0') +fp.description('Test client for V-Do MCP server') +fp.skip_executable() + +method := fp.string('method', `m`, 'test', 'Method to call (test, run, compile, vet)') +fullpath := fp.string('path', `p`, '', 'Path to the file or directory to process') +help_requested := fp.bool('help', `h`, false, 'Show help message') + +if help_requested { + println(fp.usage()) + exit(0) +} + +additional_args := fp.finalize() or { + eprintln(err) + println(fp.usage()) + exit(1) +} + +if fullpath == '' { + eprintln('Error: Path is required') + println(fp.usage()) + exit(1) +} + +// Validate method +valid_methods := ['test', 'run', 'compile', 'vet'] +if method !in valid_methods { + eprintln('Error: Invalid method. Must be one of: ${valid_methods}') + println(fp.usage()) + exit(1) +} + +// Send the request +println('Sending $method request for $fullpath...') +send_request(method, fullpath) diff --git a/lib/mcp/v_do/vdo.v b/lib/mcp/v_do/vdo.v new file mode 100644 index 00000000..b137e639 --- /dev/null +++ b/lib/mcp/v_do/vdo.v @@ -0,0 +1,12 @@ +module v_do + +import freeflowuniverse.herolib.mcp.v_do.logger + +fn main() { + logger.info('Starting V-Do server') + mut server := new_server() + server.start() or { + logger.fatal('Error starting server: $err') + exit(1) + } +} diff --git a/lib/osal/coredns/README.md b/lib/osal/coredns/README.md index 951db5c7..04203297 100644 --- a/lib/osal/coredns/README.md +++ b/lib/osal/coredns/README.md @@ -3,7 +3,7 @@ This module provides functionality for managing DNS records in Redis for use with CoreDNS. It supports various DNS record types and provides a simple interface for adding and managing DNS records. ```v -import freeflowuniverse.herolib.lib.osal.coredns +import freeflowuniverse.herolib.osal.coredns // Create a new DNS record set mut rs := coredns.new_dns_record_set() diff --git a/lib/osal/traefik/README.md b/lib/osal/traefik/README.md index 23f3468a..47076086 100644 --- a/lib/osal/traefik/README.md +++ b/lib/osal/traefik/README.md @@ -14,7 +14,7 @@ The module allows you to: ## Usage Example ```v -import freeflowuniverse.herolib.lib.osal.traefik +import freeflowuniverse.herolib.osal.traefik fn main() ! { // Create a new Traefik configuration diff --git a/lib/web/docusaurus/config.v b/lib/web/docusaurus/config.v index 46669b4c..f924365a 100644 --- a/lib/web/docusaurus/config.v +++ b/lib/web/docusaurus/config.v @@ -246,7 +246,7 @@ pub fn load_config(cfg_dir string) !Config { // Load and parse navbar config navbar_content := os.read_file(os.join_path(cfg_dir, 'navbar.json'))! navbar := json.decode(Navbar, navbar_content) or { - eprintln('navbar.json in ${cfg_dir} is not in the right format please fix.\nError: ${err}') + eprintln('navbar.json in ${cfg_dir} is not in the right format please fix.\nError: $err') exit(99) }