...
This commit is contained in:
1
cli/.gitignore
vendored
1
cli/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
hero
|
hero
|
||||||
compile
|
compile
|
||||||
compile_upload
|
compile_upload
|
||||||
|
vdo
|
||||||
|
|||||||
12
cli/vdo.v
Normal file
12
cli/vdo.v
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
209
examples/clients/qdrant_example.vsh
Normal file
209
examples/clients/qdrant_example.vsh
Normal file
@@ -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')
|
||||||
@@ -3,5 +3,8 @@
|
|||||||
import freeflowuniverse.herolib.installers.lang.python as python_module
|
import freeflowuniverse.herolib.installers.lang.python as python_module
|
||||||
|
|
||||||
mut python_installer := python_module.get()!
|
mut python_installer := python_module.get()!
|
||||||
// python_installer.install()!
|
python_installer.install()!
|
||||||
python_installer.destroy()!
|
|
||||||
|
|
||||||
|
|
||||||
|
// python_installer.destroy()!
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module jina
|
module jina
|
||||||
|
|
||||||
import freeflowuniverse.herolib.core.httpconnection
|
import freeflowuniverse.herolib.core.httpconnection
|
||||||
import os
|
// import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@[params]
|
@[params]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module jina
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.core.base
|
import freeflowuniverse.herolib.core.base
|
||||||
import freeflowuniverse.herolib.core.playbook
|
import freeflowuniverse.herolib.core.playbook
|
||||||
import freeflowuniverse.herolib.ui.console
|
// import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
__global (
|
__global (
|
||||||
jina_global map[string]&Jina
|
jina_global map[string]&Jina
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module jina
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.data.encoderhero
|
import freeflowuniverse.herolib.data.encoderhero
|
||||||
import freeflowuniverse.herolib.core.httpconnection
|
import freeflowuniverse.herolib.core.httpconnection
|
||||||
import net.http
|
// import net.http
|
||||||
import os
|
import os
|
||||||
|
|
||||||
pub const version = '0.0.0'
|
pub const version = '0.0.0'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module jina
|
module jina
|
||||||
|
|
||||||
import json
|
// import json
|
||||||
|
|
||||||
pub enum JinaRerankModel {
|
pub enum JinaRerankModel {
|
||||||
reranker_v2_base_multilingual // 278M
|
reranker_v2_base_multilingual // 278M
|
||||||
|
|||||||
394
lib/clients/qdrant/qdrant_client.v
Normal file
394
lib/clients/qdrant/qdrant_client.v
Normal file
@@ -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
|
||||||
|
}
|
||||||
117
lib/clients/qdrant/qdrant_client_test.v
Normal file
117
lib/clients/qdrant/qdrant_client_test.v
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ module qdrant
|
|||||||
|
|
||||||
import freeflowuniverse.herolib.core.base
|
import freeflowuniverse.herolib.core.base
|
||||||
import freeflowuniverse.herolib.core.playbook
|
import freeflowuniverse.herolib.core.playbook
|
||||||
import freeflowuniverse.herolib.ui.console
|
// import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
__global (
|
__global (
|
||||||
qdrant_global map[string]&QDrantClient
|
qdrant_global map[string]&QDrantClient
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
module qdrant
|
module qdrant
|
||||||
|
|
||||||
import freeflowuniverse.herolib.data.paramsparser
|
// import freeflowuniverse.herolib.data.paramsparser
|
||||||
import freeflowuniverse.herolib.data.encoderhero
|
import freeflowuniverse.herolib.data.encoderhero
|
||||||
import os
|
// import json
|
||||||
|
// import os
|
||||||
|
|
||||||
pub const version = '0.0.0'
|
pub const version = '0.0.0'
|
||||||
const singleton = false
|
const singleton = false
|
||||||
@@ -15,7 +16,7 @@ pub struct QDrantClient {
|
|||||||
pub mut:
|
pub mut:
|
||||||
name string = 'default'
|
name string = 'default'
|
||||||
secret string
|
secret string
|
||||||
url string = "http://localhost:6333/"
|
url string = 'http://localhost:6333/'
|
||||||
}
|
}
|
||||||
|
|
||||||
// your checking & initialization code if needed
|
// your checking & initialization code if needed
|
||||||
@@ -34,3 +35,363 @@ pub fn heroscript_loads(heroscript string) !QDrantClient {
|
|||||||
mut obj := encoderhero.decode[QDrantClient](heroscript)!
|
mut obj := encoderhero.decode[QDrantClient](heroscript)!
|
||||||
return obj
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
## Usage
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.clients. qdrant
|
|
||||||
|
|
||||||
mut client:= qdrant.get()!
|
|
||||||
|
|
||||||
client...
|
|
||||||
|
|
||||||
|
### 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
|
```hero
|
||||||
!!qdrant.configure
|
!!qdrant.configure
|
||||||
secret: '...'
|
name: 'default'
|
||||||
host: 'localhost'
|
secret: 'your_api_key'
|
||||||
port: 8888
|
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.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ To get started
|
|||||||
```vlang
|
```vlang
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.lib.installers.infra.coredns as coredns_installer
|
import freeflowuniverse.herolib.installers.infra.coredns as coredns_installer
|
||||||
|
|
||||||
heroscript:="
|
heroscript:="
|
||||||
!!coredns.configure name:'test'
|
!!coredns.configure name:'test'
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
module python
|
module python
|
||||||
|
|
||||||
import freeflowuniverse.herolib.data.paramsparser
|
|
||||||
import freeflowuniverse.herolib.data.encoderhero
|
import freeflowuniverse.herolib.data.encoderhero
|
||||||
import os
|
|
||||||
|
|
||||||
pub const version = '3.12.0'
|
pub const version = '3.12.0'
|
||||||
const singleton = true
|
const singleton = true
|
||||||
|
|||||||
44
lib/mcp/aiprompt.md
Normal file
44
lib/mcp/aiprompt.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
92
lib/mcp/v_do/README.md
Normal file
92
lib/mcp/v_do/README.md
Normal file
@@ -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: <length>
|
||||||
|
|
||||||
|
{"jsonrpc":"2.0","id":"<request-id>","method":"<method-name>","params":{"fullpath":"<path-to-file-or-directory>"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
- `<length>` is the length of the JSON message in bytes
|
||||||
|
- `<request-id>` is a unique identifier for the request
|
||||||
|
- `<method-name>` is one of: `test`, `run`, `compile`, or `vet`
|
||||||
|
- `<path-to-file-or-directory>` 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}
|
||||||
|
```
|
||||||
4
lib/mcp/v_do/handlers/vcompile.v
Normal file
4
lib/mcp/v_do/handlers/vcompile.v
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module handlers
|
||||||
|
|
||||||
|
import os
|
||||||
|
import freeflowuniverse.herolib.mcp.v_do.logger
|
||||||
21
lib/mcp/v_do/handlers/vlist.v
Normal file
21
lib/mcp/v_do/handlers/vlist.v
Normal file
@@ -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
|
||||||
|
}
|
||||||
4
lib/mcp/v_do/handlers/vrun.v
Normal file
4
lib/mcp/v_do/handlers/vrun.v
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module handlers
|
||||||
|
|
||||||
|
import os
|
||||||
|
import freeflowuniverse.herolib.mcp.v_do.logger
|
||||||
31
lib/mcp/v_do/handlers/vtest.v
Normal file
31
lib/mcp/v_do/handlers/vtest.v
Normal file
@@ -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}'
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
42
lib/mcp/v_do/handlers/vvet.v
Normal file
42
lib/mcp/v_do/handlers/vvet.v
Normal file
@@ -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}'
|
||||||
|
}
|
||||||
50
lib/mcp/v_do/logger/logger.v
Normal file
50
lib/mcp/v_do/logger/logger.v
Normal file
@@ -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)
|
||||||
|
}
|
||||||
188
lib/mcp/v_do/server.v
Normal file
188
lib/mcp/v_do/server.v
Normal file
@@ -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)
|
||||||
|
}
|
||||||
105
lib/mcp/v_do/test_client.vsh
Normal file
105
lib/mcp/v_do/test_client.vsh
Normal file
@@ -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)
|
||||||
12
lib/mcp/v_do/vdo.v
Normal file
12
lib/mcp/v_do/vdo.v
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
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
|
```v
|
||||||
import freeflowuniverse.herolib.lib.osal.coredns
|
import freeflowuniverse.herolib.osal.coredns
|
||||||
|
|
||||||
// Create a new DNS record set
|
// Create a new DNS record set
|
||||||
mut rs := coredns.new_dns_record_set()
|
mut rs := coredns.new_dns_record_set()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ The module allows you to:
|
|||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
```v
|
```v
|
||||||
import freeflowuniverse.herolib.lib.osal.traefik
|
import freeflowuniverse.herolib.osal.traefik
|
||||||
|
|
||||||
fn main() ! {
|
fn main() ! {
|
||||||
// Create a new Traefik configuration
|
// Create a new Traefik configuration
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ pub fn load_config(cfg_dir string) !Config {
|
|||||||
// Load and parse navbar config
|
// Load and parse navbar config
|
||||||
navbar_content := os.read_file(os.join_path(cfg_dir, 'navbar.json'))!
|
navbar_content := os.read_file(os.join_path(cfg_dir, 'navbar.json'))!
|
||||||
navbar := json.decode(Navbar, navbar_content) or {
|
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)
|
exit(99)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user