Release 0.6.8 (#1480)

This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-02-28 13:18:51 -03:00 committed by GitHub
commit a7e28c911f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 807 additions and 2107 deletions

View file

@ -3,7 +3,15 @@ name: lint
on:
push:
branches: [main]
paths:
- "poetry.lock"
- "pyproject.toml"
- "src/backend/**"
pull_request:
paths:
- "poetry.lock"
- "pyproject.toml"
- "src/backend/**"
env:
POETRY_VERSION: "1.7.0"

View file

@ -3,8 +3,16 @@ name: test
on:
push:
branches: [main]
paths:
- "poetry.lock"
- "pyproject.toml"
- "src/backend/**"
pull_request:
branches: [dev]
paths:
- "poetry.lock"
- "pyproject.toml"
- "src/backend/**"
env:
POETRY_VERSION: "1.5.0"

View file

@ -9,6 +9,21 @@ import Admonition from '@theme/Admonition';
</Admonition>
### SearchApi
Real-time search engine results API. Returns structured JSON data that includes answer box, knowledge graph, organic results, and more.
**Parameters**
- **Api Key:** A unique identifier for the SearchApi, necessary for authenticating requests to real-time search engines. This key can be retrieved from the [SearchApi dashboard](https://www.searchapi.io/).
- **Engine:** Specifies the search engine. For instance: google, google_scholar, bing, youtube, and youtube_transcripts. A full list of supported engines is available in the [documentation](https://www.searchapi.io/docs/google).
- **Parameters:** Allows the selection of any parameters recognized by SearchApi, with some being required and others optional.
**Output**
- **Document:** The JSON response from the request as a Document.
### BingSearchRun
Bing Search is a web search engine owned and operated by Microsoft. It provides search results for various types of content, including web pages, images, videos, and news articles. It uses a combination of algorithms and human editors to deliver search results to users.
@ -60,4 +75,4 @@ Tool for getting metadata about a SQL database. The input to this tool is a comm
**Params**
- **Db:** SQLDatabase to query.
- **Db:** SQLDatabase to query.

View file

@ -0,0 +1,52 @@
import Admonition from "@theme/Admonition";
# SearchApi Tool
The [SearchApi](https://www.searchapi.io/) allows developers to retrieve results from search engines such as Google, Google Scholar, YouTube, YouTube transcripts, and more, and can be used as in Langflow through the `SearchApi` tool.
<Admonition type="info">
To use the SearchApi, you must first obtain an API key by registering at [SearchApi's website](https://www.searchapi.io/).
</Admonition>
In the given example, we specify `engine` as `youtube_transcripts` and provide a `video_id`.
<Admonition type="info">
All engines and parameters can be found in [SearchApi documentation](https://www.searchapi.io/docs/google).
</Admonition>
The `RetrievalQA` chain processes a `Document` along with a user's question to return an answer.
<Admonition type="tip">
In this example, we used [`ChatOpenAI`](https://platform.openai.com/) as the
LLM, but feel free to experiment with other Language Models!
</Admonition>
The `RetrievalQA` takes `CombineDocsChain` and `SearchApi` tool as inputs, using the tool as a `Document` to answer questions.
<Admonition type="info">
Learn more about the SearchApi
[here](https://python.langchain.com/docs/integrations/tools/searchapi).
</Admonition>
## ⛓️ Langflow Example
import ThemedImage from "@theme/ThemedImage";
import useBaseUrl from "@docusaurus/useBaseUrl";
import ZoomableImage from "/src/theme/ZoomableImage.js";
<ZoomableImage
alt="Docusaurus themed image"
sources={{
light: "img/searchapi-tool.png",
}}
/>
#### <a target="\_blank" href="json_files/SearchApi_Tool.json" download>Download Flow</a>
<Admonition type="note" title="LangChain Components 🦜🔗">
- [`OpenAI`](https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai)
- [`SearchApiAPIWrapper`](https://python.langchain.com/docs/integrations/providers/searchapi#wrappers)
- [`ZeroShotAgent`](https://python.langchain.com/docs/modules/agents/how_to/custom_mrkl_agent)
</Admonition>

View file

@ -81,6 +81,7 @@ module.exports = {
"examples/buffer-memory",
"examples/midjourney-prompt-chain",
"examples/csv-loader",
"examples/searchapi-tool",
"examples/serp-api-tool",
"examples/multiple-vectorstores",
"examples/python-function",

BIN
docs/static/img/searchapi-tool.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

File diff suppressed because one or more lines are too long

973
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "langflow"
version = "0.6.7"
version = "0.6.8"
description = "A Python package with a built-in web application"
authors = ["Logspace <contact@logspace.ai>"]
maintainers = [
@ -61,7 +61,7 @@ python-multipart = "^0.0.7"
sqlmodel = "^0.0.14"
faiss-cpu = "^1.7.4"
anthropic = "^0.15.0"
orjson = "3.9.3"
orjson = "3.9.15"
multiprocess = "^0.70.14"
cachetools = "^5.3.1"
types-cachetools = "^5.3.0.5"

View file

@ -14,6 +14,11 @@ from loguru import logger
class AsyncStreamingLLMCallbackHandler(AsyncCallbackHandler):
"""Callback handler for streaming LLM responses."""
@property
def ignore_chain(self) -> bool:
"""Whether to ignore chain callbacks."""
return False
def __init__(self, client_id: str):
self.chat_service = get_chat_service()
self.client_id = client_id

View file

@ -0,0 +1,51 @@
from langflow import CustomComponent
from langchain.schema import Document
from langflow.services.database.models.base import orjson_dumps
from langchain_community.utilities.searchapi import SearchApiAPIWrapper
from typing import Optional
class SearchApi(CustomComponent):
display_name: str = "SearchApi"
description: str = "Real-time search engine results API."
output_types: list[str] = ["Document"]
documentation: str = "https://www.searchapi.io/docs/google"
field_config = {
"engine": {
"display_name": "Engine",
"field_type": "str",
"info": "The search engine to use.",
},
"params": {
"display_name": "Parameters",
"info": "The parameters to send with the request.",
},
"code": {"show": False},
"api_key": {
"display_name": "API Key",
"field_type": "str",
"required": True,
"password": True,
"info": "The API key to use SearchApi.",
},
}
def build(
self,
engine: str,
api_key: str,
params: Optional[dict] = None,
) -> Document:
if params is None:
params = {}
search_api_wrapper = SearchApiAPIWrapper(engine=engine, searchapi_api_key=api_key)
q = params.pop("q", "SearchApi Langflow")
results = search_api_wrapper.results(q, **params)
result = orjson_dumps(results, indent_2=False)
document = Document(page_content=result)
return document

View file

@ -3,12 +3,10 @@ from typing import Any
from langchain.agents import AgentExecutor
from langchain.chains.base import Chain
from langchain_core.runnables import Runnable
from loguru import logger
from langflow.api.v1.schemas import ChatMessage
from langflow.interface.utils import try_setting_streaming_options
from langflow.processing.base import get_result_and_steps
from langflow.utils.chat import ChatDefinition
from loguru import logger
LANGCHAIN_RUNNABLES = (Chain, Runnable, AgentExecutor)
@ -40,20 +38,6 @@ async def process_graph(
client_id=client_id,
session_id=session_id,
)
elif isinstance(build_result, ChatDefinition):
raw_output = await run_build_result(
build_result,
chat_inputs,
client_id=client_id,
session_id=session_id,
)
if isinstance(raw_output, dict):
if not build_result.output_key:
raise ValueError("No output key provided to ChatDefinition when returning a dict.")
result = raw_output[build_result.output_key]
else:
result = raw_output
intermediate_steps = []
else:
raise TypeError(f"Unknown type {type(build_result)}")
logger.debug("Generated result and intermediate_steps")

View file

@ -1,34 +0,0 @@
from typing import Any, Callable, Optional, Union
from langchain_core.prompts import PromptTemplate as LCPromptTemplate
from langflow.utils.prompt import GenericPromptTemplate
from llama_index.prompts import PromptTemplate as LIPromptTemplate
PromptTemplate = Union[LCPromptTemplate, LIPromptTemplate]
class ChatDefinition:
def __init__(
self,
func: Callable,
inputs: list[str],
output_key: Optional[str] = None,
prompt_template: Optional[PromptTemplate] = None,
):
self.func = func
self.input_keys = inputs
self.output_key = output_key
self.prompt_template = prompt_template
@classmethod
def from_prompt_template(cls, prompt_template: PromptTemplate, func: Callable, output_key: Optional[str] = None):
prompt = GenericPromptTemplate(prompt_template)
return cls(
func=func,
inputs=prompt.input_keys,
output_key=output_key,
prompt_template=prompt_template,
)
def __call__(self, inputs: dict, callbacks: Optional[Any] = None) -> dict:
return self.func(inputs, callbacks)

View file

@ -1,58 +0,0 @@
from typing import Any, Union
from langchain_core.prompts import PromptTemplate as LCPromptTemplate
from llama_index.prompts import PromptTemplate as LIPromptTemplate
PromptTemplateTypes = Union[LCPromptTemplate, LIPromptTemplate]
class GenericPromptTemplate:
def __init__(self, prompt_template: PromptTemplateTypes):
object.__setattr__(self, "prompt_template", prompt_template)
@property
def input_keys(self):
prompt_template = object.__getattribute__(self, "prompt_template")
if isinstance(prompt_template, LCPromptTemplate):
return prompt_template.input_variables
elif isinstance(prompt_template, LIPromptTemplate):
return prompt_template.template_vars
else:
raise TypeError(f"Unknown prompt template type {type(prompt_template)}")
def to_lc_prompt(self):
prompt_template = object.__getattribute__(self, "prompt_template")
if isinstance(prompt_template, LCPromptTemplate):
return prompt_template
elif isinstance(prompt_template, LIPromptTemplate):
return LCPromptTemplate.from_template(prompt_template.get_template())
else:
raise TypeError(f"Unknown prompt template type {type(prompt_template)}")
def to_li_prompt(self):
prompt_template = object.__getattribute__(self, "prompt_template")
if isinstance(prompt_template, LIPromptTemplate):
return prompt_template
elif isinstance(prompt_template, LCPromptTemplate):
return LIPromptTemplate(template=prompt_template.template)
else:
raise TypeError(f"Unknown prompt template type {type(prompt_template)}")
def __or__(self, other):
prompt_template = object.__getattribute__(self, "prompt_template")
if isinstance(prompt_template, LIPromptTemplate):
return self.to_lc_prompt() | other
else:
raise TypeError(f"Unknown prompt template type {type(other)}")
def __getattribute__(self, name: str) -> Any:
if name in {
"input_keys",
"to_lc_prompt",
"to_li_prompt",
"__or__",
"prompt_template",
}:
return object.__getattribute__(self, name)
prompt_template = object.__getattribute__(self, "prompt_template")
return getattr(prompt_template, name)

View file

@ -21,6 +21,7 @@ import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
import { useTypesStore } from "./stores/typesStore";
import { useStoreStore } from "./stores/storeStore";
export default function App() {
const errorData = useAlertStore((state) => state.errorData);
@ -32,7 +33,6 @@ export default function App() {
const successData = useAlertStore((state) => state.successData);
const successOpen = useAlertStore((state) => state.successOpen);
const setSuccessOpen = useAlertStore((state) => state.setSuccessOpen);
const loading = useAlertStore((state) => state.loading);
const [fetchError, setFetchError] = useState(false);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
@ -122,9 +122,11 @@ export default function App() {
const { isAuthenticated } = useContext(AuthContext);
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
const fetchApiData = useStoreStore((state) => state.fetchApiData);
const getTypes = useTypesStore((state) => state.getTypes);
const refreshVersion = useDarkStore((state) => state.refreshVersion);
const refreshStars = useDarkStore((state) => state.refreshStars);
const checkHasStore = useStoreStore((state) => state.checkHasStore);
useEffect(() => {
refreshStars();
@ -136,6 +138,8 @@ export default function App() {
getTypes().then(() => {
refreshFlows();
});
checkHasStore();
fetchApiData();
}
}, [isAuthenticated]);

View file

@ -186,7 +186,7 @@ export default function GenericNode({
<div
className={
"generic-node-title-arrangement rounded-full" +
(!showNode && "justify-center")
(!showNode && " justify-center ")
}
>
{iconNodeRender()}

View file

@ -7,6 +7,7 @@ import {
requestLogout,
} from "../controllers/API";
import useAlertStore from "../stores/alertStore";
import useFlowsManagerStore from "../stores/flowsManagerStore";
import { Users } from "../types/api";
import { AuthContextType } from "../types/contexts/auth";
@ -79,6 +80,7 @@ export function AuthProvider({ children }): React.ReactElement {
getUser();
} else {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
}
});
}, [setUserData, setLoading, autoLogin, setIsAdmin]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Before After
Before After

View file

@ -172,16 +172,16 @@ export default function NodeToolbarComponent({
<IconComponent name="Copy" className="h-4 w-4" />
</button>
</ShadTooltip>
{hasStore && (
<ShadTooltip content="Share" side="top">
{hasStore && (
<ShadTooltip content={(!hasApiKey || !validApiKey) ? "Set a valid API key to share this component." : "Share"} side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
!hasApiKey || !validApiKey ? " text-muted-foreground" : ""
(!hasApiKey || !validApiKey) ? " text-muted-foreground cursor-not-allowed" : ""
)}
onClick={(event) => {
event.preventDefault();
if (hasApiKey || hasStore) setShowconfirmShare(true);
if (hasApiKey && hasStore && validApiKey) setShowconfirmShare(true);
}}
>
<IconComponent name="Share3" className="-m-1 h-6 w-6" />

View file

@ -39,8 +39,6 @@ export default function StorePage(): JSX.Element {
const loadingApiKey = useStoreStore((state) => state.loadingApiKey);
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
const setLoadingApiKey = useStoreStore((state) => state.updateLoadingApiKey);
const setHasApiKey = useStoreStore((state) => state.updateHasApiKey);
const { apiKey } = useContext(AuthContext);
@ -48,6 +46,9 @@ export default function StorePage(): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
);
const currentFlowId = useFlowsManagerStore(
(state) => state.currentFlowId
);
const [loading, setLoading] = useState(true);
const [loadingTags, setLoadingTags] = useState(true);
const { id } = useParams();
@ -63,10 +64,6 @@ export default function StorePage(): JSX.Element {
const [searchNow, setSearchNow] = useState("");
const [selectFilter, setSelectFilter] = useState("all");
useEffect(() => {
handleGetTags();
}, []);
useEffect(() => {
if (!loadingApiKey) {
if (!hasApiKey) {
@ -86,9 +83,10 @@ export default function StorePage(): JSX.Element {
});
}
}
}, [loadingApiKey, validApiKey, hasApiKey]);
}, [loadingApiKey, validApiKey, hasApiKey, currentFlowId]);
useEffect(() => {
handleGetTags();
handleGetComponents();
}, [
tabActive,
@ -119,7 +117,7 @@ export default function StorePage(): JSX.Element {
}
function handleGetComponents() {
if (!hasApiKey || loadingApiKey) return;
if (loadingApiKey) return;
setLoading(true);
getStoreComponents({
component_id: id,
@ -176,23 +174,6 @@ export default function StorePage(): JSX.Element {
setPageSize(12);
}
const fetchApiData = async () => {
setLoadingApiKey(true);
try {
const res = await checkHasApiKey();
setHasApiKey(res?.has_api_key ?? false);
setValidApiKey(res?.is_valid ?? false);
setLoadingApiKey(false);
} catch (e) {
setLoadingApiKey(false);
console.log(e);
}
};
useEffect(() => {
fetchApiData();
}, [apiKey]);
return (
<PageLayout
betaIcon

View file

@ -1,37 +1,34 @@
import { create } from "zustand";
import { checkHasApiKey, checkHasStore } from "../controllers/API";
import { StoreStoreType } from "../types/zustand/store";
import useAlertStore from "./alertStore";
export const useStoreStore = create<StoreStoreType>((set) => ({
hasStore: true,
validApiKey: false,
hasApiKey: false,
loadingApiKey: true,
updateHasStore: (hasStore) => set(() => ({ hasStore: hasStore })),
checkHasStore: () => {
checkHasStore().then((res) => {
set({ hasStore: res?.enabled ?? false });
});
},
updateValidApiKey: (validApiKey) => set(() => ({ validApiKey: validApiKey })),
updateLoadingApiKey: (loadingApiKey) =>
set(() => ({ loadingApiKey: loadingApiKey })),
updateHasApiKey: (hasApiKey) => set(() => ({ hasApiKey: hasApiKey })),
fetchApiData: async () => {
set({ loadingApiKey: true });
try {
const res = await checkHasApiKey();
set({
validApiKey: res?.is_valid ?? false,
hasApiKey: res?.has_api_key ?? false,
loadingApiKey: false,
});
} catch (e) {
set({ loadingApiKey: false });
console.log(e);
}
},
}));
checkHasStore().then((res) => {
useStoreStore.setState({ hasStore: res?.enabled ?? false });
});
const fetchApiData = async () => {
useStoreStore.setState({ loadingApiKey: true });
try {
const res = await checkHasApiKey();
useStoreStore.setState({
loadingApiKey: false,
validApiKey: res?.is_valid ?? false,
hasApiKey: res?.has_api_key ?? false,
});
} catch (e) {
useStoreStore.setState({ loadingApiKey: false });
console.log(e);
}
};
fetchApiData();

View file

@ -16,7 +16,7 @@ export const useTypesStore = create<TypesStoreType>((set, get) => ({
setLoading(true);
getAll()
.then((response) => {
const data = response.data;
const data = response?.data;
useAlertStore.setState({ loading: false });
set((old) => ({
types: typesGenerator(data),

View file

@ -3,8 +3,9 @@ export type StoreStoreType = {
validApiKey: boolean;
hasApiKey: boolean;
loadingApiKey: boolean;
updateHasStore: (hasStore: boolean) => void;
checkHasStore: () => void;
updateValidApiKey: (validApiKey: boolean) => void;
updateHasApiKey: (hasApiKey: boolean) => void;
updateLoadingApiKey: (loadingApiKey: boolean) => void;
fetchApiData: () => Promise<void>;
};

View file

@ -1,5 +1,4 @@
from fastapi.testclient import TestClient
from langflow.services.deps import get_settings_service
@ -10,104 +9,3 @@ def test_prompts_settings(client: TestClient, logged_in_headers):
json_response = response.json()
prompts = json_response["prompts"]
assert set(prompts.keys()) == set(settings_service.settings.PROMPTS)
def test_prompt_template(client: TestClient, logged_in_headers):
response = client.get("api/v1/all", headers=logged_in_headers)
assert response.status_code == 200
json_response = response.json()
prompts = json_response["prompts"]
prompt = prompts["PromptTemplate"]
template = prompt["template"]
assert template["input_variables"] == {
"required": True,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "input_variables",
"type": "str",
"list": True,
"advanced": False,
"info": "",
"fileTypes": [],
}
assert template["output_parser"] == {
"required": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "output_parser",
"type": "BaseOutputParser",
"list": False,
"advanced": False,
"info": "",
"fileTypes": [],
}
assert template["partial_variables"] == {
"required": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
"password": False,
"name": "partial_variables",
"type": "dict",
"list": False,
"advanced": False,
"info": "",
"fileTypes": [],
}
assert template["template"] == {
"required": True,
"dynamic": True,
"placeholder": "",
"show": True,
"multiline": True,
"password": False,
"name": "template",
"type": "prompt",
"list": False,
"advanced": False,
"info": "",
"fileTypes": [],
}
assert template["template_format"] == {
"required": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
"value": "f-string",
"password": False,
"name": "template_format",
"type": "str",
"list": False,
"advanced": False,
"info": "",
"fileTypes": [],
}
assert template["validate_template"] == {
"required": False,
"dynamic": True,
"placeholder": "",
"show": False,
"multiline": False,
"value": False,
"password": False,
"name": "validate_template",
"type": "bool",
"list": False,
"advanced": False,
"info": "",
"fileTypes": [],
}