diff --git a/main.py b/main.py index 051bf04..16aefad 100644 --- a/main.py +++ b/main.py @@ -48,15 +48,16 @@ def get_all_executor_files_with_content(): return file_name_to_content -def main( +async def main( executor_name, - input_executor_description, + executor_description, input_modality, input_doc_field, output_modality, output_doc_field, - input_test_in, - input_test_out + test_in, + test_out, + do_validation=True ): recreate_folder(EXECUTOR_FOLDER) system_definition = ( @@ -69,9 +70,9 @@ def main( user_query = ( general_guidelines() - + executor_file_task(executor_name, input_executor_description, input_modality, input_doc_field, + + executor_file_task(executor_name, executor_description, input_modality, input_doc_field, output_modality, output_doc_field) - + test_executor_file_task(executor_name, input_test_in, input_test_out) + + test_executor_file_task(executor_name, test_in, test_out) + requirements_file_task() + docker_file_task() + client_file_task() @@ -85,9 +86,9 @@ def main( jina_cloud.push_executor() - host = jina_cloud.deploy_flow(executor_name) + host = await jina_cloud.deploy_flow(executor_name, do_validation) - run_client_file(f'executor/{CLIENT_FILE_NAME}', host) + run_client_file(f'executor/{CLIENT_FILE_NAME}', host, do_validation) return get_all_executor_files_with_content() @@ -95,11 +96,11 @@ def main( if __name__ == '__main__': main( executor_name='MyCoolOcrExecutor', - input_executor_description="OCR detector", + executor_description="OCR detector", input_modality='image', input_doc_field='uri', output_modality='text', output_doc_field='text', - input_test_in='https://miro.medium.com/v2/resize:fit:1024/0*4ty0Adbdg4dsVBo3.png', - input_test_out='> Hello, world!_', + test_in='https://miro.medium.com/v2/resize:fit:1024/0*4ty0Adbdg4dsVBo3.png', + test_out='> Hello, world!_', ) diff --git a/microchain-frontend/src/App.js b/microchain-frontend/src/App.js new file mode 100644 index 0000000..07ddebe --- /dev/null +++ b/microchain-frontend/src/App.js @@ -0,0 +1,183 @@ +import React, {useState} from 'react'; +import axios from 'axios'; +import {Box, Button, Container, FormControl, InputLabel, MenuItem, Select, TextField, Typography,} from '@mui/material'; + +function App() { + const [executorName, setExecutorName] = useState('MyCoolOcrExecutor'); + const [executorDescription, setExecutorDescription] = useState('OCR detector'); + const [inputModality, setInputModality] = useState('image'); + const [inputDocField, setInputDocField] = useState('uri'); + const [outputModality, setOutputModality] = useState('text'); + const [outputDocField, setOutputDocField] = useState('text'); + const [testIn, settestIn] = useState('https://miro.medium.com/v2/resize:fit:1024/0*4ty0Adbdg4dsVBo3.png'); + const [testOut, settestOut] = useState('> Hello, world!_'); + const [responseText, setResponseText] = useState(null); + + + const handleSubmit = async (e) => { + e.preventDefault(); + + const requestBody = { + executor_name: executorName, + executor_description: executorDescription, + input_modality: inputModality, + input_doc_field: inputDocField, + output_modality: outputModality, + output_doc_field: outputDocField, + test_in: testIn, + test_out: testOut, + }; + + try { + const response = await axios.post('http://0.0.0.0:8000/create', requestBody); + setResponseText(response.data); + } catch (error) { + console.error(error); + setResponseText('An error occurred while processing the request.'); + } + }; + + + return ( + + + + MicroChain + + + ✨ Magically create your microservice just by describing it. + +
+ + setExecutorName(e.target.value)} + fullWidth + /> + + + setExecutorDescription(e.target.value)} + fullWidth + /> + + + + Input Interface + + + Input Modality + + + + + + Input Doc Field + + + + + + Output Interface + + + Output Modality + + + + + + Output Doc Field + + + + + + Test Parameters + + settestIn(e.target.value)} + fullWidth + /> + + + settestOut(e.target.value)} + fullWidth + /> + + + + +
+ {responseText && ( + + + Response + + {Object.entries(responseText.result).map(([fileName, fileContent]) => ( + + + {fileName} + +
{fileContent}
+
+ ))} +
+ )} + +
+
+ ); +} + +export default App; diff --git a/microchain-frontend/src/Graph.js b/microchain-frontend/src/Graph.js deleted file mode 100644 index 40dae55..0000000 --- a/microchain-frontend/src/Graph.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useState } from 'react'; -import Microservice from './Microservice'; -import MicroserviceModal from './MicroserviceModal'; -import { Button, Grid } from '@material-ui/core'; - -const Graph = ({ onExport }) => { - const [microservices, setMicroservices] = useState([]); - const [selectedMicroservice, setSelectedMicroservice] = useState(null); - const [showModal, setShowModal] = useState(false); - - const handleSave = (microservice) => { - if (selectedMicroservice) { - setMicroservices( - microservices.map((ms) => - ms.id === selectedMicroservice.id ? microservice : ms - ) - ); - } else { - setMicroservices([...microservices, { ...microservice, id: Date.now() }]); - } - setSelectedMicroservice(null); - }; - - const handleEdit = (microservice) => { - setSelectedMicroservice(microservice); - setShowModal(true); - }; - - return ( -
- - - - {microservices.map((microservice) => ( - -
handleEdit(microservice)}> - -
-
- ))} -
- setShowModal(false)} - onSave={handleSave} - microservice={selectedMicroservice} - /> -
- ); -}; - -export default Graph; diff --git a/microchain-frontend/src/Microservice.js b/microchain-frontend/src/Microservice.js deleted file mode 100644 index 29d5dd3..0000000 --- a/microchain-frontend/src/Microservice.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { Card, CardContent, Typography } from '@material-ui/core'; - -const Microservice = ({ microservice }) => { - return ( - - - {microservice.name} - - Input: {microservice.input} | Output: {microservice.output} - - - - ); -}; - -export default Microservice; \ No newline at end of file diff --git a/microchain-frontend/src/MicroserviceModal.js b/microchain-frontend/src/MicroserviceModal.js deleted file mode 100644 index 10ba255..0000000 --- a/microchain-frontend/src/MicroserviceModal.js +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from 'react'; -import { - Dialog, - DialogTitle, - DialogContent, - TextField, - DialogActions, - Button, - FormControl, - InputLabel, - Select, - MenuItem -} from '@material-ui/core'; - -const modalities = ['image', 'audio', 'text', 'video', '3d']; - -const MicroserviceModal = ({ open, onClose, onSave, microservice }) => { - const [name, setName] = useState(microservice ? microservice.name : ''); - const [input, setInput] = useState(microservice ? microservice.input : ''); - const [output, setOutput] = useState(microservice ? microservice.output : ''); - - const handleSubmit = () => { - onSave({ name, input, output }); - setName(''); - setInput(''); - setOutput(''); - onClose(); - }; - - return ( - - {microservice ? 'Edit' : 'Add'} Microservice - - setName(e.target.value)} - /> - - Input - - - - Output - - - - - - - - - ); -}; - -export default MicroserviceModal; diff --git a/microchain-frontend/src/components/CreateExecutorForm.js b/microchain-frontend/src/components/CreateExecutorForm.js new file mode 100644 index 0000000..9fff6b5 --- /dev/null +++ b/microchain-frontend/src/components/CreateExecutorForm.js @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; + +function CreateExecutorForm({ onCreateExecutor }) { + const [formData, setFormData] = useState({ + executor_name: '', + executor_description: '', + input_modality: '', + input_doc_field: '', + output_modality: '', + output_doc_field: '', + test_in: '', + test_out: '', + }); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + onCreateExecutor(formData); + }; + + return ( +
+ + + + + + + + + +
+ ); +} + +export default CreateExecutorForm diff --git a/microchain-frontend/src/components/ExecutorOutput.js b/microchain-frontend/src/components/ExecutorOutput.js new file mode 100644 index 0000000..a8d1d48 --- /dev/null +++ b/microchain-frontend/src/components/ExecutorOutput.js @@ -0,0 +1,30 @@ +import React from 'react'; + +function ExecutorOutput({ response }) { + const { result, success, message } = response; + + return ( +
+

Generated Executor Files

+ {success ? ( +
+ {Object.entries(result).map(([filename, content]) => ( +
+

{filename}

+
+                {content}
+              
+
+ ))} +
+ ) : ( +
+

Error

+

{message}

+
+ )} +
+ ); +} + +export default ExecutorOutput; diff --git a/requirements.txt b/requirements.txt index 692df68..f9f8362 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ jina[perf]==3.14.2.dev18 openai ptest -jcloud \ No newline at end of file +jcloud +uvicorn \ No newline at end of file diff --git a/server.py b/server.py index f58c933..69fa3c2 100644 --- a/server.py +++ b/server.py @@ -1,7 +1,12 @@ from fastapi import FastAPI +from fastapi.exceptions import RequestValidationError from pydantic import BaseModel, HttpUrl from typing import Optional, Dict +from starlette.middleware.cors import CORSMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse + from main import main app = FastAPI() @@ -9,13 +14,13 @@ app = FastAPI() # Define the request model class CreateRequest(BaseModel): executor_name: str - input_executor_description: str + executor_description: str input_modality: str input_doc_field: str output_modality: str output_doc_field: str - input_test_in: HttpUrl - input_test_out: str + test_in: str + test_out: str # Define the response model class CreateResponse(BaseModel): @@ -25,17 +30,38 @@ class CreateResponse(BaseModel): @app.post("/create", response_model=CreateResponse) async def create_endpoint(request: CreateRequest): - try: - result = main( - executor_name=request.executor_name, - input_executor_description=request.input_executor_description, - input_modality=request.input_modality, - input_doc_field=request.input_doc_field, - output_modality=request.output_modality, - output_doc_field=request.output_doc_field, - input_test_in=request.input_test_in, - input_test_out=request.input_test_out, - ) - return CreateResponse(result=result, success=True, message=None) - except Exception as e: - return CreateResponse(result=None, success=False, message=str(e)) \ No newline at end of file + + result = await main( + executor_name=request.executor_name, + executor_description=request.executor_description, + input_modality=request.input_modality, + input_doc_field=request.input_doc_field, + output_modality=request.output_modality, + output_doc_field=request.output_doc_field, + test_in=request.test_in, + test_out=request.test_out, + do_validation=False + ) + return CreateResponse(result=result, success=True, message=None) + + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Add a custom exception handler for RequestValidationError +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + return JSONResponse( + status_code=422, + content={"detail": exc.errors()}, + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run("server:app", host="0.0.0.0", port=8000, log_level="info") diff --git a/src/jina_cloud.py b/src/jina_cloud.py index 4eafc8e..a70cfb7 100644 --- a/src/jina_cloud.py +++ b/src/jina_cloud.py @@ -1,3 +1,4 @@ +import asyncio import os from multiprocessing.connection import Client @@ -18,7 +19,15 @@ def get_user_name(): return response['data']['name'] -def deploy_flow(executor_name): +async def deploy_on_jcloud(flow_yaml): + cloud_flow = CloudFlow(path=flow_yaml) + await cloud_flow.__aenter__() + return cloud_flow.endpoints['gateway'] + + + + +async def deploy_flow(executor_name, do_validation): flow = f''' jtype: Flow with: @@ -44,12 +53,14 @@ executors: with open(full_flow_path, 'w') as f: f.write(flow) - # try local first - flow = Flow.load_config(full_flow_path) - with flow: - pass + if do_validation: + print('try local execution') + flow = Flow.load_config(full_flow_path) + with flow: + pass + print('deploy flow on jcloud') + return await deploy_on_jcloud(flow_yaml=full_flow_path) - return CloudFlow(path=full_flow_path).__enter__().endpoints['gateway'] def replace_client_line(file_content: str, replacement: str) -> str: lines = file_content.split('\n') @@ -59,7 +70,7 @@ def replace_client_line(file_content: str, replacement: str) -> str: break return '\n'.join(lines) -def run_client_file(file_path, host): +def run_client_file(file_path, host, do_validation): with open(file_path, 'r') as file: content = file.read() @@ -69,4 +80,5 @@ def run_client_file(file_path, host): with open(file_path, 'w') as file: file.write(replaced_content) - import executor.client # runs the client script for validation + if do_validation: + import executor.client # runs the client script for validation diff --git a/src/prompt_examples.py b/src/prompt_examples.py index b596ab8..0177d13 100644 --- a/src/prompt_examples.py +++ b/src/prompt_examples.py @@ -39,7 +39,7 @@ message DocumentProto { string text = 4; } - // a uri of the document could be: a local file path, a remote url starts with http or https or data URI scheme + // a uri of the document is a remote url starts with http or https or data URI scheme string uri = 5; // list of the sub-documents of this document (recursive structure) @@ -65,11 +65,12 @@ d1 = Document(text='hello') d2 = Document(blob=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x03L\\x00\\x00\\x01\\x18\\x08\\x06\\x00\\x00\\x00o...') d3 = Document(tensor=numpy.array([1, 2, 3]), chunks=[Document(uri=/local/path/to/file)] d4 = Document( - uri='https://docs.docarray.org', + uri='https://docs.docarray.org/img/logo.png', tags={'foo': 'bar'}, ) d5 = Document() d5.tensor = np.ones((2,4)) +d5.uri = 'https://audio.com/audio.mp3' d6 = Document() d6.blob = b'RIFF\\x00\\x00\\x00\\x00WAVEfmt \\x10\\x00...' docs = DocumentArray([ diff --git a/src/prompt_tasks.py b/src/prompt_tasks.py index 0147854..4ca4633 100644 --- a/src/prompt_tasks.py +++ b/src/prompt_tasks.py @@ -7,7 +7,9 @@ def general_guidelines(): "General guidelines: " "The code you write is production ready. " "Every file starts with comments describing what the code is doing before the first import. " - "Then all imports are listed. It is important to import all modules that could be needed in the executor code." + "Then all imports are listed. " + "It is important to import all modules that could be needed in the executor code. " + "Always import BytesIO from io. " "Comments can only be written between tags. " "Start from top-level and then fully implement all methods. " "\n" @@ -18,13 +20,14 @@ def _task(task, tag_name, file_name): return task + f"The code will go into {file_name}. Wrap the code in the string $$$start_{tag_name}$$$...$$$end_{tag_name}$$$ \n\n" -def executor_file_task(executor_name, input_executor_description, input_modality, input_doc_field, +def executor_file_task(executor_name, executor_description, input_modality, input_doc_field, output_modality, output_doc_field): return _task( f"Write the executor called '{executor_name}'. " - f"It matches the following description: '{input_executor_description}'. " + f"It matches the following description: '{executor_description}'. " f"It gets a DocumentArray as input where each document has the input modality '{input_modality}' that is stored in document.{input_doc_field}. " - f"It returns a DocumentArray as output where each document has the output modality '{output_modality}' that is stored in document.{output_doc_field}. ", + f"It returns a DocumentArray as output where each document has the output modality '{output_modality}' that is stored in document.{output_doc_field}. " + f"Have in mind that d.uri is never a path to a local file. It is always a url.", 'executor', EXECUTOR_FILE_NAME ) @@ -37,13 +40,13 @@ def requirements_file_task(): REQUIREMENTS_FILE_NAME) -def test_executor_file_task(executor_name, input_test_in, input_test_out): +def test_executor_file_task(executor_name, test_in, test_out): return _task( "Write a small unit test for the executor. " "Start the test with an extensive comment about the test case. " + ( - "Test that the executor converts the input '" + input_test_in + "' to the output '" + input_test_out + "'. " - ) if input_test_in and input_test_out else "" + "Test that the executor converts the input '" + test_in + "' to the output '" + test_out + "'. " + ) if test_in and test_out else "" "Use the following import to import the executor: " f"from executor import {executor_name} ", 'test_executor',