diff --git a/docs/api-reference/openapi.json b/docs/api-reference/openapi.json index 337f4913..e79552a3 100644 --- a/docs/api-reference/openapi.json +++ b/docs/api-reference/openapi.json @@ -1830,134 +1830,6 @@ } ] } }, - "/space/{space_id}/semantic_glob" : { - "get" : { - "description" : "Retrieve the semantic glob (glob) search results for page/folder titles within a space by its ID", - "parameters" : [ { - "description" : "Space ID", - "in" : "path", - "name" : "space_id", - "required" : true, - "schema" : { - "format" : "uuid", - "type" : "string" - } - }, { - "description" : "Search query for page/folder titles", - "in" : "query", - "name" : "query", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "description" : "Maximum number of results to return (1-50, default 10)", - "in" : "query", - "name" : "limit", - "schema" : { - "type" : "integer" - } - }, { - "description" : "Cosine distance threshold (0=identical, 2=opposite)", - "in" : "query", - "name" : "threshold", - "schema" : { - "format" : "float64", - "type" : "number" - } - } ], - "responses" : { - "200" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/_space__space_id__semantic_glob_get_200_response" - } - } - }, - "description" : "OK" - } - }, - "security" : [ { - "BearerAuth" : [ ] - } ], - "summary" : "Get semantic glob", - "tags" : [ "space" ], - "x-code-samples" : [ { - "label" : "Python", - "lang" : "python", - "source" : "from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic glob search\nresults = client.spaces.semantic_glob(\n space_id='space-uuid',\n query='authentication and authorization pages',\n limit=10,\n threshold=1.0\n)\nfor block in results:\n print(f\"{block.title} - {block.type}\")\n" - }, { - "label" : "JavaScript", - "lang" : "javascript", - "source" : "import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic glob search\nconst results = await client.spaces.semanticGlobal('space-uuid', {\n query: 'authentication and authorization pages',\n limit: 10,\n threshold: 1.0\n});\nfor (const block of results) {\n console.log(`${block.title} - ${block.type}`);\n}\n" - } ] - } - }, - "/space/{space_id}/semantic_grep" : { - "get" : { - "description" : "Retrieve the semantic grep search results for content blocks within a space by its ID", - "parameters" : [ { - "description" : "Space ID", - "in" : "path", - "name" : "space_id", - "required" : true, - "schema" : { - "format" : "uuid", - "type" : "string" - } - }, { - "description" : "Search query for content blocks", - "in" : "query", - "name" : "query", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "description" : "Maximum number of results to return (1-50, default 10)", - "in" : "query", - "name" : "limit", - "schema" : { - "type" : "integer" - } - }, { - "description" : "Cosine distance threshold (0=identical, 2=opposite)", - "in" : "query", - "name" : "threshold", - "schema" : { - "format" : "float64", - "type" : "number" - } - } ], - "responses" : { - "200" : { - "content" : { - "application/json" : { - "schema" : { - "$ref" : "#/components/schemas/_space__space_id__semantic_glob_get_200_response" - } - } - }, - "description" : "OK" - } - }, - "security" : [ { - "BearerAuth" : [ ] - } ], - "summary" : "Get semantic grep", - "tags" : [ "space" ], - "x-code-samples" : [ { - "label" : "Python", - "lang" : "python", - "source" : "from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic grep search\nresults = client.spaces.semantic_grep(\n space_id='space-uuid',\n query='JWT token validation code examples',\n limit=15,\n threshold=0.7\n)\nfor block in results:\n print(f\"{block.title} - distance: {block.distance}\")\n" - }, { - "label" : "JavaScript", - "lang" : "javascript", - "source" : "import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic grep search\nconst results = await client.spaces.semanticGrep('space-uuid', {\n query: 'JWT token validation code examples',\n limit: 15,\n threshold: 0.7\n});\nfor (const block of results) {\n console.log(`${block.title} - distance: ${block.distance}`);\n}\n" - } ] - } - }, "/tool/name" : { "get" : { "description" : "Get all tool names within a project", @@ -2992,21 +2864,6 @@ "type" : "object" } ] }, - "_space__space_id__semantic_glob_get_200_response" : { - "allOf" : [ { - "$ref" : "#/components/schemas/serializer.Response" - }, { - "properties" : { - "data" : { - "items" : { - "$ref" : "#/components/schemas/httpclient.SearchResultBlockItem" - }, - "type" : "array" - } - }, - "type" : "object" - } ] - }, "_tool_name_get_200_response" : { "allOf" : [ { "$ref" : "#/components/schemas/serializer.Response" diff --git a/src/server/api/go/docs/docs.go b/src/server/api/go/docs/docs.go index b5684224..1ff49bbb 100644 --- a/src/server/api/go/docs/docs.go +++ b/src/server/api/go/docs/docs.go @@ -2416,178 +2416,6 @@ const docTemplate = `{ ] } }, - "/space/{space_id}/semantic_glob": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve the semantic glob (glob) search results for page/folder titles within a space by its ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "space" - ], - "summary": "Get semantic glob", - "parameters": [ - { - "type": "string", - "format": "uuid", - "example": "123e4567-e89b-12d3-a456-426614174000", - "description": "Space ID", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Search query for page/folder titles", - "name": "query", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "Maximum number of results to return (1-50, default 10)", - "name": "limit", - "in": "query" - }, - { - "type": "number", - "format": "float64", - "description": "Cosine distance threshold (0=identical, 2=opposite)", - "name": "threshold", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/serializer.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/httpclient.SearchResultBlockItem" - } - } - } - } - ] - } - } - }, - "x-code-samples": [ - { - "label": "Python", - "lang": "python", - "source": "from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic glob search\nresults = client.spaces.semantic_glob(\n space_id='space-uuid',\n query='authentication and authorization pages',\n limit=10,\n threshold=1.0\n)\nfor block in results:\n print(f\"{block.title} - {block.type}\")\n" - }, - { - "label": "JavaScript", - "lang": "javascript", - "source": "import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic glob search\nconst results = await client.spaces.semanticGlobal('space-uuid', {\n query: 'authentication and authorization pages',\n limit: 10,\n threshold: 1.0\n});\nfor (const block of results) {\n console.log(` + "`" + `${block.title} - ${block.type}` + "`" + `);\n}\n" - } - ] - } - }, - "/space/{space_id}/semantic_grep": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve the semantic grep search results for content blocks within a space by its ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "space" - ], - "summary": "Get semantic grep", - "parameters": [ - { - "type": "string", - "format": "uuid", - "example": "123e4567-e89b-12d3-a456-426614174000", - "description": "Space ID", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Search query for content blocks", - "name": "query", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "Maximum number of results to return (1-50, default 10)", - "name": "limit", - "in": "query" - }, - { - "type": "number", - "format": "float64", - "description": "Cosine distance threshold (0=identical, 2=opposite)", - "name": "threshold", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/serializer.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/httpclient.SearchResultBlockItem" - } - } - } - } - ] - } - } - }, - "x-code-samples": [ - { - "label": "Python", - "lang": "python", - "source": "from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic grep search\nresults = client.spaces.semantic_grep(\n space_id='space-uuid',\n query='JWT token validation code examples',\n limit=15,\n threshold=0.7\n)\nfor block in results:\n print(f\"{block.title} - distance: {block.distance}\")\n" - }, - { - "label": "JavaScript", - "lang": "javascript", - "source": "import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic grep search\nconst results = await client.spaces.semanticGrep('space-uuid', {\n query: 'JWT token validation code examples',\n limit: 15,\n threshold: 0.7\n});\nfor (const block of results) {\n console.log(` + "`" + `${block.title} - distance: ${block.distance}` + "`" + `);\n}\n" - } - ] - } - }, "/tool/name": { "get": { "security": [ diff --git a/src/server/api/go/docs/swagger.json b/src/server/api/go/docs/swagger.json index 36a143cb..77366b0d 100644 --- a/src/server/api/go/docs/swagger.json +++ b/src/server/api/go/docs/swagger.json @@ -2413,178 +2413,6 @@ ] } }, - "/space/{space_id}/semantic_glob": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve the semantic glob (glob) search results for page/folder titles within a space by its ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "space" - ], - "summary": "Get semantic glob", - "parameters": [ - { - "type": "string", - "format": "uuid", - "example": "123e4567-e89b-12d3-a456-426614174000", - "description": "Space ID", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Search query for page/folder titles", - "name": "query", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "Maximum number of results to return (1-50, default 10)", - "name": "limit", - "in": "query" - }, - { - "type": "number", - "format": "float64", - "description": "Cosine distance threshold (0=identical, 2=opposite)", - "name": "threshold", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/serializer.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/httpclient.SearchResultBlockItem" - } - } - } - } - ] - } - } - }, - "x-code-samples": [ - { - "label": "Python", - "lang": "python", - "source": "from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic glob search\nresults = client.spaces.semantic_glob(\n space_id='space-uuid',\n query='authentication and authorization pages',\n limit=10,\n threshold=1.0\n)\nfor block in results:\n print(f\"{block.title} - {block.type}\")\n" - }, - { - "label": "JavaScript", - "lang": "javascript", - "source": "import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic glob search\nconst results = await client.spaces.semanticGlobal('space-uuid', {\n query: 'authentication and authorization pages',\n limit: 10,\n threshold: 1.0\n});\nfor (const block of results) {\n console.log(`${block.title} - ${block.type}`);\n}\n" - } - ] - } - }, - "/space/{space_id}/semantic_grep": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve the semantic grep search results for content blocks within a space by its ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "space" - ], - "summary": "Get semantic grep", - "parameters": [ - { - "type": "string", - "format": "uuid", - "example": "123e4567-e89b-12d3-a456-426614174000", - "description": "Space ID", - "name": "space_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Search query for content blocks", - "name": "query", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "Maximum number of results to return (1-50, default 10)", - "name": "limit", - "in": "query" - }, - { - "type": "number", - "format": "float64", - "description": "Cosine distance threshold (0=identical, 2=opposite)", - "name": "threshold", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/serializer.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/httpclient.SearchResultBlockItem" - } - } - } - } - ] - } - } - }, - "x-code-samples": [ - { - "label": "Python", - "lang": "python", - "source": "from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic grep search\nresults = client.spaces.semantic_grep(\n space_id='space-uuid',\n query='JWT token validation code examples',\n limit=15,\n threshold=0.7\n)\nfor block in results:\n print(f\"{block.title} - distance: {block.distance}\")\n" - }, - { - "label": "JavaScript", - "lang": "javascript", - "source": "import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic grep search\nconst results = await client.spaces.semanticGrep('space-uuid', {\n query: 'JWT token validation code examples',\n limit: 15,\n threshold: 0.7\n});\nfor (const block of results) {\n console.log(`${block.title} - distance: ${block.distance}`);\n}\n" - } - ] - } - }, "/tool/name": { "get": { "security": [ diff --git a/src/server/api/go/docs/swagger.yaml b/src/server/api/go/docs/swagger.yaml index 15fd9ecd..e5ab48de 100644 --- a/src/server/api/go/docs/swagger.yaml +++ b/src/server/api/go/docs/swagger.yaml @@ -2692,166 +2692,6 @@ paths: for (const block of result.cited_blocks) { console.log(`${block.title} (distance: ${block.distance})`); } - /space/{space_id}/semantic_glob: - get: - consumes: - - application/json - description: Retrieve the semantic glob (glob) search results for page/folder - titles within a space by its ID - parameters: - - description: Space ID - example: 123e4567-e89b-12d3-a456-426614174000 - format: uuid - in: path - name: space_id - required: true - type: string - - description: Search query for page/folder titles - in: query - name: query - required: true - type: string - - description: Maximum number of results to return (1-50, default 10) - in: query - name: limit - type: integer - - description: Cosine distance threshold (0=identical, 2=opposite) - format: float64 - in: query - name: threshold - type: number - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/serializer.Response' - - properties: - data: - items: - $ref: '#/definitions/httpclient.SearchResultBlockItem' - type: array - type: object - security: - - BearerAuth: [] - summary: Get semantic glob - tags: - - space - x-code-samples: - - label: Python - lang: python - source: | - from acontext import AcontextClient - - client = AcontextClient(api_key='sk_project_token') - - # Semantic glob search - results = client.spaces.semantic_glob( - space_id='space-uuid', - query='authentication and authorization pages', - limit=10, - threshold=1.0 - ) - for block in results: - print(f"{block.title} - {block.type}") - - label: JavaScript - lang: javascript - source: | - import { AcontextClient } from '@acontext/acontext'; - - const client = new AcontextClient({ apiKey: 'sk_project_token' }); - - // Semantic glob search - const results = await client.spaces.semanticGlobal('space-uuid', { - query: 'authentication and authorization pages', - limit: 10, - threshold: 1.0 - }); - for (const block of results) { - console.log(`${block.title} - ${block.type}`); - } - /space/{space_id}/semantic_grep: - get: - consumes: - - application/json - description: Retrieve the semantic grep search results for content blocks within - a space by its ID - parameters: - - description: Space ID - example: 123e4567-e89b-12d3-a456-426614174000 - format: uuid - in: path - name: space_id - required: true - type: string - - description: Search query for content blocks - in: query - name: query - required: true - type: string - - description: Maximum number of results to return (1-50, default 10) - in: query - name: limit - type: integer - - description: Cosine distance threshold (0=identical, 2=opposite) - format: float64 - in: query - name: threshold - type: number - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/serializer.Response' - - properties: - data: - items: - $ref: '#/definitions/httpclient.SearchResultBlockItem' - type: array - type: object - security: - - BearerAuth: [] - summary: Get semantic grep - tags: - - space - x-code-samples: - - label: Python - lang: python - source: | - from acontext import AcontextClient - - client = AcontextClient(api_key='sk_project_token') - - # Semantic grep search - results = client.spaces.semantic_grep( - space_id='space-uuid', - query='JWT token validation code examples', - limit=15, - threshold=0.7 - ) - for block in results: - print(f"{block.title} - distance: {block.distance}") - - label: JavaScript - lang: javascript - source: | - import { AcontextClient } from '@acontext/acontext'; - - const client = new AcontextClient({ apiKey: 'sk_project_token' }); - - // Semantic grep search - const results = await client.spaces.semanticGrep('space-uuid', { - query: 'JWT token validation code examples', - limit: 15, - threshold: 0.7 - }); - for (const block of results) { - console.log(`${block.title} - distance: ${block.distance}`); - } /tool/name: get: consumes: diff --git a/src/server/api/go/internal/infra/httpclient/core.go b/src/server/api/go/internal/infra/httpclient/core.go index 7d6dcac3..64550bb0 100644 --- a/src/server/api/go/internal/infra/httpclient/core.go +++ b/src/server/api/go/internal/infra/httpclient/core.go @@ -51,20 +51,6 @@ type SpaceSearchResult struct { CitedBlocks []SearchResultBlockItem `json:"cited_blocks"` } -// SemanticGrepRequest represents the request for semantic grep -type SemanticGrepRequest struct { - Query string `json:"query"` - Limit int `json:"limit"` - Threshold *float64 `json:"threshold"` -} - -// SemanticGlobalRequest represents the request for semantic glob (glob) -type SemanticGlobalRequest struct { - Query string `json:"query"` - Limit int `json:"limit"` - Threshold *float64 `json:"threshold"` -} - // ExperienceSearchRequest represents the request for experience search type ExperienceSearchRequest struct { Query string `json:"query"` @@ -74,102 +60,6 @@ type ExperienceSearchRequest struct { MaxIterations int `json:"max_iterations"` } -// SemanticGrep calls the semantic_grep endpoint -func (c *CoreClient) SemanticGrep(ctx context.Context, projectID, spaceID uuid.UUID, req SemanticGrepRequest) ([]SearchResultBlockItem, error) { - endpoint := fmt.Sprintf("%s/api/v1/project/%s/space/%s/semantic_grep", c.BaseURL, projectID.String(), spaceID.String()) - - // Build query parameters - params := url.Values{} - params.Set("query", req.Query) - params.Set("limit", fmt.Sprintf("%d", req.Limit)) - if req.Threshold != nil { - params.Set("threshold", fmt.Sprintf("%f", *req.Threshold)) - } - - fullURL := fmt.Sprintf("%s?%s", endpoint, params.Encode()) - - httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - // Important: propagate trace context to downstream service - c.Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header)) - - resp, err := c.HTTPClient.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("do request: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("read response body: %w", err) - } - - if resp.StatusCode != http.StatusOK { - c.Logger.Error("semantic_grep request failed", - zap.Int("status_code", resp.StatusCode), - zap.String("body", string(body))) - return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body)) - } - - var result []SearchResultBlockItem - if err := sonic.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("unmarshal response: %w", err) - } - - return result, nil -} - -// SemanticGlobal calls the semantic_glob endpoint -func (c *CoreClient) SemanticGlobal(ctx context.Context, projectID, spaceID uuid.UUID, req SemanticGlobalRequest) ([]SearchResultBlockItem, error) { - endpoint := fmt.Sprintf("%s/api/v1/project/%s/space/%s/semantic_glob", c.BaseURL, projectID.String(), spaceID.String()) - - // Build query parameters - params := url.Values{} - params.Set("query", req.Query) - params.Set("limit", fmt.Sprintf("%d", req.Limit)) - if req.Threshold != nil { - params.Set("threshold", fmt.Sprintf("%f", *req.Threshold)) - } - - fullURL := fmt.Sprintf("%s?%s", endpoint, params.Encode()) - - httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - - // Important: propagate trace context to downstream service - c.Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header)) - - resp, err := c.HTTPClient.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("do request: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("read response body: %w", err) - } - - if resp.StatusCode != http.StatusOK { - c.Logger.Error("semantic_glob request failed", - zap.Int("status_code", resp.StatusCode), - zap.String("body", string(body))) - return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body)) - } - - var result []SearchResultBlockItem - if err := sonic.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("unmarshal response: %w", err) - } - - return result, nil -} - // ExperienceSearch calls the experience_search endpoint func (c *CoreClient) ExperienceSearch(ctx context.Context, projectID, spaceID uuid.UUID, req ExperienceSearchRequest) (*SpaceSearchResult, error) { endpoint := fmt.Sprintf("%s/api/v1/project/%s/space/%s/experience_search", c.BaseURL, projectID.String(), spaceID.String()) diff --git a/src/server/api/go/internal/modules/handler/space.go b/src/server/api/go/internal/modules/handler/space.go index c2275001..0f109fd4 100644 --- a/src/server/api/go/internal/modules/handler/space.go +++ b/src/server/api/go/internal/modules/handler/space.go @@ -277,118 +277,6 @@ func (h *SpaceHandler) GetExperienceSearch(c *gin.Context) { c.JSON(http.StatusOK, serializer.Response{Data: result}) } -type GetSemanticGlobalReq struct { - Query string `form:"query" json:"query" binding:"required"` - Limit int `form:"limit,default=10" json:"limit" binding:"omitempty,min=1,max=50"` - Threshold *float64 `form:"threshold" json:"threshold" binding:"omitempty,min=0,max=2"` -} - -// GetSemanticGlobal godoc -// -// @Summary Get semantic glob -// @Description Retrieve the semantic glob (glob) search results for page/folder titles within a space by its ID -// @Tags space -// @Accept json -// @Produce json -// @Param space_id path string true "Space ID" Format(uuid) Example(123e4567-e89b-12d3-a456-426614174000) -// @Param query query string true "Search query for page/folder titles" -// @Param limit query int false "Maximum number of results to return (1-50, default 10)" -// @Param threshold query float64 false "Cosine distance threshold (0=identical, 2=opposite)" -// @Security BearerAuth -// @Success 200 {object} serializer.Response{data=[]httpclient.SearchResultBlockItem} -// @Router /space/{space_id}/semantic_glob [get] -// @x-code-samples [{"lang":"python","source":"from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic glob search\nresults = client.spaces.semantic_glob(\n space_id='space-uuid',\n query='authentication and authorization pages',\n limit=10,\n threshold=1.0\n)\nfor block in results:\n print(f\"{block.title} - {block.type}\")\n","label":"Python"},{"lang":"javascript","source":"import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic glob search\nconst results = await client.spaces.semanticGlobal('space-uuid', {\n query: 'authentication and authorization pages',\n limit: 10,\n threshold: 1.0\n});\nfor (const block of results) {\n console.log(`${block.title} - ${block.type}`);\n}\n","label":"JavaScript"}] -func (h *SpaceHandler) GetSemanticGlobal(c *gin.Context) { - spaceID, err := uuid.Parse(c.Param("space_id")) - if err != nil { - c.JSON(http.StatusBadRequest, serializer.ParamErr("", err)) - return - } - - req := GetSemanticGlobalReq{ - Limit: 10, - } - if err := c.ShouldBindQuery(&req); err != nil { - c.JSON(http.StatusBadRequest, serializer.ParamErr("", err)) - return - } - - project, ok := c.MustGet("project").(*model.Project) - if !ok { - c.JSON(http.StatusBadRequest, serializer.ParamErr("", errors.New("project not found"))) - return - } - - // Call core service (semantic_glob endpoint) - result, err := h.coreClient.SemanticGlobal(c.Request.Context(), project.ID, spaceID, httpclient.SemanticGlobalRequest{ - Query: req.Query, - Limit: req.Limit, - Threshold: req.Threshold, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, serializer.Err(http.StatusInternalServerError, "Failed to call core service", err)) - return - } - - c.JSON(http.StatusOK, serializer.Response{Data: result}) -} - -type GetSemanticGrepReq struct { - Query string `form:"query" json:"query" binding:"required"` - Limit int `form:"limit,default=10" json:"limit" binding:"omitempty,min=1,max=50"` - Threshold *float64 `form:"threshold" json:"threshold" binding:"omitempty,min=0,max=2"` -} - -// GetSemanticGrep godoc -// -// @Summary Get semantic grep -// @Description Retrieve the semantic grep search results for content blocks within a space by its ID -// @Tags space -// @Accept json -// @Produce json -// @Param space_id path string true "Space ID" Format(uuid) Example(123e4567-e89b-12d3-a456-426614174000) -// @Param query query string true "Search query for content blocks" -// @Param limit query int false "Maximum number of results to return (1-50, default 10)" -// @Param threshold query float64 false "Cosine distance threshold (0=identical, 2=opposite)" -// @Security BearerAuth -// @Success 200 {object} serializer.Response{data=[]httpclient.SearchResultBlockItem} -// @Router /space/{space_id}/semantic_grep [get] -// @x-code-samples [{"lang":"python","source":"from acontext import AcontextClient\n\nclient = AcontextClient(api_key='sk_project_token')\n\n# Semantic grep search\nresults = client.spaces.semantic_grep(\n space_id='space-uuid',\n query='JWT token validation code examples',\n limit=15,\n threshold=0.7\n)\nfor block in results:\n print(f\"{block.title} - distance: {block.distance}\")\n","label":"Python"},{"lang":"javascript","source":"import { AcontextClient } from '@acontext/acontext';\n\nconst client = new AcontextClient({ apiKey: 'sk_project_token' });\n\n// Semantic grep search\nconst results = await client.spaces.semanticGrep('space-uuid', {\n query: 'JWT token validation code examples',\n limit: 15,\n threshold: 0.7\n});\nfor (const block of results) {\n console.log(`${block.title} - distance: ${block.distance}`);\n}\n","label":"JavaScript"}] -func (h *SpaceHandler) GetSemanticGrep(c *gin.Context) { - spaceID, err := uuid.Parse(c.Param("space_id")) - if err != nil { - c.JSON(http.StatusBadRequest, serializer.ParamErr("", err)) - return - } - - req := GetSemanticGrepReq{ - Limit: 10, - } - if err := c.ShouldBindQuery(&req); err != nil { - c.JSON(http.StatusBadRequest, serializer.ParamErr("", err)) - return - } - - project, ok := c.MustGet("project").(*model.Project) - if !ok { - c.JSON(http.StatusBadRequest, serializer.ParamErr("", errors.New("project not found"))) - return - } - - // Call core service - result, err := h.coreClient.SemanticGrep(c.Request.Context(), project.ID, spaceID, httpclient.SemanticGrepRequest{ - Query: req.Query, - Limit: req.Limit, - Threshold: req.Threshold, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, serializer.Err(http.StatusInternalServerError, "Failed to call core service", err)) - return - } - - c.JSON(http.StatusOK, serializer.Response{Data: result}) -} - type ListExperienceConfirmationsReq struct { Limit int `form:"limit,default=20" json:"limit" binding:"required,min=1,max=200" example:"20"` Cursor string `form:"cursor" json:"cursor" example:"cHJvdGVjdGVkIHZlcnNpb24gdG8gYmUgZXhjbHVkZWQgaW4gcGFyc2luZyB0aGUgY3Vyc29y"` diff --git a/src/server/api/go/internal/modules/handler/space_test.go b/src/server/api/go/internal/modules/handler/space_test.go index b716b237..54fe34ff 100644 --- a/src/server/api/go/internal/modules/handler/space_test.go +++ b/src/server/api/go/internal/modules/handler/space_test.go @@ -476,116 +476,6 @@ func TestSpaceHandler_GetExperienceSearch(t *testing.T) { } } -func TestSpaceHandler_GetSemanticGlobal(t *testing.T) { - spaceID := uuid.New() - - tests := []struct { - name string - spaceIDParam string - requestBody GetSemanticGlobalReq - expectedStatus int - }{ - { - name: "successful global semantic call (will fail without core service)", - spaceIDParam: spaceID.String(), - requestBody: GetSemanticGlobalReq{ - Query: "global search test", - }, - expectedStatus: http.StatusInternalServerError, // Expected to fail without core service - }, - { - name: "invalid space ID", - spaceIDParam: "invalid-uuid", - requestBody: GetSemanticGlobalReq{Query: "test"}, - expectedStatus: http.StatusBadRequest, - }, - { - name: "empty query", - spaceIDParam: spaceID.String(), - requestBody: GetSemanticGlobalReq{Query: ""}, - expectedStatus: http.StatusBadRequest, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - handler := NewSpaceHandler(&MockSpaceService{}, getMockCoreClient()) - router := setupSpaceRouter() - - // Add middleware to set project in context - router.Use(func(c *gin.Context) { - c.Set("project", &model.Project{ID: uuid.New()}) - c.Next() - }) - - router.GET("/space/:space_id/semantic_glob", handler.GetSemanticGlobal) - - queryString := "?query=" + url.QueryEscape(tt.requestBody.Query) - req := httptest.NewRequest("GET", "/space/"+tt.spaceIDParam+"/semantic_glob"+queryString, nil) - w := httptest.NewRecorder() - - router.ServeHTTP(w, req) - - assert.Equal(t, tt.expectedStatus, w.Code) - }) - } -} - -func TestSpaceHandler_GetSemanticGrep(t *testing.T) { - spaceID := uuid.New() - - tests := []struct { - name string - spaceIDParam string - requestBody GetSemanticGrepReq - expectedStatus int - }{ - { - name: "successful semantic grep call (will fail without core service)", - spaceIDParam: spaceID.String(), - requestBody: GetSemanticGrepReq{ - Query: "grep search test", - }, - expectedStatus: http.StatusInternalServerError, // Expected to fail without core service - }, - { - name: "invalid space ID", - spaceIDParam: "invalid-uuid", - requestBody: GetSemanticGrepReq{Query: "test"}, - expectedStatus: http.StatusBadRequest, - }, - { - name: "empty query", - spaceIDParam: spaceID.String(), - requestBody: GetSemanticGrepReq{Query: ""}, - expectedStatus: http.StatusBadRequest, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - handler := NewSpaceHandler(&MockSpaceService{}, getMockCoreClient()) - router := setupSpaceRouter() - - // Add middleware to set project in context - router.Use(func(c *gin.Context) { - c.Set("project", &model.Project{ID: uuid.New()}) - c.Next() - }) - - router.GET("/space/:space_id/semantic_grep", handler.GetSemanticGrep) - - queryString := "?query=" + url.QueryEscape(tt.requestBody.Query) - req := httptest.NewRequest("GET", "/space/"+tt.spaceIDParam+"/semantic_grep"+queryString, nil) - w := httptest.NewRecorder() - - router.ServeHTTP(w, req) - - assert.Equal(t, tt.expectedStatus, w.Code) - }) - } -} - func TestSpaceHandler_ListExperienceConfirmations(t *testing.T) { projectID := uuid.New() spaceID := uuid.New() diff --git a/src/server/api/go/internal/router/router.go b/src/server/api/go/internal/router/router.go index 97f30591..a43cda4b 100644 --- a/src/server/api/go/internal/router/router.go +++ b/src/server/api/go/internal/router/router.go @@ -134,8 +134,6 @@ func NewRouter(d RouterDeps) *gin.Engine { space.GET("/:space_id/configs", d.SpaceHandler.GetConfigs) space.GET("/:space_id/experience_search", d.SpaceHandler.GetExperienceSearch) - space.GET("/:space_id/semantic_glob", d.SpaceHandler.GetSemanticGlobal) - space.GET("/:space_id/semantic_grep", d.SpaceHandler.GetSemanticGrep) space.GET("/:space_id/experience_confirmations", d.SpaceHandler.ListExperienceConfirmations) space.PATCH("/:space_id/experience_confirmations/:experience_id", d.SpaceHandler.ConfirmExperience) diff --git a/src/server/core/acontext_core/constants.py b/src/server/core/acontext_core/constants.py index e69de29b..4f1d15a2 100644 --- a/src/server/core/acontext_core/constants.py +++ b/src/server/core/acontext_core/constants.py @@ -0,0 +1,5 @@ +class MetricTags: + new_task_created = "task.created" + new_skill_learned = "space.learned" + new_experience_agentic_search = "search.experience.agentic" + new_experience_embedding_search = "search.experience.embedding" diff --git a/src/server/core/acontext_core/llm/agent/space_search.py b/src/server/core/acontext_core/llm/agent/space_search.py index 13068ef6..eab2f232 100644 --- a/src/server/core/acontext_core/llm/agent/space_search.py +++ b/src/server/core/acontext_core/llm/agent/space_search.py @@ -1,3 +1,4 @@ +import asyncio from ...env import LOG, bound_logging_vars from ...infra.db import AsyncSession, DB_CLIENT from ..complete import llm_complete, response_to_sendable_message @@ -6,6 +7,8 @@ from ...schema.utils import asUUID from ..prompt.space_search import SpaceSearchPrompt from ..tool.space_search_tools import SPACE_SEARCH_TOOLS, SpaceSearchCtx +from ...constants import MetricTags +from ...telemetry.capture_metrics import capture_increment async def build_space_search_ctx( @@ -102,4 +105,10 @@ async def space_agent_search( break already_iterations += 1 USE_CTX.db_session = None # remove the out-dated session + asyncio.create_task( + capture_increment( + project_id=project_id, + tag=MetricTags.new_experience_agentic_search, + ) + ) return Result.resolve(USE_CTX) diff --git a/src/server/core/acontext_core/llm/tool/space_lib/insert_candidate_data_as_content.py b/src/server/core/acontext_core/llm/tool/space_lib/insert_candidate_data_as_content.py index 20613037..e91ea9cd 100644 --- a/src/server/core/acontext_core/llm/tool/space_lib/insert_candidate_data_as_content.py +++ b/src/server/core/acontext_core/llm/tool/space_lib/insert_candidate_data_as_content.py @@ -41,6 +41,7 @@ async def _insert_data_handler( ) r = await BW.write_block_to_page( ctx.db_session, + ctx.project_id, ctx.space_id, page_block.id, insert_data, diff --git a/src/server/core/acontext_core/llm/tool/space_lib/search_content.py b/src/server/core/acontext_core/llm/tool/space_lib/search_content.py index 7630fb86..e18df78c 100644 --- a/src/server/core/acontext_core/llm/tool/space_lib/search_content.py +++ b/src/server/core/acontext_core/llm/tool/space_lib/search_content.py @@ -1,17 +1,10 @@ import json -from ..base import Tool, ToolPool +from ..base import Tool from ....env import DEFAULT_CORE_CONFIG from ....schema.llm import ToolSchema -from ....schema.orm.block import BLOCK_TYPE_FOLDER, BLOCK_TYPE_PAGE -from ....schema.utils import asUUID from ....schema.result import Result -from ....schema.orm import Task -from ....schema.block.path_node import repr_path_tree from ....service.data import block_search as BS -from ....service.data import block_nav as BN from ....service.data import block_render as BR -from ....service.data import block as BD -from ....schema.session.task import TaskStatus from .ctx import SpaceCtx @@ -25,6 +18,7 @@ async def _search_content_handler( limit = llm_arguments.get("limit", 10) r = await BS.search_content_blocks( ctx.db_session, + ctx.project_id, ctx.space_id, query, topk=limit, diff --git a/src/server/core/acontext_core/llm/tool/space_search_lib/search_content.py b/src/server/core/acontext_core/llm/tool/space_search_lib/search_content.py index 82053798..f60e1849 100644 --- a/src/server/core/acontext_core/llm/tool/space_search_lib/search_content.py +++ b/src/server/core/acontext_core/llm/tool/space_search_lib/search_content.py @@ -1,17 +1,10 @@ import json -from ..base import Tool, ToolPool +from ..base import Tool from ....env import DEFAULT_CORE_CONFIG from ....schema.llm import ToolSchema -from ....schema.orm.block import BLOCK_TYPE_FOLDER, BLOCK_TYPE_PAGE -from ....schema.utils import asUUID from ....schema.result import Result -from ....schema.orm import Task -from ....schema.block.path_node import repr_path_tree from ....service.data import block_search as BS -from ....service.data import block_nav as BN from ....service.data import block_render as BR -from ....service.data import block as BD -from ....schema.session.task import TaskStatus from .ctx import SpaceSearchCtx @@ -25,6 +18,7 @@ async def _search_content_handler( limit = llm_arguments.get("limit", 10) r = await BS.search_content_blocks( ctx.db_session, + ctx.project_id, ctx.space_id, query, topk=limit, diff --git a/src/server/core/acontext_core/llm/tool/task_lib/insert.py b/src/server/core/acontext_core/llm/tool/task_lib/insert.py index 14ae4313..e596e45b 100644 --- a/src/server/core/acontext_core/llm/tool/task_lib/insert.py +++ b/src/server/core/acontext_core/llm/tool/task_lib/insert.py @@ -1,9 +1,10 @@ -from ..base import Tool, ToolPool +import asyncio +from ..base import Tool from ....schema.llm import ToolSchema from ....schema.result import Result -from ....schema.orm import Task from ....service.data import task as TD -from ....env import LOG +from ....constants import MetricTags +from ....telemetry.capture_metrics import capture_increment from .ctx import TaskCtx @@ -22,6 +23,12 @@ async def insert_task_handler(ctx: TaskCtx, llm_arguments: dict) -> Result[str]: t, eil = r.unpack() if eil: return r + asyncio.create_task( + capture_increment( + project_id=ctx.project_id, + tag=MetricTags.new_task_created, + ) + ) return Result.resolve(f"Task {t.order} created") diff --git a/src/server/core/acontext_core/service/data/block_search.py b/src/server/core/acontext_core/service/data/block_search.py index b2db356c..e832fa2d 100644 --- a/src/server/core/acontext_core/service/data/block_search.py +++ b/src/server/core/acontext_core/service/data/block_search.py @@ -1,7 +1,10 @@ +import asyncio from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from typing import List, Tuple, cast +from ...constants import MetricTags +from ...telemetry.capture_metrics import capture_increment from ...schema.orm import Block, BlockEmbedding from ...schema.orm.block import PATH_BLOCK, CONTENT_BLOCK from ...schema.utils import asUUID @@ -127,13 +130,14 @@ async def search_path_blocks( async def search_content_blocks( db_session: AsyncSession, + project_id: asUUID, space_id: asUUID, query_text: str, topk: int = 10, threshold: float = 0.8, fetch_ratio: float = 1.5, ) -> Result[List[Tuple[Block, float]]]: - return await search_blocks( + r = await search_blocks( db_session, space_id, query_text, @@ -142,3 +146,11 @@ async def search_content_blocks( threshold, fetch_ratio, ) + if r.ok(): + asyncio.create_task( + capture_increment( + project_id=project_id, + tag=MetricTags.new_experience_embedding_search, + ) + ) + return r diff --git a/src/server/core/acontext_core/service/data/block_write.py b/src/server/core/acontext_core/service/data/block_write.py index abb14f03..164b0ec1 100644 --- a/src/server/core/acontext_core/service/data/block_write.py +++ b/src/server/core/acontext_core/service/data/block_write.py @@ -1,6 +1,10 @@ +import asyncio from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from pydantic import ValidationError +from ...constants import MetricTags +from ...telemetry.capture_metrics import capture_increment from ...schema.orm.block import BLOCK_TYPE_SOP from ...schema.orm import Block, ToolReference, ToolSOP, Space from ...schema.utils import asUUID @@ -113,18 +117,31 @@ async def write_sop_block_to_parent( async def write_block_to_page( db_session: AsyncSession, + project_id: asUUID, space_id: asUUID, par_block_id: asUUID, data: GeneralBlockData, after_block_index: Optional[int] = None, -): +) -> Result[asUUID]: if data["type"] not in WRITE_BLOCK_FACTORY: return Result.reject(f"Block type {data['type']} is not supported") - block_data = BLOCK_DATA_FACTORY[data["type"]].model_validate(data["data"]) - return await WRITE_BLOCK_FACTORY[data["type"]]( + try: + block_data = BLOCK_DATA_FACTORY[data["type"]].model_validate(data["data"]) + except ValidationError as e: + return Result.reject(f"Invalid block data: {e}") + r = await WRITE_BLOCK_FACTORY[data["type"]]( db_session, space_id, par_block_id, block_data, after_block_index=after_block_index, ) + if r.ok(): + asyncio.create_task( + capture_increment( + project_id=project_id, + tag=MetricTags.new_skill_learned, + ) + ) + + return r diff --git a/src/server/core/api.py b/src/server/core/api.py index 6ac8a2dd..806d403b 100644 --- a/src/server/core/api.py +++ b/src/server/core/api.py @@ -1,11 +1,14 @@ import asyncio from contextlib import asynccontextmanager -from pydantic import ValidationError from typing import Optional, List from fastapi import FastAPI, Query, Path, Body from fastapi.exceptions import HTTPException from acontext_core.di import setup, cleanup, MQ_CLIENT, LOG, DB_CLIENT -from acontext_core.telemetry.otel import setup_otel_tracing, instrument_fastapi, shutdown_otel_tracing +from acontext_core.telemetry.otel import ( + setup_otel_tracing, + instrument_fastapi, + shutdown_otel_tracing, +) from acontext_core.telemetry.config import TelemetryConfig from acontext_core.schema.api.request import ( SearchMode, @@ -21,11 +24,7 @@ ) from acontext_core.schema.tool.tool_reference import ToolReferenceData from acontext_core.schema.utils import asUUID -from acontext_core.schema.block.sop_block import SOPData -from acontext_core.schema.orm.block import ( - BLOCK_TYPE_SOP, - PATH_BLOCK, -) +from acontext_core.schema.orm.block import PATH_BLOCK from acontext_core.env import DEFAULT_CORE_CONFIG from acontext_core.llm.agent import space_search as SS from acontext_core.service.data import block as BB @@ -57,7 +56,7 @@ except Exception as e: LOG.warning( f"Failed to setup OpenTelemetry tracing, continuing without tracing: {e}", - exc_info=True + exc_info=True, ) @@ -65,12 +64,12 @@ async def lifespan(app: FastAPI): # Startup await setup() - + # Run consumer in the background asyncio.create_task(MQ_CLIENT.start()) - + yield - + # Shutdown if tracer_provider: try: @@ -78,7 +77,7 @@ async def lifespan(app: FastAPI): LOG.info("OpenTelemetry tracing shutdown") except Exception as e: LOG.warning(f"Failed to shutdown OpenTelemetry tracing: {e}", exc_info=True) - + await cleanup() @@ -95,12 +94,16 @@ async def lifespan(app: FastAPI): except Exception as e: LOG.warning( f"Failed to instrument FastAPI, continuing without instrumentation: {e}", - exc_info=True + exc_info=True, ) async def semantic_grep_search_func( - threshold: Optional[float], space_id: asUUID, query: str, limit: int + threshold: Optional[float], + project_id: asUUID, + space_id: asUUID, + query: str, + limit: int, ) -> List[SearchResultBlockItem]: search_threshold = ( threshold @@ -113,6 +116,7 @@ async def semantic_grep_search_func( # Perform search result = await BS.search_content_blocks( db_session, + project_id, space_id, query, topk=limit, @@ -149,96 +153,6 @@ async def semantic_grep_search_func( return search_results -@app.get("/api/v1/project/{project_id}/space/{space_id}/semantic_glob") -async def semantic_glob( - project_id: asUUID = Path(..., description="Project ID to search within"), - space_id: asUUID = Path(..., description="Space ID to search within"), - query: str = Query(..., description="Search query for page/folder titles"), - limit: int = Query( - 10, ge=1, le=50, description="Maximum number of results to return" - ), - threshold: Optional[float] = Query( - None, - ge=0.0, - le=2.0, - description="Cosine distance threshold (0=identical, 2=opposite). Uses config default if not specified", - ), -) -> List[SearchResultBlockItem]: - """ - Search for pages and folders by title using semantic vector similarity. - - - **space_id**: UUID of the space to search in - - **query**: Search query text - - **limit**: Maximum number of results (1-100, default 10) - - **threshold**: Optional distance threshold (uses config default if not provided) - """ - # Use config default if threshold not specified - search_threshold = ( - threshold - if threshold is not None - else DEFAULT_CORE_CONFIG.block_embedding_search_cosine_distance_threshold - ) - - # Get database session - async with DB_CLIENT.get_session_context() as db_session: - # Perform search - result = await BS.search_path_blocks( - db_session, - space_id, - query, - topk=limit, - threshold=search_threshold, - ) - - # Check if search was successful - if not result.ok(): - LOG.error(f"Search failed: {result.error}") - raise HTTPException(status_code=500, detail=str(result.error)) - - # Format results - block_distances = result.data - search_results = [] - - for block, distance in block_distances: - item = SearchResultBlockItem( - block_id=block.id, - title=block.title, - type=block.type, - props=block.props, - distance=distance, - ) - - search_results.append(item) - - return search_results - - -@app.get("/api/v1/project/{project_id}/space/{space_id}/semantic_grep") -async def semantic_grep( - project_id: asUUID = Path(..., description="Project ID to search within"), - space_id: asUUID = Path(..., description="Space ID to search within"), - query: str = Query(..., description="Search query for content blocks"), - limit: int = Query( - 10, ge=1, le=50, description="Maximum number of results to return" - ), - threshold: Optional[float] = Query( - None, - ge=0.0, - le=2.0, - description="Cosine distance threshold (0=identical, 2=opposite). Uses config default if not specified", - ), -) -> List[SearchResultBlockItem]: - """ - Search for pages and folders by title using semantic vector similarity. - - - **space_id**: UUID of the space to search in - - **query**: Search query text - - **limit**: Maximum number of results (1-100, default 10) - - **threshold**: Optional distance threshold (uses config default if not provided) - """ - return await semantic_grep_search_func(threshold, space_id, query, limit) - - @app.get("/api/v1/project/{project_id}/space/{space_id}/experience_search") async def search_space( project_id: asUUID = Path(..., description="Project ID to search within"), @@ -263,7 +177,7 @@ async def search_space( ) -> SpaceSearchResult: if mode == "fast": cited_blocks = await semantic_grep_search_func( - semantic_threshold, space_id, query, limit + semantic_threshold, project_id, space_id, query, limit ) return SpaceSearchResult(cited_blocks=cited_blocks, final_answer=None) elif mode == "agentic": @@ -300,17 +214,18 @@ async def insert_new_block( space_id: asUUID = Path(..., description="Space ID to search within"), request: InsertBlockRequest = Body(..., description="Request to insert new block"), ) -> InsertBlockResponse: - if request.type == BLOCK_TYPE_SOP: - - try: - sop_data = SOPData.model_validate( - {**request.props, "use_when": request.title} - ) - except ValidationError as e: - raise HTTPException(status_code=500, detail=str(e)) + if request.type in BW.WRITE_BLOCK_FACTORY: + new_data = {**request.props, "use_when": request.title} async with DB_CLIENT.get_session_context() as db_session: - r = await BW.write_sop_block_to_parent( - db_session, space_id, request.parent_id, sop_data + r = await BW.write_block_to_page( + db_session, + project_id, + space_id, + request.parent_id, + { + "type": request.type, + "data": new_data, + }, ) if not r.ok(): raise HTTPException(status_code=500, detail=str(r.error)) diff --git a/src/server/core/tests/test_api.py b/src/server/core/tests/test_api.py index c1452196..63e351ab 100644 --- a/src/server/core/tests/test_api.py +++ b/src/server/core/tests/test_api.py @@ -21,18 +21,11 @@ from api import app from acontext_core.schema.orm import ( - Block, - BlockEmbedding, Project, Space, Session, Task, ) -from acontext_core.schema.orm.block import ( - BLOCK_TYPE_PAGE, - BLOCK_TYPE_TEXT, - BLOCK_TYPE_SOP, -) from acontext_core.infra.db import DatabaseClient from acontext_core.env import DEFAULT_CORE_CONFIG from acontext_core.schema.result import Result @@ -108,515 +101,6 @@ async def get_mock_embedding(texts, phase="document"): yield mock -class TestSemanticGlobEndpoint: - """Test the /api/v1/project/{project_id}/space/{space_id}/semantic_glob endpoint""" - - @pytest.mark.asyncio - async def test_semantic_glob_success(self): - """Test successful semantic search via API endpoint""" - db_client = DatabaseClient() - await db_client.create_tables() - - # Create test data in a separate session - async with db_client.get_session_context() as session: - project = Project( - secret_key_hmac="test_key_hmac", secret_key_hash_phc="test_key_hash" - ) - session.add(project) - await session.flush() - - space = Space(project_id=project.id) - session.add(space) - await session.flush() - - # Create test blocks - page1 = Block( - space_id=space.id, - type=BLOCK_TYPE_PAGE, - title="Python Programming", - props={"view_when": "Learn Python basics"}, - sort=0, - ) - session.add(page1) - await session.flush() - - page2 = Block( - space_id=space.id, - type=BLOCK_TYPE_PAGE, - title="JavaScript Tutorials", - props={"view_when": "JavaScript fundamentals"}, - sort=1, - ) - session.add(page2) - await session.flush() - - # Create embeddings - python_embedding = np.zeros(1536, dtype=np.float32) - python_embedding[0] = 0.8 - python_embedding[1] = 0.2 - - embedding1 = BlockEmbedding( - block_id=page1.id, - space_id=space.id, - block_type=page1.type, - embedding=python_embedding, - configs={"model": "test"}, - ) - session.add(embedding1) - - js_embedding = np.zeros(1536, dtype=np.float32) - js_embedding[0] = 0.1 - js_embedding[1] = 0.8 - - embedding2 = BlockEmbedding( - block_id=page2.id, - space_id=space.id, - block_type=page2.type, - embedding=js_embedding, - configs={"model": "test"}, - ) - session.add(embedding2) - - await session.commit() - - # Store IDs for later use - project_id = project.id - space_id = space.id - page1_id = page1.id - - # Now test the API endpoint with a fresh session (embedding mock is auto-applied via fixture) - # Patch the global DB_CLIENT to use our test database client - with patch("api.DB_CLIENT", db_client): - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - response = await client.get( - f"/api/v1/project/{project_id}/space/{space_id}/semantic_glob", - params={"query": "Python programming", "limit": 10}, - ) - - # Assertions - assert ( - response.status_code == 200 - ), f"Expected 200, got {response.status_code}: {response.text}" - - data = response.json() - assert isinstance(data, list), "Response should be a list" - assert len(data) > 0, "Should return at least one result" - - # Check response structure - first_result = data[0] - assert "block_id" in first_result, "Result should have block_id" - assert "distance" in first_result, "Result should have distance" - assert isinstance( - first_result["distance"], float - ), "Distance should be a float" - - # Verify the most relevant result is the Python page - assert first_result["block_id"] == str( - page1_id - ), "Python page should be most relevant" - - print(f"✓ API test passed - Found {len(data)} results") - - # Cleanup - delete the project (cascades to space, blocks, embeddings) - async with db_client.get_session_context() as session: - project = await session.get(Project, project_id) - await session.delete(project) - await session.commit() - - @pytest.mark.asyncio - async def test_semantic_glob_with_custom_threshold(self): - """Test semantic search with custom threshold parameter""" - db_client = DatabaseClient() - await db_client.create_tables() - - # Create test data in a separate session - async with db_client.get_session_context() as session: - project = Project( - secret_key_hmac="test_key_hmac", secret_key_hash_phc="test_key_hash" - ) - session.add(project) - await session.flush() - - space = Space(project_id=project.id) - session.add(space) - await session.flush() - - page = Block( - space_id=space.id, - type=BLOCK_TYPE_PAGE, - title="Test Page", - props={"view_when": "Test content"}, - sort=0, - ) - session.add(page) - await session.flush() - - # Create embedding - embedding_vector = np.random.rand(1536).astype(np.float32) - embedding = BlockEmbedding( - block_id=page.id, - space_id=space.id, - block_type=page.type, - embedding=embedding_vector, - configs={"model": "test"}, - ) - session.add(embedding) - await session.commit() - - # Store IDs for later use - project_id = project.id - space_id = space.id - - # Test with custom threshold (embedding mock is auto-applied via fixture) - # Patch the global DB_CLIENT to use our test database client - with patch("api.DB_CLIENT", db_client): - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - response = await client.get( - f"/api/v1/project/{project_id}/space/{space_id}/semantic_glob", - params={ - "query": "test query", - "limit": 5, - "threshold": 0.5, - }, - ) - - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - print("✓ Custom threshold test passed") - - # Cleanup - async with db_client.get_session_context() as session: - project = await session.get(Project, project_id) - await session.delete(project) - await session.commit() - - @pytest.mark.asyncio - async def test_semantic_glob_invalid_space_id(self): - """Test API with invalid space ID""" - valid_project_id = str(uuid4()) - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - # Test with non-UUID space_id - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/not-a-uuid/semantic_glob", - params={"query": "test"}, - ) - - # FastAPI should return 422 for invalid UUID format - assert response.status_code == 422 - print("✓ Invalid space ID test passed") - - @pytest.mark.asyncio - async def test_semantic_glob_invalid_params(self): - """Test API with invalid query parameters""" - valid_project_id = str(uuid4()) - valid_space_id = str(uuid4()) - - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - # Test without required query parameter - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_glob" - ) - assert response.status_code == 422, "Should fail without query parameter" - - # Test with limit out of range - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_glob", - params={"query": "test", "limit": 100}, # Max is 50 - ) - assert response.status_code == 422, "Should fail with limit > 50" - - # Test with negative limit - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_glob", - params={"query": "test", "limit": -1}, - ) - assert response.status_code == 422, "Should fail with negative limit" - - # Test with threshold out of range - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_glob", - params={"query": "test", "threshold": 3.0}, # Max is 2.0 - ) - assert response.status_code == 422, "Should fail with threshold > 2.0" - - print("✓ Invalid params test passed") - - -class TestSemanticGrepEndpoint: - """Test the /api/v1/project/{project_id}/space/{space_id}/semantic_grep endpoint""" - - @pytest.mark.asyncio - async def test_semantic_grep_success(self): - """Test successful semantic search for content blocks via API endpoint""" - db_client = DatabaseClient() - await db_client.create_tables() - - # Create test data in a separate session - async with db_client.get_session_context() as session: - project = Project( - secret_key_hmac="test_key_hmac", secret_key_hash_phc="test_key_hash" - ) - session.add(project) - await session.flush() - - space = Space(project_id=project.id) - session.add(space) - await session.flush() - - # Create a parent page for content blocks - page = Block( - space_id=space.id, - type=BLOCK_TYPE_PAGE, - title="Documentation", - props={}, - sort=0, - ) - session.add(page) - await session.flush() - - # Create test content blocks - text1 = Block( - space_id=space.id, - type=BLOCK_TYPE_TEXT, - parent_id=page.id, - title="Python Tutorial Content", - props={"content": "Learn Python programming basics"}, - sort=0, - ) - session.add(text1) - await session.flush() - - text2 = Block( - space_id=space.id, - type=BLOCK_TYPE_TEXT, - parent_id=page.id, - title="JavaScript Guide Content", - props={"content": "JavaScript fundamentals and best practices"}, - sort=1, - ) - session.add(text2) - await session.flush() - - # Create embeddings for content blocks - python_embedding = np.zeros(1536, dtype=np.float32) - python_embedding[0] = 0.8 - python_embedding[1] = 0.2 - - embedding1 = BlockEmbedding( - block_id=text1.id, - space_id=space.id, - block_type=text1.type, - embedding=python_embedding, - configs={"model": "test"}, - ) - session.add(embedding1) - - js_embedding = np.zeros(1536, dtype=np.float32) - js_embedding[0] = 0.1 - js_embedding[1] = 0.8 - - embedding2 = BlockEmbedding( - block_id=text2.id, - space_id=space.id, - block_type=text2.type, - embedding=js_embedding, - configs={"model": "test"}, - ) - session.add(embedding2) - - await session.commit() - - # Store IDs for later use - project_id = project.id - space_id = space.id - text1_id = text1.id - - # Now test the API endpoint (embedding mock is auto-applied via fixture) - # Patch the global DB_CLIENT to use our test database client - with patch("api.DB_CLIENT", db_client): - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - response = await client.get( - f"/api/v1/project/{project_id}/space/{space_id}/semantic_grep", - params={"query": "Python programming", "limit": 10}, - ) - - # Assertions - assert ( - response.status_code == 200 - ), f"Expected 200, got {response.status_code}: {response.text}" - - data = response.json() - assert isinstance(data, list), "Response should be a list" - assert len(data) > 0, "Should return at least one result" - - # Check response structure - first_result = data[0] - assert "block_id" in first_result, "Result should have block_id" - assert "distance" in first_result, "Result should have distance" - assert isinstance( - first_result["distance"], float - ), "Distance should be a float" - - # Verify the most relevant result is the Python text block - assert first_result["block_id"] == str( - text1_id - ), "Python content block should be most relevant" - - print(f"✓ API test passed - Found {len(data)} results") - - # Cleanup - delete the project (cascades to space, blocks, embeddings) - async with db_client.get_session_context() as session: - project = await session.get(Project, project_id) - await session.delete(project) - await session.commit() - - @pytest.mark.asyncio - async def test_semantic_grep_with_sop_blocks(self): - """Test semantic search includes SOP blocks""" - db_client = DatabaseClient() - await db_client.create_tables() - - # Create test data with SOP blocks - async with db_client.get_session_context() as session: - project = Project( - secret_key_hmac="test_key_hmac", secret_key_hash_phc="test_key_hash" - ) - session.add(project) - await session.flush() - - space = Space(project_id=project.id) - session.add(space) - await session.flush() - - # Create a parent page - page = Block( - space_id=space.id, - type=BLOCK_TYPE_PAGE, - title="Procedures", - props={}, - sort=0, - ) - session.add(page) - await session.flush() - - # Create SOP block - sop = Block( - space_id=space.id, - type=BLOCK_TYPE_SOP, - parent_id=page.id, - title="Deployment SOP", - props={"content": "Standard operating procedure for deployment"}, - sort=0, - ) - session.add(sop) - await session.flush() - - # Create embedding - sop_embedding = np.random.rand(1536).astype(np.float32) - embedding = BlockEmbedding( - block_id=sop.id, - space_id=space.id, - block_type=sop.type, - embedding=sop_embedding, - configs={"model": "test"}, - ) - session.add(embedding) - await session.commit() - - # Store IDs for later use - project_id = project.id - space_id = space.id - - # Test the API endpoint - with patch("api.DB_CLIENT", db_client): - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - response = await client.get( - f"/api/v1/project/{project_id}/space/{space_id}/semantic_grep", - params={"query": "deployment procedure", "limit": 10}, - ) - - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - print("✓ SOP block search test passed") - - # Cleanup - async with db_client.get_session_context() as session: - project = await session.get(Project, project_id) - await session.delete(project) - await session.commit() - - @pytest.mark.asyncio - async def test_semantic_grep_invalid_space_id(self): - """Test API with invalid space ID""" - valid_project_id = str(uuid4()) - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - # Test with non-UUID space_id - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/not-a-uuid/semantic_grep", - params={"query": "test"}, - ) - - # FastAPI should return 422 for invalid UUID format - assert response.status_code == 422 - print("✓ Invalid space ID test passed") - - @pytest.mark.asyncio - async def test_semantic_grep_invalid_params(self): - """Test API with invalid query parameters""" - valid_project_id = str(uuid4()) - valid_space_id = str(uuid4()) - - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as client: - # Test without required query parameter - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_grep" - ) - assert response.status_code == 422, "Should fail without query parameter" - - # Test with limit out of range - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_grep", - params={"query": "test", "limit": 100}, # Max is 50 - ) - assert response.status_code == 422, "Should fail with limit > 50" - - # Test with negative limit - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_grep", - params={"query": "test", "limit": -1}, - ) - assert response.status_code == 422, "Should fail with negative limit" - - # Test with threshold out of range - response = await client.get( - f"/api/v1/project/{valid_project_id}/space/{valid_space_id}/semantic_grep", - params={"query": "test", "threshold": 3.0}, # Max is 2.0 - ) - assert response.status_code == 422, "Should fail with threshold > 2.0" - - print("✓ Invalid params test passed") - - class TestGetLearningStatusEndpoint: """Test the /api/v1/project/{project_id}/session/{session_id}/get_learning_status endpoint"""