Compare commits

...

7 Commits

Author SHA1 Message Date
jacoblee93 fcfc2ba2f5 Update test case 2023-10-18 23:20:18 -07:00
jacoblee93 7eb71dbd5e Adds custom array and text controls 2023-10-18 19:53:34 -07:00
jacoblee93 cb6e6425c9 Merge branch 'main' of https://github.com/langchain-ai/langserve into jacob/playground 2023-10-18 17:10:09 -07:00
Eugene Yurtsev 6eeb274875 Minor fix in contribution guidelines (#55)
Minor fix
2023-10-17 21:46:54 -04:00
Eugene Yurtsev 779b8e1cc9 Add CONTRIBUTING.md guidelines (#54)
Add contributing.md. Inserting this in the root of the project, so it's
easier to find. If folks feel strongly can put in in .github.
2023-10-17 21:45:10 -04:00
Nuno Campos ada5d103b4 Update default to match langchain (#51)
Co-authored-by: jacoblee93 <jacoblee93@gmail.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2023-10-17 21:20:44 -04:00
Eugene Yurtsev fb0f9a0dc3 Version 0.0.10 bump (#50)
Bump
2023-10-17 14:42:27 -04:00
20 changed files with 6096 additions and 2918 deletions
+51
View File
@@ -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
```
+18 -5
View File
@@ -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
View File
@@ -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":
+2
View File
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.yarn
+2
View File
@@ -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",
+5 -1
View File
@@ -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;
}
+16 -4
View File
@@ -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 -1
View File
@@ -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/>);
File diff suppressed because it is too large Load Diff
+9 -20
View File
@@ -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 {
-1
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langserve"
version = "0.0.9"
version = "0.0.10"
description = ""
readme = "README.md"
authors = ["LangChain"]
+17 -31
View File
@@ -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: