Repository: bentoml/BentoML

Issue:
bug: OpenAPI Schema components from mounted ASGI apps are not being included 
### Describe the bug

If a mounted ASGI app has an OpenAPI spec that defines schema components, these are not included in the bento's generated OpenAPI spec.

The following service file reproduces the issue:

```python
import bentoml
import pydantic
from fastapi import FastAPI

svc = bentoml.Service(name="test", runners=[])

fastapi_app = FastAPI()
svc.mount_asgi_app(fastapi_app)


class TestSchema(pydantic.BaseModel):
    text_field: str


@fastapi_app.get("/metadata")
def metadata() -> TestSchema:
    return TestSchema(text_field="Hello world")

```

If I serve this bento and navigate to the OpenAPI docs, the following error is raised: 
```
Could not resolve reference: Could not resolve pointer: /components/schemas/TestSchema does not exist in document
```

This is happening because the OpenAPI path components are being pulled in from the mounted app, but the schema component (the TestSchema class in this case) are not being pulled in. I've got a fix ready for this and will open a PR shortly

### To reproduce

Requires fastapi and pydantic:
`pip install fastapi pydantic`

This service file reproduces the issue:
```python
import bentoml
import pydantic
from fastapi import FastAPI

svc = bentoml.Service(name="test", runners=[])

fastapi_app = FastAPI()
svc.mount_asgi_app(fastapi_app)


class TestSchema(pydantic.BaseModel):
    text_field: str


@fastapi_app.get("/metadata")
def metadata() -> TestSchema:
    return TestSchema(text_field="Hello world")

```

`bentoml serve service.py:svc`

### Expected behavior

The FastAPI app's schema definitions should be included in the generated OpenAPI spec.

### Environment

#### Environment variable

```bash
BENTOML_DEBUG=''
BENTOML_QUIET=''
BENTOML_BUNDLE_LOCAL_BUILD=''
BENTOML_DO_NOT_TRACK=''
BENTOML_CONFIG=''
BENTOML_CONFIG_OPTIONS=''
BENTOML_PORT=''
BENTOML_HOST=''
BENTOML_API_WORKERS=''
```

#### System information

`bentoml`: 1.1.6
`python`: 3.10.12
`platform`: Linux-6.2.0-33-generic-x86_64-with-glibc2.35
`uid_gid`: 1000:1000
<details><summary><code>pip_packages</code></summary>

<br>

```
aiohttp==3.8.5
aiosignal==1.3.1
annotated-types==0.5.0
anyio==3.7.1
appdirs==1.4.4
asgiref==3.7.2
async-timeout==4.0.3
attrs==23.1.0
bentoml==1.1.6
build==1.0.3
cattrs==23.1.2
certifi==2023.7.22
charset-normalizer==3.2.0
circus==0.18.0
click==8.1.7
click-option-group==0.5.6
cloudpickle==2.2.1
contextlib2==21.6.0
deepmerge==1.1.0
Deprecated==1.2.14
exceptiongroup==1.1.3
fastapi==0.103.1
frozenlist==1.4.0
fs==2.4.16
h11==0.14.0
idna==3.4
importlib-metadata==6.0.1
inflection==0.5.1
Jinja2==3.1.2
jsonschema==4.19.1
jsonschema-specifications==2023.7.1
markdown-it-py==3.0.0
MarkupSafe==2.1.3
mdurl==0.1.2
multidict==6.0.4
numpy==1.26.0
openapi==1.1.0
opentelemetry-api==1.18.0
opentelemetry-instrumentation==0.39b0
opentelemetry-instrumentation-aiohttp-client==0.39b0
opentelemetry-instrumentation-asgi==0.39b0
opentelemetry-sdk==1.18.0
opentelemetry-semantic-conventions==0.39b0
opentelemetry-util-http==0.39b0
packaging==23.1
pathspec==0.11.2
pip-requirements-parser==32.0.1
pip-tools==7.3.0
prometheus-client==0.17.1
psutil==5.9.5
pydantic==2.4.1
pydantic_core==2.10.1
Pygments==2.16.1
pynvml==11.5.0
pyparsing==3.1.1
pyproject_hooks==1.0.0
python-dateutil==2.8.2
python-json-logger==2.0.7
python-multipart==0.0.6
PyYAML==6.0.1
pyzmq==25.1.1
referencing==0.30.2
requests==2.31.0
rich==13.5.3
rpds-py==0.10.3
schema==0.7.5
simple-di==0.1.5
six==1.16.0
sniffio==1.3.0
starlette==0.27.0
tomli==2.0.1
tornado==6.3.3
typing_extensions==4.8.0
urllib3==2.0.5
uvicorn==0.23.2
watchfiles==0.20.0
wrapt==1.15.0
yarl==1.9.2
zipp==3.17.0
```

</details>




Discussion:

Resolving patch:
diff --git a/src/bentoml/_internal/service/openapi/__init__.py b/src/bentoml/_internal/service/openapi/__init__.py
--- a/src/bentoml/_internal/service/openapi/__init__.py
+++ b/src/bentoml/_internal/service/openapi/__init__.py
@@ -13,7 +13,6 @@
 
 from ...types import LazyType
 from ...utils import bentoml_cattr
-from .specification import Components
 from .specification import Contact
 from .specification import Info
 from .specification import MediaType
@@ -78,7 +77,7 @@ def make_infra_endpoints() -> dict[str, PathItem]:
     }
 
 
-def generate_service_components(svc: Service) -> Components:
+def generate_service_components(svc: Service) -> dict[str, t.Any]:
     components: dict[str, t.Any] = {}
     for api in svc.apis.values():
         api_components = {}
@@ -92,14 +91,13 @@ def generate_service_components(svc: Service) -> Components:
         merger.merge(components, api_components)
 
     # merge exception at last
-    merger.merge(components, {"schemas": exception_components_schema()})
-
-    return Components(**components)
+    return merger.merge(components, {"schemas": exception_components_schema()})
 
 
 def generate_spec(svc: Service, *, openapi_version: str = "3.0.2"):
     """Generate a OpenAPI specification for a service."""
     mounted_app_paths = {}
+    schema_components = {}
 
     for app, _, _ in svc.mount_apps:
         if LazyType["fastapi.FastAPI"]("fastapi.FastAPI").isinstance(app):
@@ -118,10 +116,15 @@ def generate_spec(svc: Service, *, openapi_version: str = "3.0.2"):
                 }
             )
 
+            if "components" in openapi:
+                merger.merge(schema_components, openapi["components"])
+
+    merger.merge(schema_components, generate_service_components(svc))
+
     return OpenAPISpecification(
         openapi=openapi_version,
         tags=[APP_TAG, INFRA_TAG],
-        components=generate_service_components(svc),
+        components=schema_components,
         info=Info(
             title=svc.name,
             description=svc.doc,


Test patch: