mirror of
https://github.com/langchain-ai/langserve.git
synced 2026-07-01 20:14:01 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fcfc2ba2f5 | |||
| 7eb71dbd5e | |||
| cb6e6425c9 | |||
| 6eeb274875 | |||
| 779b8e1cc9 | |||
| ada5d103b4 | |||
| fb0f9a0dc3 |
@@ -0,0 +1,51 @@
|
||||
# Contributing
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
We are grateful to the contributors who help evolve LangServe and dedicate their time to the project. As the primary sponsor of LangServe, LangChain, Inc. aims to build products in the open that benefit thousands of developers while allowing us to build a sustainable business. For all code contributions to LangServe, we ask that contributors complete and sign a Contributor License Agreement (“CLA”). The agreement between contributors and the project is explicit, so LangServe users can be confident in the legal status of the source code and their right to use it.The CLA does not change the terms of the underlying license, LangServe License, used by our software.
|
||||
|
||||
Before you can contribute to LangServe, a bot will comment on the PR asking you to agree to the CLA if you haven't already. Agreeing to the CLA is required before code can be merged and only needs to happen on the first contribution to the project. All subsequent contributions will fall under the same CLA.
|
||||
|
||||
## 🗺️ Guidelines
|
||||
|
||||
### Dependency Management: Poetry and other env/dependency managers
|
||||
|
||||
This project uses [Poetry](https://python-poetry.org/) v1.6.1+ as a dependency manager.
|
||||
|
||||
### Local Development Dependencies
|
||||
|
||||
Install langserve development requirements (for running langchain, running examples, linting, formatting, tests, and coverage):
|
||||
|
||||
```sh
|
||||
poetry install --with test,dev
|
||||
```
|
||||
|
||||
Then verify that tests pass:
|
||||
|
||||
```sh
|
||||
make test
|
||||
```
|
||||
|
||||
### Formatting and Linting
|
||||
|
||||
Run these locally before submitting a PR; the CI system will check also.
|
||||
|
||||
#### Code Formatting
|
||||
|
||||
Formatting for this project is done via a combination of [Black](https://black.readthedocs.io/en/stable/) and [ruff](https://docs.astral.sh/ruff/rules/).
|
||||
|
||||
To run formatting for this project:
|
||||
|
||||
```sh
|
||||
make format
|
||||
```
|
||||
|
||||
#### Linting
|
||||
|
||||
Linting for this project is done via a combination of [Black](https://black.readthedocs.io/en/stable/), [ruff](https://docs.astral.sh/ruff/rules/), and [mypy](http://mypy-lang.org/).
|
||||
|
||||
To run linting for this project:
|
||||
|
||||
```sh
|
||||
make lint
|
||||
```
|
||||
@@ -1,12 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
"""Example LangChain server exposes a chain composed of a prompt and an LLM."""
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts import PromptTemplate
|
||||
|
||||
# from typing_extensions import TypedDict
|
||||
from langchain.pydantic_v1 import BaseModel
|
||||
from langchain.schema.output_parser import StrOutputParser
|
||||
from langchain.schema.runnable import ConfigurableField
|
||||
from typing_extensions import TypedDict
|
||||
from langchain.schema.runnable import ConfigurableField, RunnablePassthrough
|
||||
|
||||
from langserve import add_routes
|
||||
|
||||
@@ -17,7 +21,7 @@ model = ChatOpenAI(temperature=0.5).configurable_alternatives(
|
||||
default_key="medium_temp",
|
||||
)
|
||||
prompt = PromptTemplate.from_template(
|
||||
"tell me a joke about {topic}"
|
||||
"tell me a joke about {topic}.\nChat history: {chat_history}"
|
||||
).configurable_fields(
|
||||
template=ConfigurableField(
|
||||
id="prompt",
|
||||
@@ -25,7 +29,12 @@ prompt = PromptTemplate.from_template(
|
||||
description="The prompt to use. Must contain {topic}",
|
||||
)
|
||||
)
|
||||
chain = prompt | model | StrOutputParser()
|
||||
chain = (
|
||||
RunnablePassthrough.assign(chat_history=(lambda x: "\n".join(x)))
|
||||
| prompt
|
||||
| model
|
||||
| StrOutputParser()
|
||||
)
|
||||
|
||||
app = FastAPI(
|
||||
title="LangChain Server",
|
||||
@@ -47,11 +56,15 @@ app.add_middleware(
|
||||
# The input type is automatically inferred from the runnable
|
||||
# interface; however, if you want to override it, you can do so
|
||||
# by passing in the input_type argument to add_routes.
|
||||
class ChainInput(TypedDict):
|
||||
class ChainInput(BaseModel):
|
||||
"""The input to the chain."""
|
||||
|
||||
topic: str
|
||||
"""The topic of the joke."""
|
||||
chat_history: List[str]
|
||||
chat_history_tuples: List[Tuple[str, str]]
|
||||
chat_history_object_list: List[Dict[str, str]]
|
||||
tester: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
add_routes(app, chain, input_type=ChainInput, config_keys=["configurable"])
|
||||
|
||||
+6
-13
@@ -17,7 +17,7 @@ from urllib.parse import urljoin
|
||||
|
||||
import httpx
|
||||
from httpx._types import AuthTypes, CertTypes, CookieTypes, HeaderTypes, VerifyTypes
|
||||
from langchain.callbacks.tracers.log_stream import RunLog, RunLogPatch
|
||||
from langchain.callbacks.tracers.log_stream import RunLogPatch
|
||||
from langchain.load.dump import dumpd
|
||||
from langchain.schema.runnable import Runnable
|
||||
from langchain.schema.runnable.config import (
|
||||
@@ -401,7 +401,6 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
diff: bool = False,
|
||||
include_names: Optional[Sequence[str]] = None,
|
||||
include_types: Optional[Sequence[str]] = None,
|
||||
include_tags: Optional[Sequence[str]] = None,
|
||||
@@ -409,7 +408,7 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
exclude_types: Optional[Sequence[str]] = None,
|
||||
exclude_tags: Optional[Sequence[str]] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Union[AsyncIterator[RunLogPatch], AsyncIterator[RunLog]]:
|
||||
) -> AsyncIterator[RunLogPatch]:
|
||||
"""Stream all output from a runnable, as reported to the callback system.
|
||||
|
||||
This includes all inner runs of LLMs, Retrievers, Tools, etc.
|
||||
@@ -436,7 +435,7 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
"input": simple_dumpd(input),
|
||||
"config": _without_callbacks(config),
|
||||
"kwargs": kwargs,
|
||||
"diff": diff,
|
||||
"diff": True,
|
||||
"include_names": include_names,
|
||||
"include_types": include_types,
|
||||
"include_tags": include_tags,
|
||||
@@ -458,18 +457,12 @@ class RemoteRunnable(Runnable[Input, Output]):
|
||||
async for sse in event_source.aiter_sse():
|
||||
if sse.event == "data":
|
||||
data = simple_loads(sse.data)
|
||||
if diff:
|
||||
chunk = RunLogPatch(*data["ops"])
|
||||
else:
|
||||
chunk = RunLog(*data["ops"], state=data["state"])
|
||||
chunk = RunLogPatch(*data["ops"])
|
||||
|
||||
yield chunk
|
||||
|
||||
if diff:
|
||||
if final_output:
|
||||
final_output += chunk
|
||||
else:
|
||||
final_output = chunk
|
||||
if final_output:
|
||||
final_output += chunk
|
||||
else:
|
||||
final_output = chunk
|
||||
elif sse.event == "end":
|
||||
|
||||
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.yarn
|
||||
@@ -23,12 +23,14 @@
|
||||
"dayjs": "^1.11.10",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"json-schema-defaults": "^0.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lz-string": "^1.5.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"vaul": "^0.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
|
||||
@@ -55,10 +55,14 @@
|
||||
@apply focus-within:border-ls-blue focus-within:outline focus-within:outline-4 focus-within:outline-ls-blue/20;
|
||||
}
|
||||
|
||||
.control > label {
|
||||
.control > label, .control h6 {
|
||||
@apply text-xs uppercase font-semibold text-ls-gray-100;
|
||||
}
|
||||
|
||||
.control div .MuiGrid-item {
|
||||
@apply pt-0;
|
||||
}
|
||||
|
||||
.control > select {
|
||||
@apply -ml-1;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
IntegerCell,
|
||||
NumberCell,
|
||||
SliderCell,
|
||||
TextAreaCell,
|
||||
TimeCell,
|
||||
booleanCellTester,
|
||||
dateCellTester,
|
||||
@@ -51,7 +50,9 @@ import {
|
||||
|
||||
import { useSchemas } from "./useSchemas";
|
||||
import { RunState, useStreamLog } from "./useStreamLog";
|
||||
import { JsonFormsCore } from "@jsonforms/core";
|
||||
import { JsonFormsCore, RankedTester, rankWith } from "@jsonforms/core";
|
||||
import CustomArrayControlRenderer, { materialArrayControlTester } from "./components/CustomArrayControlRenderer";
|
||||
import CustomTextAreaCell from "./components/CustomTextAreaCell";
|
||||
|
||||
dayjs.extend(relativeDate);
|
||||
dayjs.extend(utc);
|
||||
@@ -73,8 +74,18 @@ const renderers = [
|
||||
{ tester: materialAllOfControlTester, renderer: MaterialAllOfRenderer },
|
||||
{ tester: materialAnyOfControlTester, renderer: MaterialAnyOfRenderer },
|
||||
{ tester: materialOneOfControlTester, renderer: MaterialOneOfRenderer },
|
||||
|
||||
// custom renderers
|
||||
{ tester: materialArrayControlTester, renderer: CustomArrayControlRenderer }
|
||||
];
|
||||
|
||||
export const nestedArrayControlTester: RankedTester = rankWith(
|
||||
1,
|
||||
(_, jsonSchema) => {
|
||||
return jsonSchema.type === "array";
|
||||
}
|
||||
);
|
||||
|
||||
const cells = [
|
||||
{ tester: booleanCellTester, cell: BooleanCell },
|
||||
{ tester: dateCellTester, cell: DateCell },
|
||||
@@ -83,9 +94,10 @@ const cells = [
|
||||
{ tester: integerCellTester, cell: IntegerCell },
|
||||
{ tester: numberCellTester, cell: NumberCell },
|
||||
{ tester: sliderCellTester, cell: SliderCell },
|
||||
{ tester: textAreaCellTester, cell: TextAreaCell },
|
||||
{ tester: textCellTester, cell: TextAreaCell },
|
||||
{ tester: textAreaCellTester, cell: CustomTextAreaCell },
|
||||
{ tester: textCellTester, cell: CustomTextAreaCell },
|
||||
{ tester: timeCellTester, cell: TimeCell },
|
||||
{ tester: nestedArrayControlTester, cell: CustomArrayControlRenderer },
|
||||
];
|
||||
|
||||
function IntermediateSteps(props: { latest: RunState }) {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// From https://github.com/eclipsesource/jsonforms/blob/44070b325121ad7173082fdf33be079f42ef96c4/packages/material/src/complex/MaterialArrayControlRenderer.tsx
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017-2019 EclipseSource Munich
|
||||
https://github.com/eclipsesource/jsonforms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
ArrayLayoutProps,
|
||||
RankedTester,
|
||||
isObjectArrayControl,
|
||||
isObjectArrayWithNesting,
|
||||
isPrimitiveArrayControl,
|
||||
or,
|
||||
rankWith,
|
||||
} from '@jsonforms/core';
|
||||
import { withJsonFormsArrayLayoutProps } from '@jsonforms/react';
|
||||
import { MaterialTableControl } from './MaterialTableControl';
|
||||
import { Hidden } from '@mui/material';
|
||||
import { DeleteDialog } from './DeleteDialog';
|
||||
|
||||
export const MaterialArrayControlRenderer = (props: ArrayLayoutProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [path, setPath] = useState(undefined);
|
||||
const [rowData, setRowData] = useState(undefined);
|
||||
const { removeItems, visible } = props;
|
||||
|
||||
const openDeleteDialog = useCallback(
|
||||
(p: string, rowIndex: number) => {
|
||||
setOpen(true);
|
||||
setPath(p);
|
||||
setRowData(rowIndex);
|
||||
},
|
||||
[setOpen, setPath, setRowData]
|
||||
);
|
||||
const deleteCancel = useCallback(() => setOpen(false), [setOpen]);
|
||||
const deleteConfirm = useCallback(() => {
|
||||
const p = path.substring(0, path.lastIndexOf('.'));
|
||||
removeItems(p, [rowData])();
|
||||
setOpen(false);
|
||||
}, [setOpen, path, rowData]);
|
||||
const deleteClose = useCallback(() => setOpen(false), [setOpen]);
|
||||
|
||||
return (
|
||||
<div className="control mt-4 mb-4">
|
||||
<Hidden xsUp={!visible}>
|
||||
<MaterialTableControl {...props} openDeleteDialog={openDeleteDialog} />
|
||||
<DeleteDialog
|
||||
open={open}
|
||||
onCancel={deleteCancel}
|
||||
onConfirm={deleteConfirm}
|
||||
onClose={deleteClose}
|
||||
acceptText={props.translations.deleteDialogAccept}
|
||||
declineText={props.translations.deleteDialogDecline}
|
||||
title={props.translations.deleteDialogTitle}
|
||||
message={props.translations.deleteDialogMessage}
|
||||
/>
|
||||
</Hidden>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const materialArrayControlTester: RankedTester = rankWith(
|
||||
999,
|
||||
or(isObjectArrayControl, isPrimitiveArrayControl, isObjectArrayWithNesting)
|
||||
);
|
||||
|
||||
export default withJsonFormsArrayLayoutProps(MaterialArrayControlRenderer);
|
||||
@@ -0,0 +1,64 @@
|
||||
// From https://github.com/eclipsesource/jsonforms/blob/master/packages/vanilla-renderers/src/cells/TextAreaCell.tsx
|
||||
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017-2019 EclipseSource Munich
|
||||
https://github.com/eclipsesource/jsonforms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
CellProps,
|
||||
isMultiLineControl,
|
||||
RankedTester,
|
||||
rankWith,
|
||||
} from '@jsonforms/core';
|
||||
import { withJsonFormsCellProps } from '@jsonforms/react';
|
||||
import { withVanillaCellProps, type VanillaRendererProps } from '@jsonforms/vanilla-renderers';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
export const TextAreaCell = (props: CellProps & VanillaRendererProps) => {
|
||||
const { data, className, id, enabled, config, uischema, path, handleChange } =
|
||||
props;
|
||||
const appliedUiSchemaOptions = merge({}, config, uischema.options);
|
||||
return (
|
||||
<textarea
|
||||
value={data || ''}
|
||||
onChange={(ev) =>
|
||||
handleChange(path, ev.target.value === '' ? undefined : ev.target.value)
|
||||
}
|
||||
className={className + " control"}
|
||||
style={{width: "100%", fontSize: "18px"}}
|
||||
id={id}
|
||||
disabled={!enabled}
|
||||
autoFocus={appliedUiSchemaOptions.focus}
|
||||
placeholder={appliedUiSchemaOptions.placeholder}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tester for a multi-line string control.
|
||||
* @type {RankedTester}
|
||||
*/
|
||||
export const textAreaCellTester: RankedTester = rankWith(2, isMultiLineControl);
|
||||
|
||||
export default withJsonFormsCellProps(withVanillaCellProps(TextAreaCell));
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017-2019 EclipseSource Munich
|
||||
https://github.com/eclipsesource/jsonforms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
} from '@mui/material';
|
||||
|
||||
export interface DeleteDialogProps {
|
||||
open: boolean;
|
||||
onClose(): void;
|
||||
onConfirm(): void;
|
||||
onCancel(): void;
|
||||
title: string;
|
||||
message: string;
|
||||
acceptText: string;
|
||||
declineText: string;
|
||||
}
|
||||
|
||||
export interface WithDeleteDialogSupport {
|
||||
openDeleteDialog(path: string, data: number): void;
|
||||
}
|
||||
|
||||
export const DeleteDialog = React.memo(function DeleteDialog({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
title,
|
||||
message,
|
||||
acceptText,
|
||||
declineText,
|
||||
}: DeleteDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
keepMounted
|
||||
onClose={onClose}
|
||||
aria-labelledby='alert-dialog-confirmdelete-title'
|
||||
aria-describedby='alert-dialog-confirmdelete-description'
|
||||
>
|
||||
<DialogTitle id='alert-dialog-confirmdelete-title'>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id='alert-dialog-confirmdelete-description'>
|
||||
{message}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel} color='primary'>
|
||||
{declineText}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} color='primary'>
|
||||
{acceptText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017-2019 EclipseSource Munich
|
||||
https://github.com/eclipsesource/jsonforms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import union from 'lodash/union';
|
||||
import {
|
||||
DispatchCell,
|
||||
JsonFormsStateContext,
|
||||
useJsonForms,
|
||||
} from '@jsonforms/react';
|
||||
import startCase from 'lodash/startCase';
|
||||
import range from 'lodash/range';
|
||||
import React, { Fragment, useMemo } from 'react';
|
||||
import {
|
||||
FormHelperText,
|
||||
Grid,
|
||||
Hidden,
|
||||
IconButton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrayLayoutProps,
|
||||
ControlElement,
|
||||
errorsAt,
|
||||
formatErrorMessage,
|
||||
JsonSchema,
|
||||
Paths,
|
||||
Resolve,
|
||||
JsonFormsRendererRegistryEntry,
|
||||
JsonFormsCellRendererRegistryEntry,
|
||||
encode,
|
||||
ArrayTranslations,
|
||||
} from '@jsonforms/core';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ArrowDownward from '@mui/icons-material/ArrowDownward';
|
||||
import ArrowUpward from '@mui/icons-material/ArrowUpward';
|
||||
|
||||
import { WithDeleteDialogSupport } from './DeleteDialog';
|
||||
import NoBorderTableCell from './NoBorderTableCell';
|
||||
import TableToolbar from './TableToolbar';
|
||||
import { ErrorObject } from 'ajv';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
// we want a cell that doesn't automatically span
|
||||
const styles = {
|
||||
fixedCell: {
|
||||
width: '150px',
|
||||
height: '50px',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
textAlign: 'center',
|
||||
},
|
||||
fixedCellSmall: {
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
textAlign: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
const generateCells = (
|
||||
Cell: React.ComponentType<OwnPropsOfNonEmptyCell | TableHeaderCellProps>,
|
||||
schema: JsonSchema,
|
||||
rowPath: string,
|
||||
enabled: boolean,
|
||||
cells?: JsonFormsCellRendererRegistryEntry[]
|
||||
) => {
|
||||
if (schema.type === 'object') {
|
||||
return getValidColumnProps(schema).map((prop) => {
|
||||
const cellPath = Paths.compose(rowPath, prop);
|
||||
const props = {
|
||||
propName: prop,
|
||||
schema,
|
||||
title: schema.properties?.[prop]?.title ?? startCase(prop),
|
||||
rowPath,
|
||||
cellPath,
|
||||
enabled,
|
||||
cells,
|
||||
};
|
||||
return <Cell key={cellPath} {...props} />;
|
||||
});
|
||||
} else {
|
||||
// primitives
|
||||
const props = {
|
||||
schema,
|
||||
rowPath,
|
||||
cellPath: rowPath,
|
||||
enabled,
|
||||
};
|
||||
return <Cell key={rowPath} {...props} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getValidColumnProps = (scopedSchema: JsonSchema) => {
|
||||
if (
|
||||
scopedSchema.type === 'object' &&
|
||||
typeof scopedSchema.properties === 'object'
|
||||
) {
|
||||
return Object.keys(scopedSchema.properties).filter(
|
||||
(prop) => scopedSchema.properties[prop].type !== 'array'
|
||||
);
|
||||
}
|
||||
// primitives
|
||||
return [''];
|
||||
};
|
||||
|
||||
export interface EmptyTableProps {
|
||||
numColumns: number;
|
||||
translations: ArrayTranslations;
|
||||
}
|
||||
|
||||
const EmptyTable = ({ numColumns, translations }: EmptyTableProps) => (
|
||||
<TableRow>
|
||||
<NoBorderTableCell colSpan={numColumns}>
|
||||
<Typography align='center'>{translations.noDataMessage}</Typography>
|
||||
</NoBorderTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
interface TableHeaderCellProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const TableHeaderCell = React.memo(function TableHeaderCell({
|
||||
title,
|
||||
}: TableHeaderCellProps) {
|
||||
return <TableCell>{title}</TableCell>;
|
||||
});
|
||||
|
||||
interface NonEmptyCellProps extends OwnPropsOfNonEmptyCell {
|
||||
rootSchema: JsonSchema;
|
||||
errors: string;
|
||||
path: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
interface OwnPropsOfNonEmptyCell {
|
||||
rowPath: string;
|
||||
propName?: string;
|
||||
schema: JsonSchema;
|
||||
enabled: boolean;
|
||||
renderers?: JsonFormsRendererRegistryEntry[];
|
||||
cells?: JsonFormsCellRendererRegistryEntry[];
|
||||
}
|
||||
const ctxToNonEmptyCellProps = (
|
||||
ctx: JsonFormsStateContext,
|
||||
ownProps: OwnPropsOfNonEmptyCell
|
||||
): NonEmptyCellProps => {
|
||||
const path =
|
||||
ownProps.rowPath +
|
||||
(ownProps.schema.type === 'object' ? '.' + ownProps.propName : '');
|
||||
const errors = formatErrorMessage(
|
||||
union(
|
||||
errorsAt(
|
||||
path,
|
||||
ownProps.schema,
|
||||
(p) => p === path
|
||||
)(ctx.core.errors).map((error: ErrorObject) => error.message)
|
||||
)
|
||||
);
|
||||
return {
|
||||
rowPath: ownProps.rowPath,
|
||||
propName: ownProps.propName,
|
||||
schema: ownProps.schema,
|
||||
rootSchema: ctx.core.schema,
|
||||
errors,
|
||||
path,
|
||||
enabled: ownProps.enabled,
|
||||
cells: ownProps.cells || ctx.cells,
|
||||
renderers: ownProps.renderers || ctx.renderers,
|
||||
};
|
||||
};
|
||||
|
||||
const controlWithoutLabel = (scope: string): ControlElement => ({
|
||||
type: 'Control',
|
||||
scope: scope,
|
||||
label: false,
|
||||
});
|
||||
|
||||
interface NonEmptyCellComponentProps {
|
||||
path: string;
|
||||
propName?: string;
|
||||
schema: JsonSchema;
|
||||
rootSchema: JsonSchema;
|
||||
errors: string;
|
||||
enabled: boolean;
|
||||
renderers?: JsonFormsRendererRegistryEntry[];
|
||||
cells?: JsonFormsCellRendererRegistryEntry[];
|
||||
isValid: boolean;
|
||||
}
|
||||
const NonEmptyCellComponent = React.memo(function NonEmptyCellComponent({
|
||||
path,
|
||||
propName,
|
||||
schema,
|
||||
rootSchema,
|
||||
errors,
|
||||
enabled,
|
||||
renderers,
|
||||
cells,
|
||||
isValid,
|
||||
}: NonEmptyCellComponentProps) {
|
||||
return (
|
||||
<NoBorderTableCell>
|
||||
{schema.properties ? (
|
||||
<DispatchCell
|
||||
schema={Resolve.schema(
|
||||
schema,
|
||||
`#/properties/${encode(propName)}`,
|
||||
rootSchema
|
||||
)}
|
||||
uischema={controlWithoutLabel(`#/properties/${encode(propName)}`)}
|
||||
path={path}
|
||||
enabled={enabled}
|
||||
renderers={renderers}
|
||||
cells={cells}
|
||||
/>
|
||||
) : (
|
||||
<DispatchCell
|
||||
schema={schema}
|
||||
uischema={controlWithoutLabel('#')}
|
||||
path={path}
|
||||
enabled={enabled}
|
||||
renderers={renderers}
|
||||
cells={cells}
|
||||
/>
|
||||
)}
|
||||
<FormHelperText error={!isValid}>{!isValid && errors}</FormHelperText>
|
||||
</NoBorderTableCell>
|
||||
);
|
||||
});
|
||||
|
||||
const NonEmptyCell = (ownProps: OwnPropsOfNonEmptyCell) => {
|
||||
const ctx = useJsonForms();
|
||||
const emptyCellProps = ctxToNonEmptyCellProps(ctx, ownProps);
|
||||
|
||||
const isValid = isEmpty(emptyCellProps.errors);
|
||||
return <NonEmptyCellComponent {...emptyCellProps} isValid={isValid} />;
|
||||
};
|
||||
|
||||
interface NonEmptyRowProps {
|
||||
childPath: string;
|
||||
schema: JsonSchema;
|
||||
rowIndex: number;
|
||||
moveUpCreator: (path: string, position: number) => () => void;
|
||||
moveDownCreator: (path: string, position: number) => () => void;
|
||||
enableUp: boolean;
|
||||
enableDown: boolean;
|
||||
showSortButtons: boolean;
|
||||
enabled: boolean;
|
||||
cells?: JsonFormsCellRendererRegistryEntry[];
|
||||
path: string;
|
||||
translations: ArrayTranslations;
|
||||
}
|
||||
|
||||
const NonEmptyRowComponent = ({
|
||||
childPath,
|
||||
schema,
|
||||
rowIndex,
|
||||
openDeleteDialog,
|
||||
moveUpCreator,
|
||||
moveDownCreator,
|
||||
enableUp,
|
||||
enableDown,
|
||||
showSortButtons,
|
||||
enabled,
|
||||
cells,
|
||||
path,
|
||||
translations,
|
||||
}: NonEmptyRowProps & WithDeleteDialogSupport) => {
|
||||
const moveUp = useMemo(
|
||||
() => moveUpCreator(path, rowIndex),
|
||||
[moveUpCreator, path, rowIndex]
|
||||
);
|
||||
const moveDown = useMemo(
|
||||
() => moveDownCreator(path, rowIndex),
|
||||
[moveDownCreator, path, rowIndex]
|
||||
);
|
||||
return (
|
||||
<TableRow key={childPath} hover>
|
||||
{generateCells(NonEmptyCell, schema, childPath, enabled, cells)}
|
||||
{enabled ? (
|
||||
<NoBorderTableCell
|
||||
style={showSortButtons ? styles.fixedCell : styles.fixedCellSmall}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
direction='row'
|
||||
justifyContent='flex-end'
|
||||
alignItems='center'
|
||||
>
|
||||
{showSortButtons ? (
|
||||
<Fragment>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
aria-label={translations.upAriaLabel}
|
||||
onClick={moveUp}
|
||||
disabled={!enableUp}
|
||||
size='large'
|
||||
>
|
||||
<ArrowUpward />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
aria-label={translations.downAriaLabel}
|
||||
onClick={moveDown}
|
||||
disabled={!enableDown}
|
||||
size='large'
|
||||
>
|
||||
<ArrowDownward />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Fragment>
|
||||
) : null}
|
||||
<Grid item>
|
||||
<IconButton
|
||||
aria-label={translations.removeAriaLabel}
|
||||
onClick={() => openDeleteDialog(childPath, rowIndex)}
|
||||
size='large'
|
||||
>
|
||||
<DeleteIcon style={{ color: 'white' }} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</NoBorderTableCell>
|
||||
) : null}
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
export const NonEmptyRow = React.memo(NonEmptyRowComponent);
|
||||
interface TableRowsProp {
|
||||
data: number;
|
||||
path: string;
|
||||
schema: JsonSchema;
|
||||
uischema: ControlElement;
|
||||
config?: any;
|
||||
enabled: boolean;
|
||||
cells?: JsonFormsCellRendererRegistryEntry[];
|
||||
moveUp?(path: string, toMove: number): () => void;
|
||||
moveDown?(path: string, toMove: number): () => void;
|
||||
translations: ArrayTranslations;
|
||||
}
|
||||
const TableRows = ({
|
||||
data,
|
||||
path,
|
||||
schema,
|
||||
openDeleteDialog,
|
||||
moveUp,
|
||||
moveDown,
|
||||
uischema,
|
||||
config,
|
||||
enabled,
|
||||
cells,
|
||||
translations,
|
||||
}: TableRowsProp & WithDeleteDialogSupport) => {
|
||||
const isEmptyTable = data === 0;
|
||||
|
||||
if (isEmptyTable) {
|
||||
return (
|
||||
<EmptyTable
|
||||
numColumns={getValidColumnProps(schema).length + 1}
|
||||
translations={translations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const appliedUiSchemaOptions = merge({}, config, uischema.options);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{range(data).map((index: number) => {
|
||||
const childPath = Paths.compose(path, `${index}`);
|
||||
|
||||
return (
|
||||
<NonEmptyRow
|
||||
key={childPath}
|
||||
childPath={childPath}
|
||||
rowIndex={index}
|
||||
schema={schema}
|
||||
openDeleteDialog={openDeleteDialog}
|
||||
moveUpCreator={moveUp}
|
||||
moveDownCreator={moveDown}
|
||||
enableUp={index !== 0}
|
||||
enableDown={index !== data - 1}
|
||||
showSortButtons={
|
||||
appliedUiSchemaOptions.showSortButtons ||
|
||||
appliedUiSchemaOptions.showArrayTableSortButtons
|
||||
}
|
||||
enabled={enabled}
|
||||
cells={cells}
|
||||
path={path}
|
||||
translations={translations}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export class MaterialTableControl extends React.Component<
|
||||
ArrayLayoutProps & WithDeleteDialogSupport,
|
||||
any
|
||||
> {
|
||||
addItem = (path: string, value: any) => this.props.addItem(path, value);
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
path,
|
||||
schema,
|
||||
rootSchema,
|
||||
uischema,
|
||||
errors,
|
||||
openDeleteDialog,
|
||||
visible,
|
||||
enabled,
|
||||
cells,
|
||||
translations,
|
||||
} = this.props;
|
||||
|
||||
const controlElement = uischema as ControlElement;
|
||||
const isObjectSchema = schema.type === 'object';
|
||||
const headerCells: any = isObjectSchema
|
||||
? generateCells(TableHeaderCell, schema, path, enabled, cells)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Hidden xsUp={!visible}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableToolbar
|
||||
errors={errors}
|
||||
label={label}
|
||||
addItem={this.addItem}
|
||||
numColumns={isObjectSchema ? headerCells.length : 1}
|
||||
path={path}
|
||||
uischema={controlElement}
|
||||
schema={schema}
|
||||
rootSchema={rootSchema}
|
||||
enabled={enabled}
|
||||
translations={translations}
|
||||
/>
|
||||
{isObjectSchema && (
|
||||
<TableRow>
|
||||
{headerCells}
|
||||
{enabled ? <TableCell /> : null}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRows
|
||||
openDeleteDialog={openDeleteDialog}
|
||||
translations={translations}
|
||||
{...this.props}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Hidden>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017-2019 EclipseSource Munich
|
||||
https://github.com/eclipsesource/jsonforms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { TableCell } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const StyledTableCell = styled(TableCell)({
|
||||
borderBottom: 'none',
|
||||
color: "white",
|
||||
fill: "white",
|
||||
});
|
||||
|
||||
const NoBorderTableCell = ({ children, ...otherProps }: any) => (
|
||||
<StyledTableCell {...otherProps}>{children}</StyledTableCell>
|
||||
);
|
||||
|
||||
export default NoBorderTableCell;
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017-2019 EclipseSource Munich
|
||||
https://github.com/eclipsesource/jsonforms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
ControlElement,
|
||||
createDefaultValue,
|
||||
JsonSchema,
|
||||
ArrayTranslations,
|
||||
} from '@jsonforms/core';
|
||||
import { IconButton, TableRow, Tooltip, Grid, Typography } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ValidationIcon from './ValidationIcon';
|
||||
import NoBorderTableCell from './NoBorderTableCell';
|
||||
|
||||
export interface MaterialTableToolbarProps {
|
||||
numColumns: number;
|
||||
errors: string;
|
||||
label: string;
|
||||
path: string;
|
||||
uischema: ControlElement;
|
||||
schema: JsonSchema;
|
||||
rootSchema: JsonSchema;
|
||||
enabled: boolean;
|
||||
translations: ArrayTranslations;
|
||||
addItem(path: string, value: any): () => void;
|
||||
}
|
||||
|
||||
const fixedCellSmall = {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
};
|
||||
|
||||
const TableToolbar = React.memo(function TableToolbar({
|
||||
numColumns,
|
||||
errors,
|
||||
label,
|
||||
path,
|
||||
addItem,
|
||||
schema,
|
||||
enabled,
|
||||
translations,
|
||||
}: MaterialTableToolbarProps) {
|
||||
return (
|
||||
<TableRow>
|
||||
<NoBorderTableCell colSpan={numColumns}>
|
||||
<Grid
|
||||
container
|
||||
justifyContent={'flex-start'}
|
||||
alignItems={'center'}
|
||||
spacing={2}
|
||||
>
|
||||
<Grid item>
|
||||
<Typography variant={'h6'}>{label}</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{errors.length !== 0 && (
|
||||
<Grid item>
|
||||
<ValidationIcon
|
||||
id='tooltip-validation'
|
||||
errorMessages={errors}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</NoBorderTableCell>
|
||||
{enabled ? (
|
||||
<NoBorderTableCell align='right' style={fixedCellSmall}>
|
||||
<Tooltip
|
||||
id='tooltip-add'
|
||||
title={translations.addTooltip}
|
||||
placement='bottom'
|
||||
>
|
||||
<IconButton
|
||||
aria-label={translations.addAriaLabel}
|
||||
onClick={addItem(path, createDefaultValue(schema))}
|
||||
size='large'
|
||||
style={{ color: 'white' }}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</NoBorderTableCell>
|
||||
) : null}
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
export default TableToolbar;
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017-2019 EclipseSource Munich
|
||||
https://github.com/eclipsesource/jsonforms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import { Badge, Tooltip, styled } from '@mui/material';
|
||||
|
||||
const StyledBadge = styled(Badge)(({ theme }: any) => ({
|
||||
color: theme.palette.error.main,
|
||||
}));
|
||||
|
||||
export interface ValidationProps {
|
||||
errorMessages: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ValidationIcon: React.FC<ValidationProps> = ({ errorMessages, id }) => {
|
||||
return (
|
||||
<Tooltip id={id} title={errorMessages}>
|
||||
<StyledBadge badgeContent={errorMessages.split('\n').length}>
|
||||
<ErrorOutlineIcon color='inherit' />
|
||||
</StyledBadge>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ValidationIcon;
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App/>);
|
||||
|
||||
+5048
-2841
File diff suppressed because it is too large
Load Diff
+9
-20
@@ -21,7 +21,7 @@ from typing import (
|
||||
)
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from langchain.callbacks.tracers.log_stream import RunLog, RunLogPatch
|
||||
from langchain.callbacks.tracers.log_stream import RunLogPatch
|
||||
from langchain.load.serializable import Serializable
|
||||
from langchain.schema.runnable import Runnable
|
||||
from langchain.schema.runnable.config import merge_configs
|
||||
@@ -467,7 +467,7 @@ def add_routes(
|
||||
async for chunk in runnable.astream_log(
|
||||
input_,
|
||||
config=config,
|
||||
diff=stream_log_request.diff,
|
||||
diff=True,
|
||||
include_names=stream_log_request.include_names,
|
||||
include_types=stream_log_request.include_types,
|
||||
include_tags=stream_log_request.include_tags,
|
||||
@@ -475,24 +475,13 @@ def add_routes(
|
||||
exclude_types=stream_log_request.exclude_types,
|
||||
exclude_tags=stream_log_request.exclude_tags,
|
||||
):
|
||||
if stream_log_request.diff: # Run log patch
|
||||
if not isinstance(chunk, RunLogPatch):
|
||||
raise AssertionError(
|
||||
f"Expected a RunLog instance got {type(chunk)}"
|
||||
)
|
||||
data = {
|
||||
"ops": chunk.ops,
|
||||
}
|
||||
else:
|
||||
# Then it's a run log
|
||||
if not isinstance(chunk, RunLog):
|
||||
raise AssertionError(
|
||||
f"Expected a RunLog instance got {type(chunk)}"
|
||||
)
|
||||
data = {
|
||||
"state": chunk.state,
|
||||
"ops": chunk.ops,
|
||||
}
|
||||
if not isinstance(chunk, RunLogPatch):
|
||||
raise AssertionError(
|
||||
f"Expected a RunLog instance got {type(chunk)}"
|
||||
)
|
||||
data = {
|
||||
"ops": chunk.ops,
|
||||
}
|
||||
|
||||
# Temporary adapter
|
||||
yield {
|
||||
|
||||
@@ -136,7 +136,6 @@ def create_stream_log_request_model(
|
||||
f"{namespace}StreamLogRequest",
|
||||
input=(input_type, ...),
|
||||
config=(config, Field(default_factory=dict)),
|
||||
diff=(Optional[bool], False),
|
||||
include_names=(
|
||||
Optional[Sequence[str]],
|
||||
Field(
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langserve"
|
||||
version = "0.0.9"
|
||||
version = "0.0.10"
|
||||
description = ""
|
||||
readme = "README.md"
|
||||
authors = ["LangChain"]
|
||||
|
||||
@@ -11,7 +11,7 @@ import pytest_asyncio
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx import AsyncClient
|
||||
from langchain.callbacks.tracers.log_stream import RunLog, RunLogPatch
|
||||
from langchain.callbacks.tracers.log_stream import RunLogPatch
|
||||
from langchain.prompts import PromptTemplate
|
||||
from langchain.prompts.base import StringPromptValue
|
||||
from langchain.schema.messages import HumanMessage, SystemMessage
|
||||
@@ -278,47 +278,33 @@ async def test_astream(async_client: RemoteRunnable) -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_astream_log_no_diff(async_client: RemoteRunnable) -> None:
|
||||
async def test_astream_log_diff_no_effect(async_client: RemoteRunnable) -> None:
|
||||
"""Test async stream."""
|
||||
run_logs = []
|
||||
|
||||
async for chunk in async_client.astream_log(1, diff=False):
|
||||
run_logs.append(chunk)
|
||||
|
||||
assert len(run_logs) == 3
|
||||
|
||||
op = run_logs[0].ops[0]
|
||||
uuid = op["value"]["id"]
|
||||
|
||||
for run_log in run_logs:
|
||||
assert isinstance(run_log, RunLog)
|
||||
|
||||
states = [run_log.state for run_log in run_logs]
|
||||
|
||||
assert states == [
|
||||
{
|
||||
"final_output": None,
|
||||
"id": uuid,
|
||||
"logs": {},
|
||||
"streamed_output": [],
|
||||
},
|
||||
{
|
||||
"final_output": {"output": 2},
|
||||
"id": uuid,
|
||||
"logs": {},
|
||||
"streamed_output": [],
|
||||
},
|
||||
{
|
||||
"final_output": {"output": 2},
|
||||
"id": uuid,
|
||||
"logs": {},
|
||||
"streamed_output": [2],
|
||||
},
|
||||
assert [run_log_patch.ops for run_log_patch in run_logs] == [
|
||||
[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "",
|
||||
"value": {
|
||||
"final_output": {"output": 2},
|
||||
"id": uuid,
|
||||
"logs": {},
|
||||
"streamed_output": [],
|
||||
},
|
||||
}
|
||||
],
|
||||
[{"op": "replace", "path": "/final_output", "value": {"output": 2}}],
|
||||
[{"op": "add", "path": "/streamed_output/-", "value": 2}],
|
||||
]
|
||||
|
||||
# Check that we're picking up one extra op on each chunk
|
||||
assert [len(run_log.ops) for run_log in run_logs] == [1, 2, 3]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_astream_log(async_client: RemoteRunnable) -> None:
|
||||
|
||||
Reference in New Issue
Block a user