Skip to content

Commit

Permalink
feat: parse Tool Spec in building stage
Browse files Browse the repository at this point in the history
  • Loading branch information
wenfengwang committed Jun 16, 2024
1 parent ec690ac commit 4037be6
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 44 deletions.
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ credentials/

**/.next
**/node_modules
config
config

.dist
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ config/**.yaml

screenshots

**/tmp
**/tmp

.dist
Binary file removed dist/npiai-0.1.0-py3-none-any.whl
Binary file not shown.
Binary file removed dist/npiai-0.1.0.tar.gz
Binary file not shown.
4 changes: 2 additions & 2 deletions npiai/app/github/npi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ env:
dependencies:
- name: python
version: "^3.10"
- name: npiai
version: "0.1.1"
- name: openai
version: "^1.30.3"
- name: termcolor
version: "^2.4.0"
- name: pygithub
version: "^2.3.0"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "npiai"
version = "0.1.0"
version = "0.1.1"
description = ""
authors = ["wenfeng <w@npi.ai>"]
readme = "README.md"
Expand Down
49 changes: 25 additions & 24 deletions server/model/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ type ToolFunctionSpec struct {
Functions []Function `json:"functions" bson:"functions"`
}

type Language int
type Language string

const (
Python = Language(1)
Python = Language("python")
)

type Runtime struct {
Expand All @@ -126,17 +126,17 @@ type Dependency struct {
}

type Function struct {
Name string `json:"name" bson:"name"`
Description string `json:"description" bson:"description"`
Parameters []Parameter `json:"parameters" bson:"parameters"`
FewShots []FewShot `json:"few_shots" bson:"few_shots"`
Name string `json:"name" bson:"name"`
Description string `json:"description" bson:"description"`
Parameters Parameters `json:"parameters" bson:"parameters"`
FewShots []FewShot `json:"fewShots" bson:"few_shots"`
}

func (f *Function) Arguments(lan Language) string {
switch lan {
case Python:
args := ""
for _, para := range f.Parameters {
for _, para := range f.Parameters.Properties {
args += fmt.Sprintf("%s=event['%s'], ", para.Name, para.Name)
}
return strings.TrimRight(args, ", ")
Expand All @@ -146,15 +146,11 @@ func (f *Function) Arguments(lan Language) string {

func (f *Function) Schema() map[string]interface{} {
properties := map[string]interface{}{}
var required []string
for _, para := range f.Parameters {
for _, para := range f.Parameters.Properties {
properties[para.Name] = map[string]string{
"type": para.Type.Name(),
"description": para.Description,
}
if para.Required {
required = append(required, para.Name)
}
}

schema := map[string]interface{}{
Expand All @@ -165,7 +161,7 @@ func (f *Function) Schema() map[string]interface{} {
"parameters": map[string]interface{}{
"type": "object",
"properties": properties,
"required": required,
"required": f.Parameters.Required,
},
},
}
Expand All @@ -190,7 +186,7 @@ func (f *Function) OpenAPISchema() *v3.PathItem {

content := orderedmap.New[string, *v3.MediaType]()
properties := orderedmap.New[string, *base.SchemaProxy]()
for _, para := range f.Parameters {
for _, para := range f.Parameters.Properties {
properties.Set(para.Name, base.CreateSchemaProxy(&base.Schema{
Type: []string{para.Type.Name()},
Description: para.Description,
Expand Down Expand Up @@ -219,7 +215,7 @@ func (f *Function) OpenAPISchema() *v3.PathItem {
return item
}

type ParameterType int
type ParameterType string

func (pt ParameterType) Name() string {
switch pt {
Expand All @@ -240,20 +236,25 @@ func (pt ParameterType) Name() string {
}

const (
String = ParameterType(1)
Int = ParameterType(2)
Float = ParameterType(3)
Bool = ParameterType(4)
Map = ParameterType(5)
List = ParameterType(6)
String = ParameterType("string")
Int = ParameterType("integer")
Float = ParameterType("number")
Bool = ParameterType("bool")
Map = ParameterType("object")
List = ParameterType("array")
)

type Parameter struct {
type Parameters struct {
Description string `json:"description" bson:"description"`
Type ParameterType `json:"type" bson:"type"`
Required []string `json:"required" bson:"required"`
Properties map[string]Property `json:"properties" bson:"properties"`
}

type Property struct {
Name string `json:"name" bson:"name"`
Description string `json:"description" bson:"description"`
Type ParameterType `json:"type" bson:"type"`
Required bool `json:"required" bson:"required"`
Default interface{} `json:"default" bson:"default"`
}

type FewShot struct {
Expand Down
94 changes: 91 additions & 3 deletions server/reconcile/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ package reconcile

import (
"context"
"encoding/json"
"errors"
"github.com/mattn/go-isatty"
"github.com/npi-ai/npi/server/api"
"github.com/npi-ai/npi/server/db"
"github.com/npi-ai/npi/server/log"
"github.com/npi-ai/npi/server/model"
"github.com/npi-ai/npi/server/service"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"os"
"os/exec"
"strings"
"time"
)

Expand Down Expand Up @@ -81,16 +88,63 @@ func (tc *toolReconciler) Start(ctx context.Context) error {
return nil
}

func (tc *toolReconciler) BuildingHandler(ctx context.Context, ws model.ToolInstance) error {
imageURI, err := tc.builderSvc.Build(ctx, ws.Metadata)
func (tc *toolReconciler) BuildingHandler(ctx context.Context, toolInstance model.ToolInstance) error {
imageURI, err := tc.builderSvc.Build(ctx, toolInstance.Metadata)
if err != nil {
return err
}

args := []string{"run", "--rm", "--entrypoint", "poetry", imageURI, "run", "python", "main.py", "spec"}
// get tool spec
if isatty.IsTerminal(os.Stdin.Fd()) {
//args = append([]string{"-it"}, args...)
}
cmd := exec.Command("docker", args...)
output, err := cmd.CombinedOutput()
if err != nil {
log.Warn().Str("output", string(output)).Msg("Failed to build docker image")
return api.ErrInternal.
WithMessage("Failed to build docker image").WithError(err)
}

strings.Split(string(output), "\n")
success := false
var md model.ToolMetadata
var spec model.ToolFunctionSpec
for _, line := range strings.Split(string(output), "\n") {
if strings.HasPrefix(line, "{\"kind\"") {
md, spec, err = parseToolSpec([]byte(line))
if err != nil {
return api.ErrInternal.
WithMessage("Failed to parse tool spec").WithError(err)
}

success = true
break
}
}
if !success {
return api.ErrInternal.WithMessage("Failed to get tool spec")
}

toolInstance.Metadata.Name = md.Name
toolInstance.Metadata.Description = md.Description
toolInstance.FunctionSpec = spec
// upload image
log.Info().Str("image", imageURI).Msg("Uploading docker image")
cmd = exec.Command("docker", "push", imageURI)
if output, err = cmd.CombinedOutput(); err != nil {
log.Warn().Str("output", string(output)).Msg("Failed to upload docker image")
return api.ErrInternal.
WithMessage("Failed to upload docker image").WithError(err)
}
// update tool instance
_, err = tc.coll.UpdateOne(ctx,
bson.M{"_id": ws.ID},
bson.M{"_id": toolInstance.ID},
bson.M{
"$set": bson.M{
"metadata": toolInstance.Metadata,
"spec": toolInstance.FunctionSpec,
"image": imageURI,
"updated_at": time.Now(),
},
Expand All @@ -112,3 +166,37 @@ func (tc *toolReconciler) PausingHandler(ctx context.Context, ws model.ToolInsta
println("pausing")
return nil
}

func parseToolSpec(data []byte) (model.ToolMetadata, model.ToolFunctionSpec, error) {
m := map[string]interface{}{}
md := model.ToolMetadata{}
spec := model.ToolFunctionSpec{}
if err := json.Unmarshal(data, &m); err != nil {
return md, spec, err
}
if v, ok := m["metadata"]; ok {
mdMap := v.(map[string]interface{})
value, exist := mdMap["name"]
if !exist {
return md, spec, errors.New("metadata.name not found")
}
md.Name = value.(string)

value, exist = mdMap["description"]
if !exist {
return md, spec, errors.New("metadata.description not found")
}
md.Description = value.(string)
}
if v, ok := m["spec"]; ok {
d, err := json.Marshal(v)
if err != nil {
return md, spec, errors.New("failed to parse function spec")
}
if err = json.Unmarshal(d, &spec); err != nil {
return md, spec, err
}
// TODO merge dependencies
}
return md, spec, nil
}
1 change: 1 addition & 0 deletions server/reconcile/tool.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kind": "Function", "metadata": {"name": "github", "description": "Manage GitHub issues and pull requests", "provider": "private"}, "spec": {"runtime": {"language": "python", "version": "3.11"}, "dependencies": [{"name": "npiai", "version": "0.1.0"}], "functions": [{"description": "Add a comment to the target issue.", "name": "add_issue_comment", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "number": {"description": "Issue number", "type": "integer"}, "body": {"description": "Body of the comment in markdown format", "type": "string"}}, "required": ["repo", "number", "body"], "type": "object"}, "fewShots": null}, {"description": "Add a comment to the target pull request.", "name": "add_pull_request_comment", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "number": {"description": "Pull request number", "type": "integer"}, "body": {"description": "Body of the comment in markdown format", "type": "string"}}, "required": ["repo", "number", "body"], "type": "object"}, "fewShots": null}, {"description": "Create an issue under the given repository.", "name": "create_issue", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "title": {"description": "Title of the issue", "type": "string"}, "body": {"description": "Body of the issue in markdown format", "type": "string"}, "labels": {"default": null, "description": "List of labels to add to the issue", "items": {"type": "string"}, "type": "array"}, "assignees": {"default": null, "description": "List of users to assign to the issue", "items": {"type": "string"}, "type": "array"}}, "required": ["repo", "title", "body"], "type": "object"}, "fewShots": null}, {"description": "Create a pull request under the given repository.", "name": "create_pull_request", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "title": {"description": "Title of the pull request", "type": "string"}, "body": {"description": "Body of the pull request", "type": "string"}, "base": {"description": "Base branch of the pull request, i.e., the branch to merge the pull request into", "type": "string"}, "head": {"description": "Head branch of the pull request, i.e., the branch with your changes", "type": "string"}, "is_draft": {"default": false, "description": "Whether the pull request is a draft or not", "type": "boolean"}, "labels": {"default": null, "description": "List of labels to add to the pull request", "items": {"type": "string"}, "type": "array"}, "assignees": {"default": null, "description": "List of users to assign to the pull request", "items": {"type": "string"}, "type": "array"}}, "required": ["repo", "title", "body", "base", "head"], "type": "object"}, "fewShots": null}, {"description": "Edit an existing issue. You can also close or reopen an issue by specifying the state parameter.", "name": "edit_issue", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "number": {"description": "Issue number", "type": "integer"}, "title": {"default": null, "description": "Title of the issue", "type": "string"}, "body": {"default": null, "description": "Body of the issue in markdown format", "type": "string"}, "labels": {"default": null, "description": "List of labels to add to the issue", "items": {"type": "string"}, "type": "array"}, "assignees": {"default": null, "description": "List of users to assign to the issue", "items": {"type": "string"}, "type": "array"}, "state": {"default": null, "description": "Whether the issue is open or closed", "enum": ["open", "closed"], "type": "string"}}, "required": ["repo", "number"], "type": "object"}, "fewShots": null}, {"description": "Edit an existing pull request. You can also close or reopen a pull request by specifying the state parameter.", "name": "edit_pull_request", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "number": {"description": "Pull request number", "type": "integer"}, "title": {"default": null, "description": "Title of the pull request", "type": "string"}, "body": {"default": null, "description": "Body of the pull request in markdown format", "type": "string"}, "base": {"default": null, "description": "Base branch of the pull request, i.e., the branch with your changes", "type": "string"}, "labels": {"default": null, "description": "List of labels to add to the pull request", "items": {"type": "string"}, "type": "array"}, "assignees": {"default": null, "description": "List of users to assign to the pull request", "items": {"type": "string"}, "type": "array"}, "state": {"default": null, "description": "Whether the pull request is open or closed", "enum": ["open", "closed"], "type": "string"}}, "required": ["repo", "number"], "type": "object"}, "fewShots": null}, {"description": "Fork a repository on GitHub.", "name": "fork", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}}, "required": ["repo"], "type": "object"}, "fewShots": null}, {"description": "Get an issue from the given repository.", "name": "get_issue", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "number": {"description": "Issue number", "type": "integer"}}, "required": ["repo", "number"], "type": "object"}, "fewShots": null}, {"description": "Get a pull request from the given repository.", "name": "get_pull_request", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}, "number": {"description": "Pull request number", "type": "integer"}}, "required": ["repo", "number"], "type": "object"}, "fewShots": null}, {"description": "Search for issues or pull requests on GitHub.", "name": "search_issue_pr", "parameters": {"properties": {"query": {"description": "Search query to search for GitHub issues and pull requests. Below are some query examples:\n1. Search for all issues containing \"performance\" in the repository npi/npi: `is:issue repo:npi/npi performance`\n2. Search for open pull requests in repository npi/npi: `is:pr is:open repo:npi/npi`", "type": "string"}, "max_results": {"description": "Maximum number of results to return", "type": "integer"}}, "required": ["query", "max_results"], "type": "object"}, "fewShots": null}, {"description": "Search for repositories on GitHub.", "name": "search_repositories", "parameters": {"properties": {"query": {"description": "Search query to search for GitHub repositories. Below are some query examples:\n1. Search for all repositories that contain \"test\" and use Python: `test language:python`\n2. Search for repositories with more than 1000 stars: `stars:>1000`", "type": "string"}, "max_results": {"description": "Maximum number of results to return", "type": "integer"}}, "required": ["query", "max_results"], "type": "object"}, "fewShots": null}, {"description": "Star a repository on GitHub.", "name": "star", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}}, "required": ["repo"], "type": "object"}, "fewShots": null}, {"description": "Subscribe to notifications (a.k.a. watch) for activity in a repository on GitHub.", "name": "watch", "parameters": {"properties": {"repo": {"description": "Name of the repository in format {owner}/{repo}", "type": "string"}}, "required": ["repo"], "type": "object"}, "fewShots": null}]}}
17 changes: 17 additions & 0 deletions server/reconcile/tool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package reconcile

import (
"os"
"testing"
)

func Test_ParseToolSpec(t *testing.T) {
data, err := os.ReadFile("tool.spec.json")
if err != nil {
t.Fatal(err)
}
_, _, err = parseToolSpec(data)
if err != nil {
t.Fatal(err)
}
}
20 changes: 16 additions & 4 deletions server/scripts/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import sys
import importlib
import time
import asyncio
import json


def print_tool_spec():
# Add the directory containing the file to sys.path
module_dir, module_name = os.path.split('/npiai/tools/app.py')
module_dir, module_name = os.path.split('%s')
module_name = os.path.splitext(module_name)[0] # Remove the .py extension

if module_dir not in sys.path:
Expand All @@ -15,9 +17,9 @@ def print_tool_spec():
# Now you can import the module using its name
module = importlib.import_module(module_name)
# Create an instance of the class
tool_class = getattr(module, 'GitHub')
tool_class = getattr(module, '%s')
instance = tool_class()
print(instance.export())
print(json.dumps(instance.export()))


async def main():
Expand All @@ -39,4 +41,14 @@ async def main():
time.sleep(1000)

if __name__ == '__main__':
print_tool_spec()
if len(sys.argv) > 1:
if sys.argv[1] == 'spec':
print_tool_spec()
elif sys.argv[1] == 'server':
asyncio.run(main())
else:
print('Usage: python entrypoint.py spec|server')
sys.exit(1)
else:
print('Usage: python entrypoint.py spec|server')
sys.exit(1)
Loading

0 comments on commit 4037be6

Please sign in to comment.