...
This commit is contained in:
1
cli/.gitignore
vendored
1
cli/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
hero
|
||||
compile
|
||||
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
|
||||
|
||||
mut python_installer := python_module.get()!
|
||||
// python_installer.install()!
|
||||
python_installer.destroy()!
|
||||
python_installer.install()!
|
||||
|
||||
|
||||
|
||||
// python_installer.destroy()!
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module jina
|
||||
|
||||
import json
|
||||
// import json
|
||||
|
||||
pub enum JinaRerankModel {
|
||||
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.playbook
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
// import freeflowuniverse.herolib.ui.console
|
||||
|
||||
__global (
|
||||
qdrant_global map[string]&QDrantClient
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
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.
|
||||
|
||||
```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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user