mirror of
https://github.com/hwchase17/langchain.git
synced 2026-02-15 01:28:57 +00:00
Compare commits
16 Commits
langchain-
...
langchain=
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d0c1d2dc9 | ||
|
|
a7296bddc2 | ||
|
|
c9473367b1 | ||
|
|
f77659463a | ||
|
|
ccdaf14eff | ||
|
|
cacdf96f9c | ||
|
|
36ee083753 | ||
|
|
e8a21146d3 | ||
|
|
a0958c0607 | ||
|
|
620b118c70 | ||
|
|
888fbc07b5 | ||
|
|
ab2d7821a7 | ||
|
|
6fc7610b1c | ||
|
|
0da5078cad | ||
|
|
d0728b0ba0 | ||
|
|
9224027e45 |
@@ -78,7 +78,7 @@ def _load_module_members(module_path: str, namespace: str) -> ModuleMembers:
|
||||
continue
|
||||
|
||||
if inspect.isclass(type_):
|
||||
# The clasification of the class is used to select a template
|
||||
# The type of the class is used to select a template
|
||||
# for the object when rendering the documentation.
|
||||
# See `templates` directory for defined templates.
|
||||
# This is a hacky solution to distinguish between different
|
||||
|
||||
@@ -821,7 +821,7 @@ We recommend this method as a starting point when working with structured output
|
||||
- If multiple underlying techniques are supported, you can supply a `method` parameter to
|
||||
[toggle which one is used](/docs/how_to/structured_output/#advanced-specifying-the-method-for-structuring-outputs).
|
||||
|
||||
You may want or need to use other techiniques if:
|
||||
You may want or need to use other techniques if:
|
||||
|
||||
- The chat model you are using does not support tool calling.
|
||||
- You are working with very complex schemas and the model is having trouble generating outputs that conform.
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
"\n",
|
||||
"Make sure you have the integration packages installed for any model providers you want to support. E.g. you should have `langchain-openai` installed to init an OpenAI model.\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
":::info Requires ``langchain >= 0.2.8``\n",
|
||||
"\n",
|
||||
"This functionality was added in ``langchain-core == 0.2.8``. Please make sure your package is up to date.\n",
|
||||
"\n",
|
||||
":::"
|
||||
]
|
||||
},
|
||||
@@ -25,7 +31,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install -qU langchain langchain-openai langchain-anthropic langchain-google-vertexai"
|
||||
"%pip install -qU langchain>=0.2.8 langchain-openai langchain-anthropic langchain-google-vertexai"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -76,32 +82,6 @@
|
||||
"print(\"Gemini 1.5: \" + gemini_15.invoke(\"what's your name\").content + \"\\n\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "fff9a4c8-b6ee-4a1a-8d3d-0ecaa312d4ed",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Simple config example"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "75c25d39-bf47-4b51-a6c6-64d9c572bfd6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"user_config = {\n",
|
||||
" \"model\": \"...user-specified...\",\n",
|
||||
" \"model_provider\": \"...user-specified...\",\n",
|
||||
" \"temperature\": 0,\n",
|
||||
" \"max_tokens\": 1000,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"llm = init_chat_model(**user_config)\n",
|
||||
"llm.invoke(\"what's your name\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f811f219-5e78-4b62-b495-915d52a22532",
|
||||
@@ -125,12 +105,215 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "da07b5c0-d2e6-42e4-bfcd-2efcfaae6221",
|
||||
"cell_type": "markdown",
|
||||
"id": "476a44db-c50d-4846-951d-0f1c9ba8bbaa",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
"source": [
|
||||
"## Creating a configurable model\n",
|
||||
"\n",
|
||||
"You can also create a runtime-configurable model by specifying `configurable_fields`. If you don't specify a `model` value, then \"model\" and \"model_provider\" be configurable by default."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "6c037f27-12d7-4e83-811e-4245c0e3ba58",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=\"I'm an AI language model created by OpenAI, and I don't have a personal name. You can call me Assistant or any other name you prefer! How can I assist you today?\", response_metadata={'token_usage': {'completion_tokens': 37, 'prompt_tokens': 11, 'total_tokens': 48}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_d576307f90', 'finish_reason': 'stop', 'logprobs': None}, id='run-5428ab5c-b5c0-46de-9946-5d4ca40dbdc8-0', usage_metadata={'input_tokens': 11, 'output_tokens': 37, 'total_tokens': 48})"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"configurable_model = init_chat_model(temperature=0)\n",
|
||||
"\n",
|
||||
"configurable_model.invoke(\n",
|
||||
" \"what's your name\", config={\"configurable\": {\"model\": \"gpt-4o\"}}\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "321e3036-abd2-4e1f-bcc6-606efd036954",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=\"My name is Claude. It's nice to meet you!\", response_metadata={'id': 'msg_012XvotUJ3kGLXJUWKBVxJUi', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 11, 'output_tokens': 15}}, id='run-1ad1eefe-f1c6-4244-8bc6-90e2cb7ee554-0', usage_metadata={'input_tokens': 11, 'output_tokens': 15, 'total_tokens': 26})"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"configurable_model.invoke(\n",
|
||||
" \"what's your name\", config={\"configurable\": {\"model\": \"claude-3-5-sonnet-20240620\"}}\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7f3b3d4a-4066-45e4-8297-ea81ac8e70b7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Configurable model with default values\n",
|
||||
"\n",
|
||||
"We can create a configurable model with default model values, specify which parameters are configurable, and add prefixes to configurable params:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "814a2289-d0db-401e-b555-d5116112b413",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=\"I'm an AI language model created by OpenAI, and I don't have a personal name. You can call me Assistant or any other name you prefer! How can I assist you today?\", response_metadata={'token_usage': {'completion_tokens': 37, 'prompt_tokens': 11, 'total_tokens': 48}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_ce0793330f', 'finish_reason': 'stop', 'logprobs': None}, id='run-3923e328-7715-4cd6-b215-98e4b6bf7c9d-0', usage_metadata={'input_tokens': 11, 'output_tokens': 37, 'total_tokens': 48})"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"first_llm = init_chat_model(\n",
|
||||
" model=\"gpt-4o\",\n",
|
||||
" temperature=0,\n",
|
||||
" configurable_fields=(\"model\", \"model_provider\", \"temperature\", \"max_tokens\"),\n",
|
||||
" config_prefix=\"first\", # useful when you have a chain with multiple models\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"first_llm.invoke(\"what's your name\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "6c8755ba-c001-4f5a-a497-be3f1db83244",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content=\"My name is Claude. It's nice to meet you!\", response_metadata={'id': 'msg_01RyYR64DoMPNCfHeNnroMXm', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 11, 'output_tokens': 15}}, id='run-22446159-3723-43e6-88df-b84797e7751d-0', usage_metadata={'input_tokens': 11, 'output_tokens': 15, 'total_tokens': 26})"
|
||||
]
|
||||
},
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"first_llm.invoke(\n",
|
||||
" \"what's your name\",\n",
|
||||
" config={\n",
|
||||
" \"configurable\": {\n",
|
||||
" \"first_model\": \"claude-3-5-sonnet-20240620\",\n",
|
||||
" \"first_temperature\": 0.5,\n",
|
||||
" \"first_max_tokens\": 100,\n",
|
||||
" }\n",
|
||||
" },\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0072b1a3-7e44-4b4e-8b07-efe1ba91a689",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Using a configurable model declaratively\n",
|
||||
"\n",
|
||||
"We can call declarative operations like `bind_tools`, `with_structured_output`, `with_configurable`, etc. on a configurable model and chain a configurable model in the same way that we would a regularly instantiated chat model object."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "067dabee-1050-4110-ae24-c48eba01e13b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'name': 'GetPopulation',\n",
|
||||
" 'args': {'location': 'Los Angeles, CA'},\n",
|
||||
" 'id': 'call_sYT3PFMufHGWJD32Hi2CTNUP'},\n",
|
||||
" {'name': 'GetPopulation',\n",
|
||||
" 'args': {'location': 'New York, NY'},\n",
|
||||
" 'id': 'call_j1qjhxRnD3ffQmRyqjlI1Lnk'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class GetWeather(BaseModel):\n",
|
||||
" \"\"\"Get the current weather in a given location\"\"\"\n",
|
||||
"\n",
|
||||
" location: str = Field(..., description=\"The city and state, e.g. San Francisco, CA\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class GetPopulation(BaseModel):\n",
|
||||
" \"\"\"Get the current population in a given location\"\"\"\n",
|
||||
"\n",
|
||||
" location: str = Field(..., description=\"The city and state, e.g. San Francisco, CA\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"llm = init_chat_model(temperature=0)\n",
|
||||
"llm_with_tools = llm.bind_tools([GetWeather, GetPopulation])\n",
|
||||
"\n",
|
||||
"llm_with_tools.invoke(\n",
|
||||
" \"what's bigger in 2024 LA or NYC\", config={\"configurable\": {\"model\": \"gpt-4o\"}}\n",
|
||||
").tool_calls"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "e57dfe9f-cd24-4e37-9ce9-ccf8daf78f89",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'name': 'GetPopulation',\n",
|
||||
" 'args': {'location': 'Los Angeles, CA'},\n",
|
||||
" 'id': 'toolu_01CxEHxKtVbLBrvzFS7GQ5xR'},\n",
|
||||
" {'name': 'GetPopulation',\n",
|
||||
" 'args': {'location': 'New York City, NY'},\n",
|
||||
" 'id': 'toolu_013A79qt5toWSsKunFBDZd5S'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"llm_with_tools.invoke(\n",
|
||||
" \"what's bigger in 2024 LA or NYC\",\n",
|
||||
" config={\"configurable\": {\"model\": \"claude-3-5-sonnet-20240620\"}},\n",
|
||||
").tool_calls"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -149,7 +332,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.1"
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
"id": "32b1a992-8997-4c98-8eb2-c9fe9431b799",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Alternatively, we can add typing information via [Runnable.with_types](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable.with_types):"
|
||||
"Alternatively, the schema can be fully specified by directly passing the desired [args_schema](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.BaseTool.html#langchain_core.tools.BaseTool.args_schema) for the tool:"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -190,10 +190,18 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"as_tool = runnable.with_types(input_type=Args).as_tool(\n",
|
||||
" name=\"My tool\",\n",
|
||||
" description=\"Explanation of when to use tool.\",\n",
|
||||
")"
|
||||
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class GSchema(BaseModel):\n",
|
||||
" \"\"\"Apply a function to an integer and list of integers.\"\"\"\n",
|
||||
"\n",
|
||||
" a: int = Field(..., description=\"Integer\")\n",
|
||||
" b: List[int] = Field(..., description=\"List of ints\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"runnable = RunnableLambda(g)\n",
|
||||
"as_tool = runnable.as_tool(GSchema)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -768,13 +768,189 @@
|
||||
"\n",
|
||||
"get_weather_tool.invoke({\"city\": \"foobar\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1a8d8383-11b3-445e-956f-df4e96995e00",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Returning artifacts of Tool execution\n",
|
||||
"\n",
|
||||
"Sometimes there are artifacts of a tool's execution that we want to make accessible to downstream components in our chain or agent, but that we don't want to expose to the model itself. For example if a tool returns custom objects like Documents, we may want to pass some view or metadata about this output to the model without passing the raw output to the model. At the same time, we may want to be able to access this full output elsewhere, for example in downstream tools.\n",
|
||||
"\n",
|
||||
"The Tool and [ToolMessage](https://api.python.langchain.com/en/latest/messages/langchain_core.messages.tool.ToolMessage.html) interfaces make it possible to distinguish between the parts of the tool output meant for the model (this is the ToolMessage.content) and those parts which are meant for use outside the model (ToolMessage.artifact).\n",
|
||||
"\n",
|
||||
":::info Requires ``langchain-core >= 0.2.19``\n",
|
||||
"\n",
|
||||
"This functionality was added in ``langchain-core == 0.2.19``. Please make sure your package is up to date.\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
"If we want our tool to distinguish between message content and other artifacts, we need to specify `response_format=\"content_and_artifact\"` when defining our tool and make sure that we return a tuple of (content, artifact):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "14905425-0334-43a0-9de9-5bcf622ede0e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"from typing import List, Tuple\n",
|
||||
"\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(response_format=\"content_and_artifact\")\n",
|
||||
"def generate_random_ints(min: int, max: int, size: int) -> Tuple[str, List[int]]:\n",
|
||||
" \"\"\"Generate size random ints in the range [min, max].\"\"\"\n",
|
||||
" array = [random.randint(min, max) for _ in range(size)]\n",
|
||||
" content = f\"Successfully generated array of {size} random ints in [{min}, {max}].\"\n",
|
||||
" return content, array"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "49f057a6-8938-43ea-8faf-ae41e797ceb8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we invoke our tool directly with the tool arguments, we'll get back just the content part of the output:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "0f2e1528-404b-46e6-b87c-f0957c4b9217",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'Successfully generated array of 10 random ints in [0, 9].'"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"generate_random_ints.invoke({\"min\": 0, \"max\": 9, \"size\": 10})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1e62ebba-1737-4b97-b61a-7313ade4e8c2",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we invoke our tool with a ToolCall (like the ones generated by tool-calling models), we'll get back a ToolMessage that contains both the content and artifact generated by the Tool:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "cc197777-26eb-46b3-a83b-c2ce116c6311",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"ToolMessage(content='Successfully generated array of 10 random ints in [0, 9].', name='generate_random_ints', tool_call_id='123', artifact=[1, 4, 2, 5, 3, 9, 0, 4, 7, 7])"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"generate_random_ints.invoke(\n",
|
||||
" {\n",
|
||||
" \"name\": \"generate_random_ints\",\n",
|
||||
" \"args\": {\"min\": 0, \"max\": 9, \"size\": 10},\n",
|
||||
" \"id\": \"123\", # required\n",
|
||||
" \"type\": \"tool_call\", # required\n",
|
||||
" }\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dfdc1040-bf25-4790-b4c3-59452db84e11",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can do the same when subclassing BaseTool:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "fe1a09d1-378b-4b91-bb5e-0697c3d7eb92",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.tools import BaseTool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class GenerateRandomFloats(BaseTool):\n",
|
||||
" name: str = \"generate_random_floats\"\n",
|
||||
" description: str = \"Generate size random floats in the range [min, max].\"\n",
|
||||
" response_format: str = \"content_and_artifact\"\n",
|
||||
"\n",
|
||||
" ndigits: int = 2\n",
|
||||
"\n",
|
||||
" def _run(self, min: float, max: float, size: int) -> Tuple[str, List[float]]:\n",
|
||||
" range_ = max - min\n",
|
||||
" array = [\n",
|
||||
" round(min + (range_ * random.random()), ndigits=self.ndigits)\n",
|
||||
" for _ in range(size)\n",
|
||||
" ]\n",
|
||||
" content = f\"Generated {size} floats in [{min}, {max}], rounded to {self.ndigits} decimals.\"\n",
|
||||
" return content, array\n",
|
||||
"\n",
|
||||
" # Optionally define an equivalent async method\n",
|
||||
"\n",
|
||||
" # async def _arun(self, min: float, max: float, size: int) -> Tuple[str, List[float]]:\n",
|
||||
" # ..."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "8c3d16f6-1c4a-48ab-b05a-38547c592e79",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"ToolMessage(content='Generated 3 floats in [0.1, 3.3333], rounded to 4 decimals.', name='generate_random_floats', tool_call_id='123', artifact=[1.4277, 0.7578, 2.4871])"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"rand_gen = GenerateRandomFloats(ndigits=4)\n",
|
||||
"\n",
|
||||
"rand_gen.invoke(\n",
|
||||
" {\n",
|
||||
" \"name\": \"generate_random_floats\",\n",
|
||||
" \"args\": {\"min\": 0.1, \"max\": 3.3333, \"size\": 3},\n",
|
||||
" \"id\": \"123\",\n",
|
||||
" \"type\": \"tool_call\",\n",
|
||||
" }\n",
|
||||
")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"display_name": "poetry-venv-311",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
"name": "poetry-venv-311"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
@@ -786,7 +962,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.4"
|
||||
"version": "3.11.9"
|
||||
},
|
||||
"vscode": {
|
||||
"interpreter": {
|
||||
|
||||
@@ -84,7 +84,7 @@ These are the core building blocks you can use when building applications.
|
||||
- [How to: use chat model to call tools](/docs/how_to/tool_calling)
|
||||
- [How to: stream tool calls](/docs/how_to/tool_streaming)
|
||||
- [How to: few shot prompt tool behavior](/docs/how_to/tools_few_shot)
|
||||
- [How to: bind model-specific formated tools](/docs/how_to/tools_model_specific)
|
||||
- [How to: bind model-specific formatted tools](/docs/how_to/tools_model_specific)
|
||||
- [How to: force a specific tool call](/docs/how_to/tool_choice)
|
||||
- [How to: init any model in one line](/docs/how_to/chat_models_universal_init/)
|
||||
|
||||
@@ -197,6 +197,7 @@ LangChain [Tools](/docs/concepts/#tools) contain a description of the tool (to p
|
||||
- [How to: disable parallel tool calling](/docs/how_to/tool_choice)
|
||||
- [How to: access the `RunnableConfig` object within a custom tool](/docs/how_to/tool_configure)
|
||||
- [How to: stream events from child runs within a custom tool](/docs/how_to/tool_stream_events)
|
||||
- [How to: return extra artifacts from a tool](/docs/how_to/tool_artifacts/)
|
||||
|
||||
### Multimodal
|
||||
|
||||
|
||||
@@ -63,6 +63,38 @@
|
||||
"Notice that if the contents of one of the messages to merge is a list of content blocks then the merged message will have a list of content blocks. And if both messages to merge have string contents then those are concatenated with a newline character."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "11f7e8d3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The `merge_message_runs` utility also works with messages composed together using the overloaded `+` operation:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b51855c5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"messages = (\n",
|
||||
" SystemMessage(\"you're a good assistant.\")\n",
|
||||
" + SystemMessage(\"you always respond with a joke.\")\n",
|
||||
" + HumanMessage([{\"type\": \"text\", \"text\": \"i wonder why it's called langchain\"}])\n",
|
||||
" + HumanMessage(\"and who is harrison chasing anyways\")\n",
|
||||
" + AIMessage(\n",
|
||||
" 'Well, I guess they thought \"WordRope\" and \"SentenceString\" just didn\\'t have the same ring to it!'\n",
|
||||
" )\n",
|
||||
" + AIMessage(\n",
|
||||
" \"Why, he's probably chasing after the last cup of coffee in the office!\"\n",
|
||||
" )\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"merged = merge_message_runs(messages)\n",
|
||||
"print(\"\\n\\n\".join([repr(x) for x in merged]))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1b2eee74-71c8-4168-b968-bca580c25d18",
|
||||
|
||||
395
docs/docs/how_to/tool_artifacts.ipynb
Normal file
395
docs/docs/how_to/tool_artifacts.ipynb
Normal file
@@ -0,0 +1,395 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "503e36ae-ca62-4f8a-880c-4fe78ff5df93",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to return extra artifacts from a tool\n",
|
||||
"\n",
|
||||
":::info Prerequisites\n",
|
||||
"This guide assumes familiarity with the following concepts:\n",
|
||||
"\n",
|
||||
"- [Tools](/docs/concepts/#tools)\n",
|
||||
"- [Function/tool calling](/docs/concepts/#functiontool-calling)\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
"Tools are utilities that can be called by a model, and whose outputs are designed to be fed back to a model. Sometimes, however, there are artifacts of a tool's execution that we want to make accessible to downstream components in our chain or agent, but that we don't want to expose to the model itself. For example if a tool returns a custom object, a dataframe or an image, we may want to pass some metadata about this output to the model without passing the actual output to the model. At the same time, we may want to be able to access this full output elsewhere, for example in downstream tools.\n",
|
||||
"\n",
|
||||
"The Tool and [ToolMessage](https://api.python.langchain.com/en/latest/messages/langchain_core.messages.tool.ToolMessage.html) interfaces make it possible to distinguish between the parts of the tool output meant for the model (this is the ToolMessage.content) and those parts which are meant for use outside the model (ToolMessage.artifact).\n",
|
||||
"\n",
|
||||
":::info Requires ``langchain-core >= 0.2.19``\n",
|
||||
"\n",
|
||||
"This functionality was added in ``langchain-core == 0.2.19``. Please make sure your package is up to date.\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
"## Defining the tool\n",
|
||||
"\n",
|
||||
"If we want our tool to distinguish between message content and other artifacts, we need to specify `response_format=\"content_and_artifact\"` when defining our tool and make sure that we return a tuple of (content, artifact):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "762b9199-885f-4946-9c98-cc54d72b0d76",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install -qU \"langchain-core>=0.2.19\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "b9eb179d-1f41-4748-9866-b3d3e8c73cd0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"from typing import List, Tuple\n",
|
||||
"\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(response_format=\"content_and_artifact\")\n",
|
||||
"def generate_random_ints(min: int, max: int, size: int) -> Tuple[str, List[int]]:\n",
|
||||
" \"\"\"Generate size random ints in the range [min, max].\"\"\"\n",
|
||||
" array = [random.randint(min, max) for _ in range(size)]\n",
|
||||
" content = f\"Successfully generated array of {size} random ints in [{min}, {max}].\"\n",
|
||||
" return content, array"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0ab05d25-af4a-4e5a-afe2-f090416d7ee7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Invoking the tool with ToolCall\n",
|
||||
"\n",
|
||||
"If we directly invoke our tool with just the tool arguments, you'll notice that we only get back the content part of the Tool output:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "5e7d5e77-3102-4a59-8ade-e4e699dd1817",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'Successfully generated array of 10 random ints in [0, 9].'"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Failed to batch ingest runs: LangSmithRateLimitError('Rate limit exceeded for https://api.smith.langchain.com/runs/batch. HTTPError(\\'429 Client Error: Too Many Requests for url: https://api.smith.langchain.com/runs/batch\\', \\'{\"detail\":\"Monthly unique traces usage limit exceeded\"}\\')')\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"generate_random_ints.invoke({\"min\": 0, \"max\": 9, \"size\": 10})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "30db7228-f04c-489e-afda-9a572eaa90a1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In order to get back both the content and the artifact, we need to invoke our model with a ToolCall (which is just a dictionary with \"name\", \"args\", \"id\" and \"type\" keys), which has additional info needed to generate a ToolMessage like the tool call ID:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "da1d939d-a900-4b01-92aa-d19011a6b034",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"ToolMessage(content='Successfully generated array of 10 random ints in [0, 9].', name='generate_random_ints', tool_call_id='123', artifact=[2, 8, 0, 6, 0, 0, 1, 5, 0, 0])"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"generate_random_ints.invoke(\n",
|
||||
" {\n",
|
||||
" \"name\": \"generate_random_ints\",\n",
|
||||
" \"args\": {\"min\": 0, \"max\": 9, \"size\": 10},\n",
|
||||
" \"id\": \"123\", # required\n",
|
||||
" \"type\": \"tool_call\", # required\n",
|
||||
" }\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a3cfc03d-020b-42c7-b0f8-c824af19e45e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Using with a model\n",
|
||||
"\n",
|
||||
"With a [tool-calling model](/docs/how_to/tool_calling/), we can easily use a model to call our Tool and generate ToolMessages:\n",
|
||||
"\n",
|
||||
"```{=mdx}\n",
|
||||
"import ChatModelTabs from \"@theme/ChatModelTabs\";\n",
|
||||
"\n",
|
||||
"<ChatModelTabs\n",
|
||||
" customVarName=\"llm\"\n",
|
||||
"/>\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "74de0286-b003-4b48-9cdd-ecab435515ca",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# | echo: false\n",
|
||||
"# | output: false\n",
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"\n",
|
||||
"llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\", temperature=0)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "8a67424b-d19c-43df-ac7b-690bca42146c",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'name': 'generate_random_ints',\n",
|
||||
" 'args': {'min': 1, 'max': 24, 'size': 6},\n",
|
||||
" 'id': 'toolu_01EtALY3Wz1DVYhv1TLvZGvE',\n",
|
||||
" 'type': 'tool_call'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"llm_with_tools = llm.bind_tools([generate_random_ints])\n",
|
||||
"\n",
|
||||
"ai_msg = llm_with_tools.invoke(\"generate 6 positive ints less than 25\")\n",
|
||||
"ai_msg.tool_calls"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "00c4e906-3ca8-41e8-a0be-65cb0db7d574",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"ToolMessage(content='Successfully generated array of 6 random ints in [1, 24].', name='generate_random_ints', tool_call_id='toolu_01EtALY3Wz1DVYhv1TLvZGvE', artifact=[2, 20, 23, 8, 1, 15])"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"generate_random_ints.invoke(ai_msg.tool_calls[0])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ddef2690-70de-4542-ab20-2337f77f3e46",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we just pass in the tool call args, we'll only get back the content:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "f4a6c9a6-0ffc-4b0e-a59f-f3c3d69d824d",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'Successfully generated array of 6 random ints in [1, 24].'"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"generate_random_ints.invoke(ai_msg.tool_calls[0][\"args\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "98d6443b-ff41-4d91-8523-b6274fc74ee5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we wanted to declaratively create a chain, we could do this:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "eb55ec23-95a4-464e-b886-d9679bf3aaa2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[ToolMessage(content='Successfully generated array of 1 random ints in [1, 5].', name='generate_random_ints', tool_call_id='toolu_01FwYhnkwDPJPbKdGq4ng6uD', artifact=[5])]"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from operator import attrgetter\n",
|
||||
"\n",
|
||||
"chain = llm_with_tools | attrgetter(\"tool_calls\") | generate_random_ints.map()\n",
|
||||
"\n",
|
||||
"chain.invoke(\"give me a random number between 1 and 5\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4df46be2-babb-4bfe-a641-91cd3d03ffaf",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Creating from BaseTool class\n",
|
||||
"\n",
|
||||
"If you want to create a BaseTool object directly, instead of decorating a function with `@tool`, you can do so like this:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "9a9129e1-6aee-4a10-ad57-62ef3bf0276c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.tools import BaseTool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class GenerateRandomFloats(BaseTool):\n",
|
||||
" name: str = \"generate_random_floats\"\n",
|
||||
" description: str = \"Generate size random floats in the range [min, max].\"\n",
|
||||
" response_format: str = \"content_and_artifact\"\n",
|
||||
"\n",
|
||||
" ndigits: int = 2\n",
|
||||
"\n",
|
||||
" def _run(self, min: float, max: float, size: int) -> Tuple[str, List[float]]:\n",
|
||||
" range_ = max - min\n",
|
||||
" array = [\n",
|
||||
" round(min + (range_ * random.random()), ndigits=self.ndigits)\n",
|
||||
" for _ in range(size)\n",
|
||||
" ]\n",
|
||||
" content = f\"Generated {size} floats in [{min}, {max}], rounded to {self.ndigits} decimals.\"\n",
|
||||
" return content, array\n",
|
||||
"\n",
|
||||
" # Optionally define an equivalent async method\n",
|
||||
"\n",
|
||||
" # async def _arun(self, min: float, max: float, size: int) -> Tuple[str, List[float]]:\n",
|
||||
" # ..."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "d7322619-f420-4b29-8ee5-023e693d0179",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'Generated 3 floats in [0.1, 3.3333], rounded to 4 decimals.'"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"rand_gen = GenerateRandomFloats(ndigits=4)\n",
|
||||
"rand_gen.invoke({\"min\": 0.1, \"max\": 3.3333, \"size\": 3})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "0892f277-23a6-4bb8-a0e9-59f533ac9750",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"ToolMessage(content='Generated 3 floats in [0.1, 3.3333], rounded to 4 decimals.', name='generate_random_floats', tool_call_id='123', artifact=[1.5789, 2.464, 2.2719])"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"rand_gen.invoke(\n",
|
||||
" {\n",
|
||||
" \"name\": \"generate_random_floats\",\n",
|
||||
" \"args\": {\"min\": 0.1, \"max\": 3.3333, \"size\": 3},\n",
|
||||
" \"id\": \"123\",\n",
|
||||
" \"type\": \"tool_call\",\n",
|
||||
" }\n",
|
||||
")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "poetry-venv-311",
|
||||
"language": "python",
|
||||
"name": "poetry-venv-311"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -6,12 +6,20 @@
|
||||
"source": [
|
||||
"# How to pass tool outputs to the model\n",
|
||||
"\n",
|
||||
"If we're using the model-generated tool invocations to actually call tools and want to pass the tool results back to the model, we can do so using `ToolMessage`s. First, let's define our tools and our model."
|
||||
":::info Prerequisites\n",
|
||||
"This guide assumes familiarity with the following concepts:\n",
|
||||
"\n",
|
||||
"- [Tools](/docs/concepts/#tools)\n",
|
||||
"- [Function/tool calling](/docs/concepts/#functiontool-calling)\n",
|
||||
"\n",
|
||||
":::\n",
|
||||
"\n",
|
||||
"If we're using the model-generated tool invocations to actually call tools and want to pass the tool results back to the model, we can do so using `ToolMessage`s and `ToolCall`s. First, let's define our tools and our model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -35,7 +43,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -54,25 +62,32 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now we can use ``ToolMessage`` to pass back the output of the tool calls to the model."
|
||||
"The nice thing about Tools is that if we invoke them with a ToolCall, we'll automatically get back a ToolMessage that can be fed back to the model: \n",
|
||||
"\n",
|
||||
":::info Requires ``langchain-core >= 0.2.19``\n",
|
||||
"\n",
|
||||
"This functionality was added in ``langchain-core == 0.2.19``. Please make sure your package is up to date.\n",
|
||||
"\n",
|
||||
":::"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[HumanMessage(content='What is 3 * 12? Also, what is 11 + 49?'),\n",
|
||||
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_svc2GLSxNFALbaCAbSjMI9J8', 'function': {'arguments': '{\"a\": 3, \"b\": 12}', 'name': 'Multiply'}, 'type': 'function'}, {'id': 'call_r8jxte3zW6h3MEGV3zH2qzFh', 'function': {'arguments': '{\"a\": 11, \"b\": 49}', 'name': 'Add'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 50, 'prompt_tokens': 105, 'total_tokens': 155}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': 'fp_d9767fc5b9', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-a79ad1dd-95f1-4a46-b688-4c83f327a7b3-0', tool_calls=[{'name': 'Multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_svc2GLSxNFALbaCAbSjMI9J8'}, {'name': 'Add', 'args': {'a': 11, 'b': 49}, 'id': 'call_r8jxte3zW6h3MEGV3zH2qzFh'}]),\n",
|
||||
" ToolMessage(content='36', tool_call_id='call_svc2GLSxNFALbaCAbSjMI9J8'),\n",
|
||||
" ToolMessage(content='60', tool_call_id='call_r8jxte3zW6h3MEGV3zH2qzFh')]"
|
||||
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Smg3NHJNxrKfAmd4f9GkaYn3', 'function': {'arguments': '{\"a\": 3, \"b\": 12}', 'name': 'multiply'}, 'type': 'function'}, {'id': 'call_55K1C0DmH6U5qh810gW34xZ0', 'function': {'arguments': '{\"a\": 11, \"b\": 49}', 'name': 'add'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 49, 'prompt_tokens': 88, 'total_tokens': 137}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-56657feb-96dd-456c-ab8e-1857eab2ade0-0', tool_calls=[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': 'call_Smg3NHJNxrKfAmd4f9GkaYn3', 'type': 'tool_call'}, {'name': 'add', 'args': {'a': 11, 'b': 49}, 'id': 'call_55K1C0DmH6U5qh810gW34xZ0', 'type': 'tool_call'}], usage_metadata={'input_tokens': 88, 'output_tokens': 49, 'total_tokens': 137}),\n",
|
||||
" ToolMessage(content='36', name='multiply', tool_call_id='call_Smg3NHJNxrKfAmd4f9GkaYn3'),\n",
|
||||
" ToolMessage(content='60', name='add', tool_call_id='call_55K1C0DmH6U5qh810gW34xZ0')]"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
@@ -85,24 +100,25 @@
|
||||
"messages.append(ai_msg)\n",
|
||||
"for tool_call in ai_msg.tool_calls:\n",
|
||||
" selected_tool = {\"add\": add, \"multiply\": multiply}[tool_call[\"name\"].lower()]\n",
|
||||
" tool_output = selected_tool.invoke(tool_call[\"args\"])\n",
|
||||
" messages.append(ToolMessage(tool_output, tool_call_id=tool_call[\"id\"]))\n",
|
||||
" tool_msg = selected_tool.invoke(tool_call)\n",
|
||||
" messages.append(tool_msg)\n",
|
||||
"messages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"AIMessage(content='3 * 12 is 36 and 11 + 49 is 60.', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 171, 'total_tokens': 189}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': 'fp_d9767fc5b9', 'finish_reason': 'stop', 'logprobs': None}, id='run-20b52149-e00d-48ea-97cf-f8de7a255f8c-0')"
|
||||
"AIMessage(content='3 * 12 is 36 and 11 + 49 is 60.', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 153, 'total_tokens': 171}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-ba5032f0-f773-406d-a408-8314e66511d0-0', usage_metadata={'input_tokens': 153, 'output_tokens': 18, 'total_tokens': 171})"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
@@ -118,10 +134,24 @@
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "poetry-venv-311",
|
||||
"language": "python",
|
||||
"name": "poetry-venv-311"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Install the package\n",
|
||||
"%pip install --upgrade --quiet dashscope"
|
||||
"%pip install --upgrade --quiet langchain-community dashscope"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -61,7 +61,7 @@ When ready to deploy, you can self-host models with NVIDIA NIM—which is includ
|
||||
```python
|
||||
from langchain_nvidia_ai_endpoints import ChatNVIDIA, NVIDIAEmbeddings, NVIDIARerank
|
||||
|
||||
# connect to an chat NIM running at localhost:8000, specifyig a specific model
|
||||
# connect to a chat NIM running at localhost:8000, specifying a model
|
||||
llm = ChatNVIDIA(base_url="http://localhost:8000/v1", model="meta/llama3-8b-instruct")
|
||||
|
||||
# connect to an embedding NIM running at localhost:8080
|
||||
|
||||
@@ -202,7 +202,7 @@ Prem Templates are also available for Streaming too.
|
||||
|
||||
## Prem Embeddings
|
||||
|
||||
In this section we are going to dicuss how we can get access to different embedding model using `PremEmbeddings` with LangChain. Lets start by importing our modules and setting our API Key.
|
||||
In this section we cover how we can get access to different embedding models using `PremEmbeddings` with LangChain. Let's start by importing our modules and setting our API Key.
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_API_BASE = "https://api.endpoints.anyscale.com/v1"
|
||||
DEFAULT_MODEL = "meta-llama/Llama-2-7b-chat-hf"
|
||||
DEFAULT_MODEL = "meta-llama/Meta-Llama-3-8B-Instruct"
|
||||
|
||||
|
||||
class ChatAnyscale(ChatOpenAI):
|
||||
|
||||
@@ -60,7 +60,7 @@ class HuggingFaceCrossEncoder(BaseModel, BaseCrossEncoder):
|
||||
List of scores, one for each pair.
|
||||
"""
|
||||
scores = self.client.predict(text_pairs)
|
||||
# Somes models e.g bert-multilingual-passage-reranking-msmarco
|
||||
# Some models e.g bert-multilingual-passage-reranking-msmarco
|
||||
# gives two score not_relevant and relevant as compare with the query.
|
||||
if len(scores.shape) > 1: # we are going to get the relevant scores
|
||||
scores = map(lambda x: x[1], scores)
|
||||
|
||||
@@ -60,7 +60,7 @@ class AscendEmbeddings(Embeddings, BaseModel):
|
||||
raise ValueError("model_path is required")
|
||||
if not os.access(values["model_path"], os.F_OK):
|
||||
raise FileNotFoundError(
|
||||
f"Unabled to find valid model path in [{values['model_path']}]"
|
||||
f"Unable to find valid model path in [{values['model_path']}]"
|
||||
)
|
||||
try:
|
||||
import torch_npu
|
||||
|
||||
@@ -72,7 +72,7 @@ class SQLStore(BaseStore[str, bytes]):
|
||||
from langchain_rag.storage import SQLStore
|
||||
|
||||
# Instantiate the SQLStore with the root path
|
||||
sql_store = SQLStore(namespace="test", db_url="sqllite://:memory:")
|
||||
sql_store = SQLStore(namespace="test", db_url="sqlite://:memory:")
|
||||
|
||||
# Set values for keys
|
||||
sql_store.mset([("key1", b"value1"), ("key2", b"value2")])
|
||||
|
||||
@@ -9,7 +9,7 @@ from langchain_community.tools.zenguard.tool import Detector, ZenGuardTool
|
||||
@pytest.fixture()
|
||||
def zenguard_tool() -> ZenGuardTool:
|
||||
if os.getenv("ZENGUARD_API_KEY") is None:
|
||||
raise ValueError("ZENGUARD_API_KEY is not set in environment varibale")
|
||||
raise ValueError("ZENGUARD_API_KEY is not set in environment variable")
|
||||
return ZenGuardTool()
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ PAGE_1 = """
|
||||
Hello.
|
||||
<a href="relative">Relative</a>
|
||||
<a href="/relative-base">Relative base.</a>
|
||||
<a href="http://cnn.com">Aboslute</a>
|
||||
<a href="http://cnn.com">Absolute</a>
|
||||
<a href="//same.foo">Test</a>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -15,7 +15,7 @@ PathLike = Union[str, PurePath]
|
||||
class BaseMedia(Serializable):
|
||||
"""Use to represent media content.
|
||||
|
||||
Media objets can be used to represent raw data, such as text or binary data.
|
||||
Media objects can be used to represent raw data, such as text or binary data.
|
||||
|
||||
LangChain Media objects allow associating metadata and an optional identifier
|
||||
with the content.
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
@@ -40,6 +41,7 @@ if TYPE_CHECKING:
|
||||
from langchain_text_splitters import TextSplitter
|
||||
|
||||
from langchain_core.language_models import BaseLanguageModel
|
||||
from langchain_core.prompt_values import PromptValue
|
||||
from langchain_core.runnables.base import Runnable
|
||||
|
||||
AnyMessage = Union[
|
||||
@@ -284,7 +286,7 @@ def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:
|
||||
|
||||
|
||||
def convert_to_messages(
|
||||
messages: Sequence[MessageLikeRepresentation],
|
||||
messages: Union[Iterable[MessageLikeRepresentation], PromptValue],
|
||||
) -> List[BaseMessage]:
|
||||
"""Convert a sequence of messages to a list of messages.
|
||||
|
||||
@@ -294,6 +296,11 @@ def convert_to_messages(
|
||||
Returns:
|
||||
List of messages (BaseMessages).
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from langchain_core.prompt_values import PromptValue
|
||||
|
||||
if isinstance(messages, PromptValue):
|
||||
return messages.to_messages()
|
||||
return [_convert_to_message(m) for m in messages]
|
||||
|
||||
|
||||
@@ -329,7 +336,7 @@ def _runnable_support(func: Callable) -> Callable:
|
||||
|
||||
@_runnable_support
|
||||
def filter_messages(
|
||||
messages: Sequence[MessageLikeRepresentation],
|
||||
messages: Union[Iterable[MessageLikeRepresentation], PromptValue],
|
||||
*,
|
||||
include_names: Optional[Sequence[str]] = None,
|
||||
exclude_names: Optional[Sequence[str]] = None,
|
||||
@@ -417,7 +424,7 @@ def filter_messages(
|
||||
|
||||
@_runnable_support
|
||||
def merge_message_runs(
|
||||
messages: Sequence[MessageLikeRepresentation],
|
||||
messages: Union[Iterable[MessageLikeRepresentation], PromptValue],
|
||||
) -> List[BaseMessage]:
|
||||
"""Merge consecutive Messages of the same type.
|
||||
|
||||
@@ -506,7 +513,7 @@ def merge_message_runs(
|
||||
|
||||
@_runnable_support
|
||||
def trim_messages(
|
||||
messages: Sequence[MessageLikeRepresentation],
|
||||
messages: Union[Iterable[MessageLikeRepresentation], PromptValue],
|
||||
*,
|
||||
max_tokens: int,
|
||||
token_counter: Union[
|
||||
|
||||
@@ -1327,7 +1327,7 @@ class Runnable(Generic[Input, Output], ABC):
|
||||
def with_config(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
# Sadly Unpack is not well supported by mypy so this will have to be untyped
|
||||
# Sadly Unpack is not well-supported by mypy so this will have to be untyped
|
||||
**kwargs: Any,
|
||||
) -> Runnable[Input, Output]:
|
||||
"""
|
||||
@@ -2150,6 +2150,7 @@ class Runnable(Generic[Input, Output], ABC):
|
||||
@beta_decorator.beta(message="This API is in beta and may change in the future.")
|
||||
def as_tool(
|
||||
self,
|
||||
args_schema: Optional[Type[BaseModel]] = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
@@ -2161,9 +2162,11 @@ class Runnable(Generic[Input, Output], ABC):
|
||||
``args_schema`` from a Runnable. Where possible, schemas are inferred
|
||||
from ``runnable.get_input_schema``. Alternatively (e.g., if the
|
||||
Runnable takes a dict as input and the specific dict keys are not typed),
|
||||
pass ``arg_types`` to specify the required arguments.
|
||||
the schema can be specified directly with ``args_schema``. You can also
|
||||
pass ``arg_types`` to just specify the required arguments and their types.
|
||||
|
||||
Args:
|
||||
args_schema: The schema for the tool. Defaults to None.
|
||||
name: The name of the tool. Defaults to None.
|
||||
description: The description of the tool. Defaults to None.
|
||||
arg_types: A dictionary of argument names to types. Defaults to None.
|
||||
@@ -2190,7 +2193,28 @@ class Runnable(Generic[Input, Output], ABC):
|
||||
as_tool = runnable.as_tool()
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
|
||||
``dict`` input, specifying schema:
|
||||
``dict`` input, specifying schema via ``args_schema``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field
|
||||
from langchain_core.runnables import RunnableLambda
|
||||
|
||||
def f(x: Dict[str, Any]) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
|
||||
class FSchema(BaseModel):
|
||||
\"\"\"Apply a function to an integer and list of integers.\"\"\"
|
||||
|
||||
a: int = Field(..., description="Integer")
|
||||
b: List[int] = Field(..., description="List of ints")
|
||||
|
||||
runnable = RunnableLambda(f)
|
||||
as_tool = runnable.as_tool(FSchema)
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
|
||||
``dict`` input, specifying schema via ``arg_types``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -2226,7 +2250,11 @@ class Runnable(Generic[Input, Output], ABC):
|
||||
from langchain_core.tools import convert_runnable_to_tool
|
||||
|
||||
return convert_runnable_to_tool(
|
||||
self, name=name, description=description, arg_types=arg_types
|
||||
self,
|
||||
args_schema=args_schema,
|
||||
name=name,
|
||||
description=description,
|
||||
arg_types=arg_types,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ class ChildTool(BaseTool):
|
||||
if error_to_raise:
|
||||
run_manager.on_tool_error(error_to_raise)
|
||||
raise error_to_raise
|
||||
output = _format_output(content, artifact, tool_call_id)
|
||||
output = _format_output(content, artifact, tool_call_id, self.name)
|
||||
run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
|
||||
return output
|
||||
|
||||
@@ -672,7 +672,7 @@ class ChildTool(BaseTool):
|
||||
await run_manager.on_tool_error(error_to_raise)
|
||||
raise error_to_raise
|
||||
|
||||
output = _format_output(content, artifact, tool_call_id)
|
||||
output = _format_output(content, artifact, tool_call_id, self.name)
|
||||
await run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
|
||||
return output
|
||||
|
||||
@@ -1385,7 +1385,7 @@ def _prep_run_args(
|
||||
|
||||
|
||||
def _format_output(
|
||||
content: Any, artifact: Any, tool_call_id: Optional[str]
|
||||
content: Any, artifact: Any, tool_call_id: Optional[str], name: str
|
||||
) -> Union[ToolMessage, Any]:
|
||||
if tool_call_id:
|
||||
# NOTE: This will fail to stringify lists which aren't actually content blocks
|
||||
@@ -1397,7 +1397,9 @@ def _format_output(
|
||||
and isinstance(content[0], (str, dict))
|
||||
):
|
||||
content = _stringify(content)
|
||||
return ToolMessage(content, artifact=artifact, tool_call_id=tool_call_id)
|
||||
return ToolMessage(
|
||||
content, artifact=artifact, tool_call_id=tool_call_id, name=name
|
||||
)
|
||||
else:
|
||||
return content
|
||||
|
||||
@@ -1436,11 +1438,15 @@ def _get_schema_from_runnable_and_arg_types(
|
||||
|
||||
def convert_runnable_to_tool(
|
||||
runnable: Runnable,
|
||||
args_schema: Optional[Type[BaseModel]] = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
arg_types: Optional[Dict[str, Type]] = None,
|
||||
) -> BaseTool:
|
||||
"""Convert a Runnable into a BaseTool."""
|
||||
if args_schema:
|
||||
runnable = runnable.with_types(input_type=args_schema)
|
||||
description = description or _get_description_from_runnable(runnable)
|
||||
name = name or runnable.get_name()
|
||||
|
||||
|
||||
@@ -62,7 +62,21 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Start a trace for an LLM run."""
|
||||
"""Start a trace for an LLM run.
|
||||
|
||||
Args:
|
||||
serialized: The serialized model.
|
||||
messages: The messages to start the chat with.
|
||||
run_id: The run ID.
|
||||
tags: The tags for the run. Defaults to None.
|
||||
parent_run_id: The parent run ID. Defaults to None.
|
||||
metadata: The metadata for the run. Defaults to None.
|
||||
name: The name of the run.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
chat_model_run = self._create_chat_model_run(
|
||||
serialized=serialized,
|
||||
messages=messages,
|
||||
@@ -89,7 +103,21 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Start a trace for an LLM run."""
|
||||
"""Start a trace for an LLM run.
|
||||
|
||||
Args:
|
||||
serialized: The serialized model.
|
||||
prompts: The prompts to start the LLM with.
|
||||
run_id: The run ID.
|
||||
tags: The tags for the run. Defaults to None.
|
||||
parent_run_id: The parent run ID. Defaults to None.
|
||||
metadata: The metadata for the run. Defaults to None.
|
||||
name: The name of the run.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
llm_run = self._create_llm_run(
|
||||
serialized=serialized,
|
||||
prompts=prompts,
|
||||
@@ -113,7 +141,18 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
parent_run_id: Optional[UUID] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Run on new LLM token. Only available when streaming is enabled."""
|
||||
"""Run on new LLM token. Only available when streaming is enabled.
|
||||
|
||||
Args:
|
||||
token: The token.
|
||||
chunk: The chunk. Defaults to None.
|
||||
run_id: The run ID.
|
||||
parent_run_id: The parent run ID. Defaults to None.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
# "chat_model" is only used for the experimental new streaming_events format.
|
||||
# This change should not affect any existing tracers.
|
||||
llm_run = self._llm_run_with_token_event(
|
||||
@@ -133,6 +172,16 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
run_id: UUID,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Run on retry.
|
||||
|
||||
Args:
|
||||
retry_state: The retry state.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
llm_run = self._llm_run_with_retry_event(
|
||||
retry_state=retry_state,
|
||||
run_id=run_id,
|
||||
@@ -140,7 +189,16 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
return llm_run
|
||||
|
||||
def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> Run:
|
||||
"""End a trace for an LLM run."""
|
||||
"""End a trace for an LLM run.
|
||||
|
||||
Args:
|
||||
response: The response.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
# "chat_model" is only used for the experimental new streaming_events format.
|
||||
# This change should not affect any existing tracers.
|
||||
llm_run = self._complete_llm_run(
|
||||
@@ -158,7 +216,16 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
run_id: UUID,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Handle an error for an LLM run."""
|
||||
"""Handle an error for an LLM run.
|
||||
|
||||
Args:
|
||||
error: The error.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
# "chat_model" is only used for the experimental new streaming_events format.
|
||||
# This change should not affect any existing tracers.
|
||||
llm_run = self._errored_llm_run(
|
||||
@@ -182,7 +249,22 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Start a trace for a chain run."""
|
||||
"""Start a trace for a chain run.
|
||||
|
||||
Args:
|
||||
serialized: The serialized chain.
|
||||
inputs: The inputs for the chain.
|
||||
run_id: The run ID.
|
||||
tags: The tags for the run. Defaults to None.
|
||||
parent_run_id: The parent run ID. Defaults to None.
|
||||
metadata: The metadata for the run. Defaults to None.
|
||||
run_type: The type of the run. Defaults to None.
|
||||
name: The name of the run.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
chain_run = self._create_chain_run(
|
||||
serialized=serialized,
|
||||
inputs=inputs,
|
||||
@@ -206,7 +288,17 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
inputs: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""End a trace for a chain run."""
|
||||
"""End a trace for a chain run.
|
||||
|
||||
Args:
|
||||
outputs: The outputs for the chain.
|
||||
run_id: The run ID.
|
||||
inputs: The inputs for the chain. Defaults to None.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
chain_run = self._complete_chain_run(
|
||||
outputs=outputs,
|
||||
run_id=run_id,
|
||||
@@ -225,7 +317,17 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
run_id: UUID,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Handle an error for a chain run."""
|
||||
"""Handle an error for a chain run.
|
||||
|
||||
Args:
|
||||
error: The error.
|
||||
inputs: The inputs for the chain. Defaults to None.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
chain_run = self._errored_chain_run(
|
||||
error=error,
|
||||
run_id=run_id,
|
||||
@@ -249,7 +351,22 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
inputs: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Start a trace for a tool run."""
|
||||
"""Start a trace for a tool run.
|
||||
|
||||
Args:
|
||||
serialized: The serialized tool.
|
||||
input_str: The input string.
|
||||
run_id: The run ID.
|
||||
tags: The tags for the run. Defaults to None.
|
||||
parent_run_id: The parent run ID. Defaults to None.
|
||||
metadata: The metadata for the run. Defaults to None.
|
||||
name: The name of the run.
|
||||
inputs: The inputs for the tool.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
tool_run = self._create_tool_run(
|
||||
serialized=serialized,
|
||||
input_str=input_str,
|
||||
@@ -266,7 +383,16 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
return tool_run
|
||||
|
||||
def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> Run:
|
||||
"""End a trace for a tool run."""
|
||||
"""End a trace for a tool run.
|
||||
|
||||
Args:
|
||||
output: The output for the tool.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
tool_run = self._complete_tool_run(
|
||||
output=output,
|
||||
run_id=run_id,
|
||||
@@ -283,7 +409,16 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
run_id: UUID,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Handle an error for a tool run."""
|
||||
"""Handle an error for a tool run.
|
||||
|
||||
Args:
|
||||
error: The error.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
tool_run = self._errored_tool_run(
|
||||
error=error,
|
||||
run_id=run_id,
|
||||
@@ -304,7 +439,21 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Run when Retriever starts running."""
|
||||
"""Run when the Retriever starts running.
|
||||
|
||||
Args:
|
||||
serialized: The serialized retriever.
|
||||
query: The query.
|
||||
run_id: The run ID.
|
||||
parent_run_id: The parent run ID. Defaults to None.
|
||||
tags: The tags for the run. Defaults to None.
|
||||
metadata: The metadata for the run. Defaults to None.
|
||||
name: The name of the run.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
retrieval_run = self._create_retrieval_run(
|
||||
serialized=serialized,
|
||||
query=query,
|
||||
@@ -326,7 +475,16 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
run_id: UUID,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Run when Retriever errors."""
|
||||
"""Run when Retriever errors.
|
||||
|
||||
Args:
|
||||
error: The error.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
retrieval_run = self._errored_retrieval_run(
|
||||
error=error,
|
||||
run_id=run_id,
|
||||
@@ -339,7 +497,16 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
|
||||
def on_retriever_end(
|
||||
self, documents: Sequence[Document], *, run_id: UUID, **kwargs: Any
|
||||
) -> Run:
|
||||
"""Run when Retriever ends running."""
|
||||
"""Run when the Retriever ends running.
|
||||
|
||||
Args:
|
||||
documents: The documents.
|
||||
run_id: The run ID.
|
||||
**kwargs: Additional arguments.
|
||||
|
||||
Returns:
|
||||
The run.
|
||||
"""
|
||||
retrieval_run = self._complete_retrieval_run(
|
||||
documents=documents,
|
||||
run_id=run_id,
|
||||
|
||||
@@ -68,8 +68,8 @@ def tracing_v2_enabled(
|
||||
client (LangSmithClient, optional): The client of the langsmith.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
None
|
||||
Yields:
|
||||
LangChainTracer: The LangChain tracer.
|
||||
|
||||
Example:
|
||||
>>> with tracing_v2_enabled():
|
||||
@@ -100,7 +100,7 @@ def tracing_v2_enabled(
|
||||
def collect_runs() -> Generator[RunCollectorCallbackHandler, None, None]:
|
||||
"""Collect all run traces in context.
|
||||
|
||||
Returns:
|
||||
Yields:
|
||||
run_collector.RunCollectorCallbackHandler: The run collector callback handler.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -46,7 +46,8 @@ SCHEMA_FORMAT_TYPE = Literal["original", "streaming_events"]
|
||||
|
||||
class _TracerCore(ABC):
|
||||
"""
|
||||
Abstract base class for tracers
|
||||
Abstract base class for tracers.
|
||||
|
||||
This class provides common methods, and reusable methods for tracers.
|
||||
"""
|
||||
|
||||
@@ -65,17 +66,18 @@ class _TracerCore(ABC):
|
||||
Args:
|
||||
_schema_format: Primarily changes how the inputs and outputs are
|
||||
handled. For internal use only. This API will change.
|
||||
|
||||
- 'original' is the format used by all current tracers.
|
||||
This format is slightly inconsistent with respect to inputs
|
||||
and outputs.
|
||||
This format is slightly inconsistent with respect to inputs
|
||||
and outputs.
|
||||
- 'streaming_events' is used for supporting streaming events,
|
||||
for internal usage. It will likely change in the future, or
|
||||
be deprecated entirely in favor of a dedicated async tracer
|
||||
for streaming events.
|
||||
for internal usage. It will likely change in the future, or
|
||||
be deprecated entirely in favor of a dedicated async tracer
|
||||
for streaming events.
|
||||
- 'original+chat' is a format that is the same as 'original'
|
||||
except it does NOT raise an attribute error on_chat_model_start
|
||||
except it does NOT raise an attribute error on_chat_model_start
|
||||
kwargs: Additional keyword arguments that will be passed to
|
||||
the super class.
|
||||
the superclass.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._schema_format = _schema_format # For internal use only API will change.
|
||||
@@ -207,7 +209,7 @@ class _TracerCore(ABC):
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Create a llm run"""
|
||||
"""Create a llm run."""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
if metadata:
|
||||
kwargs.update({"metadata": metadata})
|
||||
@@ -234,7 +236,7 @@ class _TracerCore(ABC):
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""
|
||||
Append token event to LLM run and return the run
|
||||
Append token event to LLM run and return the run.
|
||||
"""
|
||||
llm_run = self._get_run(run_id, run_type={"llm", "chat_model"})
|
||||
event_kwargs: Dict[str, Any] = {"token": token}
|
||||
@@ -314,7 +316,7 @@ class _TracerCore(ABC):
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Create a chain Run"""
|
||||
"""Create a chain Run."""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
if metadata:
|
||||
kwargs.update({"metadata": metadata})
|
||||
|
||||
@@ -104,7 +104,7 @@ class EvaluatorCallbackHandler(BaseTracer):
|
||||
def _evaluate_in_project(self, run: Run, evaluator: langsmith.RunEvaluator) -> None:
|
||||
"""Evaluate the run in the project.
|
||||
|
||||
Parameters
|
||||
Args:
|
||||
----------
|
||||
run : Run
|
||||
The run to be evaluated.
|
||||
@@ -200,7 +200,7 @@ class EvaluatorCallbackHandler(BaseTracer):
|
||||
def _persist_run(self, run: Run) -> None:
|
||||
"""Run the evaluator on the run.
|
||||
|
||||
Parameters
|
||||
Args:
|
||||
----------
|
||||
run : Run
|
||||
The run to be evaluated.
|
||||
|
||||
@@ -52,7 +52,18 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunInfo(TypedDict):
|
||||
"""Information about a run."""
|
||||
"""Information about a run.
|
||||
|
||||
This is used to keep track of the metadata associated with a run.
|
||||
|
||||
Parameters:
|
||||
name: The name of the run.
|
||||
tags: The tags associated with the run.
|
||||
metadata: The metadata associated with the run.
|
||||
run_type: The type of the run.
|
||||
inputs: The inputs to the run.
|
||||
parent_run_id: The ID of the parent run.
|
||||
"""
|
||||
|
||||
name: str
|
||||
tags: List[str]
|
||||
@@ -150,7 +161,19 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
async def tap_output_aiter(
|
||||
self, run_id: UUID, output: AsyncIterator[T]
|
||||
) -> AsyncIterator[T]:
|
||||
"""Tap the output aiter."""
|
||||
"""Tap the output aiter.
|
||||
|
||||
This method is used to tap the output of a Runnable that produces
|
||||
an async iterator. It is used to generate stream events for the
|
||||
output of the Runnable.
|
||||
|
||||
Args:
|
||||
run_id: The ID of the run.
|
||||
output: The output of the Runnable.
|
||||
|
||||
Yields:
|
||||
T: The output of the Runnable.
|
||||
"""
|
||||
sentinel = object()
|
||||
# atomic check and set
|
||||
tap = self.is_tapped.setdefault(run_id, sentinel)
|
||||
@@ -192,7 +215,15 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
|
||||
yield chunk
|
||||
|
||||
def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
|
||||
"""Tap the output aiter."""
|
||||
"""Tap the output aiter.
|
||||
|
||||
Args:
|
||||
run_id: The ID of the run.
|
||||
output: The output of the Runnable.
|
||||
|
||||
Yields:
|
||||
T: The output of the Runnable.
|
||||
"""
|
||||
sentinel = object()
|
||||
# atomic check and set
|
||||
tap = self.is_tapped.setdefault(run_id, sentinel)
|
||||
|
||||
@@ -32,7 +32,12 @@ _EXECUTOR: Optional[ThreadPoolExecutor] = None
|
||||
|
||||
|
||||
def log_error_once(method: str, exception: Exception) -> None:
|
||||
"""Log an error once."""
|
||||
"""Log an error once.
|
||||
|
||||
Args:
|
||||
method: The method that raised the exception.
|
||||
exception: The exception that was raised.
|
||||
"""
|
||||
global _LOGGED
|
||||
if (method, type(exception)) in _LOGGED:
|
||||
return
|
||||
@@ -82,7 +87,15 @@ class LangChainTracer(BaseTracer):
|
||||
tags: Optional[List[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the LangChain tracer."""
|
||||
"""Initialize the LangChain tracer.
|
||||
|
||||
Args:
|
||||
example_id: The example ID.
|
||||
project_name: The project name. Defaults to the tracer project.
|
||||
client: The client. Defaults to the global client.
|
||||
tags: The tags. Defaults to an empty list.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.example_id = (
|
||||
UUID(example_id) if isinstance(example_id, str) else example_id
|
||||
@@ -104,7 +117,21 @@ class LangChainTracer(BaseTracer):
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Run:
|
||||
"""Start a trace for an LLM run."""
|
||||
"""Start a trace for an LLM run.
|
||||
|
||||
Args:
|
||||
serialized: The serialized model.
|
||||
messages: The messages.
|
||||
run_id: The run ID.
|
||||
tags: The tags. Defaults to None.
|
||||
parent_run_id: The parent run ID. Defaults to None.
|
||||
metadata: The metadata. Defaults to None.
|
||||
name: The name. Defaults to None.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
Run: The run.
|
||||
"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
if metadata:
|
||||
kwargs.update({"metadata": metadata})
|
||||
@@ -130,7 +157,15 @@ class LangChainTracer(BaseTracer):
|
||||
self.latest_run = run_
|
||||
|
||||
def get_run_url(self) -> str:
|
||||
"""Get the LangSmith root run URL"""
|
||||
"""Get the LangSmith root run URL.
|
||||
|
||||
Returns:
|
||||
str: The LangSmith root run URL.
|
||||
|
||||
Raises:
|
||||
ValueError: If no traced run is found.
|
||||
ValueError: If the run URL cannot be found.
|
||||
"""
|
||||
if not self.latest_run:
|
||||
raise ValueError("No traced run found.")
|
||||
# If this is the first run in a project, the project may not yet be created.
|
||||
|
||||
@@ -189,12 +189,15 @@ class LogStreamCallbackHandler(BaseTracer, _StreamingCallbackHandler):
|
||||
handled.
|
||||
**For internal use only. This API will change.**
|
||||
- 'original' is the format used by all current tracers.
|
||||
This format is slightly inconsistent with respect to inputs
|
||||
and outputs.
|
||||
This format is slightly inconsistent with respect to inputs
|
||||
and outputs.
|
||||
- 'streaming_events' is used for supporting streaming events,
|
||||
for internal usage. It will likely change in the future, or
|
||||
be deprecated entirely in favor of a dedicated async tracer
|
||||
for streaming events.
|
||||
for internal usage. It will likely change in the future, or
|
||||
be deprecated entirely in favor of a dedicated async tracer
|
||||
for streaming events.
|
||||
|
||||
Raises:
|
||||
ValueError: If an invalid schema format is provided (internal use only).
|
||||
"""
|
||||
if _schema_format not in {"original", "streaming_events"}:
|
||||
raise ValueError(
|
||||
@@ -224,7 +227,15 @@ class LogStreamCallbackHandler(BaseTracer, _StreamingCallbackHandler):
|
||||
return self.receive_stream.__aiter__()
|
||||
|
||||
def send(self, *ops: Dict[str, Any]) -> bool:
|
||||
"""Send a patch to the stream, return False if the stream is closed."""
|
||||
"""Send a patch to the stream, return False if the stream is closed.
|
||||
|
||||
Args:
|
||||
*ops: The operations to send to the stream.
|
||||
|
||||
Returns:
|
||||
bool: True if the patch was sent successfully, False if the stream
|
||||
is closed.
|
||||
"""
|
||||
# We will likely want to wrap this in try / except at some point
|
||||
# to handle exceptions that might arise at run time.
|
||||
# For now we'll let the exception bubble up, and always return
|
||||
@@ -235,7 +246,15 @@ class LogStreamCallbackHandler(BaseTracer, _StreamingCallbackHandler):
|
||||
async def tap_output_aiter(
|
||||
self, run_id: UUID, output: AsyncIterator[T]
|
||||
) -> AsyncIterator[T]:
|
||||
"""Tap an output async iterator to stream its values to the log."""
|
||||
"""Tap an output async iterator to stream its values to the log.
|
||||
|
||||
Args:
|
||||
run_id: The ID of the run.
|
||||
output: The output async iterator.
|
||||
|
||||
Yields:
|
||||
T: The output value.
|
||||
"""
|
||||
async for chunk in output:
|
||||
# root run is handled in .astream_log()
|
||||
if run_id != self.root_id:
|
||||
@@ -254,7 +273,15 @@ class LogStreamCallbackHandler(BaseTracer, _StreamingCallbackHandler):
|
||||
yield chunk
|
||||
|
||||
def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
|
||||
"""Tap an output async iterator to stream its values to the log."""
|
||||
"""Tap an output async iterator to stream its values to the log.
|
||||
|
||||
Args:
|
||||
run_id: The ID of the run.
|
||||
output: The output iterator.
|
||||
|
||||
Yields:
|
||||
T: The output value.
|
||||
"""
|
||||
for chunk in output:
|
||||
# root run is handled in .astream_log()
|
||||
if run_id != self.root_id:
|
||||
@@ -273,6 +300,14 @@ class LogStreamCallbackHandler(BaseTracer, _StreamingCallbackHandler):
|
||||
yield chunk
|
||||
|
||||
def include_run(self, run: Run) -> bool:
|
||||
"""Check if a Run should be included in the log.
|
||||
|
||||
Args:
|
||||
run: The Run to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the run should be included, False otherwise.
|
||||
"""
|
||||
if run.id == self.root_id:
|
||||
return False
|
||||
|
||||
@@ -454,7 +489,7 @@ def _get_standardized_inputs(
|
||||
Returns:
|
||||
Valid inputs are only dict. By conventions, inputs always represented
|
||||
invocation using named arguments.
|
||||
A None means that the input is not yet known!
|
||||
None means that the input is not yet known!
|
||||
"""
|
||||
if schema_format == "original":
|
||||
raise NotImplementedError(
|
||||
|
||||
@@ -33,11 +33,27 @@ class _SendStream(Generic[T]):
|
||||
self._done = done
|
||||
|
||||
async def send(self, item: T) -> None:
|
||||
"""Schedule the item to be written to the queue using the original loop."""
|
||||
"""Schedule the item to be written to the queue using the original loop.
|
||||
|
||||
This is a coroutine that can be awaited.
|
||||
|
||||
Args:
|
||||
item: The item to write to the queue.
|
||||
"""
|
||||
return self.send_nowait(item)
|
||||
|
||||
def send_nowait(self, item: T) -> None:
|
||||
"""Schedule the item to be written to the queue using the original loop."""
|
||||
"""Schedule the item to be written to the queue using the original loop.
|
||||
|
||||
This is a non-blocking call.
|
||||
|
||||
Args:
|
||||
item: The item to write to the queue.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the event loop is already closed when trying to write
|
||||
to the queue.
|
||||
"""
|
||||
try:
|
||||
self._reader_loop.call_soon_threadsafe(self._queue.put_nowait, item)
|
||||
except RuntimeError:
|
||||
@@ -45,11 +61,18 @@ class _SendStream(Generic[T]):
|
||||
raise # Raise the exception if the loop is not closed
|
||||
|
||||
async def aclose(self) -> None:
|
||||
"""Schedule the done object write the queue using the original loop."""
|
||||
"""Async schedule the done object write the queue using the original loop."""
|
||||
return self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Schedule the done object write the queue using the original loop."""
|
||||
"""Schedule the done object write the queue using the original loop.
|
||||
|
||||
This is a non-blocking call.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the event loop is already closed when trying to write
|
||||
to the queue.
|
||||
"""
|
||||
try:
|
||||
self._reader_loop.call_soon_threadsafe(self._queue.put_nowait, self._done)
|
||||
except RuntimeError:
|
||||
@@ -87,7 +110,7 @@ class _MemoryStream(Generic[T]):
|
||||
|
||||
This implementation is meant to be used with a single writer and a single reader.
|
||||
|
||||
This is an internal implementation to LangChain please do not use it directly.
|
||||
This is an internal implementation to LangChain. Please do not use it directly.
|
||||
"""
|
||||
|
||||
def __init__(self, loop: AbstractEventLoop) -> None:
|
||||
@@ -103,11 +126,19 @@ class _MemoryStream(Generic[T]):
|
||||
self._done = object()
|
||||
|
||||
def get_send_stream(self) -> _SendStream[T]:
|
||||
"""Get a writer for the channel."""
|
||||
"""Get a writer for the channel.
|
||||
|
||||
Returns:
|
||||
_SendStream: The writer for the channel.
|
||||
"""
|
||||
return _SendStream[T](
|
||||
reader_loop=self._loop, queue=self._queue, done=self._done
|
||||
)
|
||||
|
||||
def get_receive_stream(self) -> _ReceiveStream[T]:
|
||||
"""Get a reader for the channel."""
|
||||
"""Get a reader for the channel.
|
||||
|
||||
Returns:
|
||||
_ReceiveStream: The reader for the channel.
|
||||
"""
|
||||
return _ReceiveStream[T](queue=self._queue, done=self._done)
|
||||
|
||||
@@ -16,7 +16,16 @@ AsyncListener = Union[
|
||||
|
||||
|
||||
class RootListenersTracer(BaseTracer):
|
||||
"""Tracer that calls listeners on run start, end, and error."""
|
||||
"""Tracer that calls listeners on run start, end, and error.
|
||||
|
||||
Parameters:
|
||||
log_missing_parent: Whether to log a warning if the parent is missing.
|
||||
Default is False.
|
||||
config: The runnable config.
|
||||
on_start: The listener to call on run start.
|
||||
on_end: The listener to call on run end.
|
||||
on_error: The listener to call on run error.
|
||||
"""
|
||||
|
||||
log_missing_parent = False
|
||||
|
||||
@@ -28,6 +37,14 @@ class RootListenersTracer(BaseTracer):
|
||||
on_end: Optional[Listener],
|
||||
on_error: Optional[Listener],
|
||||
) -> None:
|
||||
"""Initialize the tracer.
|
||||
|
||||
Args:
|
||||
config: The runnable config.
|
||||
on_start: The listener to call on run start.
|
||||
on_end: The listener to call on run end.
|
||||
on_error: The listener to call on run error
|
||||
"""
|
||||
super().__init__(_schema_format="original+chat")
|
||||
|
||||
self.config = config
|
||||
@@ -63,7 +80,16 @@ class RootListenersTracer(BaseTracer):
|
||||
|
||||
|
||||
class AsyncRootListenersTracer(AsyncBaseTracer):
|
||||
"""Async Tracer that calls listeners on run start, end, and error."""
|
||||
"""Async Tracer that calls listeners on run start, end, and error.
|
||||
|
||||
Parameters:
|
||||
log_missing_parent: Whether to log a warning if the parent is missing.
|
||||
Default is False.
|
||||
config: The runnable config.
|
||||
on_start: The listener to call on run start.
|
||||
on_end: The listener to call on run end.
|
||||
on_error: The listener to call on run error.
|
||||
"""
|
||||
|
||||
log_missing_parent = False
|
||||
|
||||
@@ -75,6 +101,14 @@ class AsyncRootListenersTracer(AsyncBaseTracer):
|
||||
on_end: Optional[AsyncListener],
|
||||
on_error: Optional[AsyncListener],
|
||||
) -> None:
|
||||
"""Initialize the tracer.
|
||||
|
||||
Args:
|
||||
config: The runnable config.
|
||||
on_start: The listener to call on run start.
|
||||
on_end: The listener to call on run end.
|
||||
on_error: The listener to call on run error
|
||||
"""
|
||||
super().__init__(_schema_format="original+chat")
|
||||
|
||||
self.config = config
|
||||
|
||||
@@ -8,13 +8,13 @@ from langchain_core.tracers.schemas import Run
|
||||
|
||||
|
||||
class RunCollectorCallbackHandler(BaseTracer):
|
||||
"""
|
||||
Tracer that collects all nested runs in a list.
|
||||
"""Tracer that collects all nested runs in a list.
|
||||
|
||||
This tracer is useful for inspection and evaluation purposes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str, default="run-collector_callback_handler"
|
||||
example_id : Optional[Union[UUID, str]], default=None
|
||||
The ID of the example being traced. It can be either a UUID or a string.
|
||||
"""
|
||||
@@ -31,6 +31,8 @@ class RunCollectorCallbackHandler(BaseTracer):
|
||||
----------
|
||||
example_id : Optional[Union[UUID, str]], default=None
|
||||
The ID of the example being traced. It can be either a UUID or a string.
|
||||
**kwargs : Any
|
||||
Additional keyword arguments
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self.example_id = (
|
||||
|
||||
@@ -112,7 +112,15 @@ class ToolRun(BaseRun):
|
||||
|
||||
|
||||
class Run(BaseRunV2):
|
||||
"""Run schema for the V2 API in the Tracer."""
|
||||
"""Run schema for the V2 API in the Tracer.
|
||||
|
||||
Parameters:
|
||||
child_runs: The child runs.
|
||||
tags: The tags. Default is an empty list.
|
||||
events: The events. Default is an empty list.
|
||||
trace_id: The trace ID. Default is None.
|
||||
dotted_order: The dotted order.
|
||||
"""
|
||||
|
||||
child_runs: List[Run] = Field(default_factory=list)
|
||||
tags: Optional[List[str]] = Field(default_factory=list)
|
||||
|
||||
@@ -7,15 +7,14 @@ from langchain_core.utils.input import get_bolded_text, get_colored_text
|
||||
|
||||
|
||||
def try_json_stringify(obj: Any, fallback: str) -> str:
|
||||
"""
|
||||
Try to stringify an object to JSON.
|
||||
"""Try to stringify an object to JSON.
|
||||
|
||||
Args:
|
||||
obj: Object to stringify.
|
||||
fallback: Fallback string to return if the object cannot be stringified.
|
||||
|
||||
Returns:
|
||||
A JSON string if the object can be stringified, otherwise the fallback string.
|
||||
|
||||
"""
|
||||
try:
|
||||
return json.dumps(obj, indent=2, ensure_ascii=False)
|
||||
@@ -45,6 +44,8 @@ class FunctionCallbackHandler(BaseTracer):
|
||||
"""Tracer that calls a function with a single str parameter."""
|
||||
|
||||
name: str = "function_callback_handler"
|
||||
"""The name of the tracer. This is used to identify the tracer in the logs.
|
||||
Default is "function_callback_handler"."""
|
||||
|
||||
def __init__(self, function: Callable[[str], None], **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
@@ -54,6 +55,14 @@ class FunctionCallbackHandler(BaseTracer):
|
||||
pass
|
||||
|
||||
def get_parents(self, run: Run) -> List[Run]:
|
||||
"""Get the parents of a run.
|
||||
|
||||
Args:
|
||||
run: The run to get the parents of.
|
||||
|
||||
Returns:
|
||||
A list of parent runs.
|
||||
"""
|
||||
parents = []
|
||||
current_run = run
|
||||
while current_run.parent_run_id:
|
||||
@@ -66,6 +75,14 @@ class FunctionCallbackHandler(BaseTracer):
|
||||
return parents
|
||||
|
||||
def get_breadcrumbs(self, run: Run) -> str:
|
||||
"""Get the breadcrumbs of a run.
|
||||
|
||||
Args:
|
||||
run: The run to get the breadcrumbs of.
|
||||
|
||||
Returns:
|
||||
A string with the breadcrumbs of the run.
|
||||
"""
|
||||
parents = self.get_parents(run)[::-1]
|
||||
string = " > ".join(
|
||||
f"{parent.run_type}:{parent.name}"
|
||||
|
||||
@@ -8,6 +8,17 @@ def merge_dicts(left: Dict[str, Any], *others: Dict[str, Any]) -> Dict[str, Any]
|
||||
dictionaries but has a value of None in 'left'. In such cases, the method uses the
|
||||
value from 'right' for that key in the merged dictionary.
|
||||
|
||||
Args:
|
||||
left: The first dictionary to merge.
|
||||
others: The other dictionaries to merge.
|
||||
|
||||
Returns:
|
||||
The merged dictionary.
|
||||
|
||||
Raises:
|
||||
TypeError: If the key exists in both dictionaries but has a different type.
|
||||
TypeError: If the value has an unsupported type.
|
||||
|
||||
Example:
|
||||
If left = {"function_call": {"arguments": None}} and
|
||||
right = {"function_call": {"arguments": "{\n"}}
|
||||
@@ -46,7 +57,15 @@ def merge_dicts(left: Dict[str, Any], *others: Dict[str, Any]) -> Dict[str, Any]
|
||||
|
||||
|
||||
def merge_lists(left: Optional[List], *others: Optional[List]) -> Optional[List]:
|
||||
"""Add many lists, handling None."""
|
||||
"""Add many lists, handling None.
|
||||
|
||||
Args:
|
||||
left: The first list to merge.
|
||||
others: The other lists to merge.
|
||||
|
||||
Returns:
|
||||
The merged list.
|
||||
"""
|
||||
merged = left.copy() if left is not None else None
|
||||
for other in others:
|
||||
if other is None:
|
||||
@@ -75,6 +94,23 @@ def merge_lists(left: Optional[List], *others: Optional[List]) -> Optional[List]
|
||||
|
||||
|
||||
def merge_obj(left: Any, right: Any) -> Any:
|
||||
"""Merge two objects.
|
||||
|
||||
It handles specific scenarios where a key exists in both
|
||||
dictionaries but has a value of None in 'left'. In such cases, the method uses the
|
||||
value from 'right' for that key in the merged dictionary.
|
||||
|
||||
Args:
|
||||
left: The first object to merge.
|
||||
right: The other object to merge.
|
||||
|
||||
Returns:
|
||||
The merged object.
|
||||
|
||||
Raises:
|
||||
TypeError: If the key exists in both dictionaries but has a different type.
|
||||
ValueError: If the two objects cannot be merged.
|
||||
"""
|
||||
if left is None or right is None:
|
||||
return left if left is not None else right
|
||||
elif type(left) is not type(right):
|
||||
|
||||
@@ -44,6 +44,18 @@ def py_anext(
|
||||
Can be used to compare the built-in implementation of the inner
|
||||
coroutines machinery to C-implementation of __anext__() and send()
|
||||
or throw() on the returned generator.
|
||||
|
||||
Args:
|
||||
iterator: The async iterator to advance.
|
||||
default: The value to return if the iterator is exhausted.
|
||||
If not provided, a StopAsyncIteration exception is raised.
|
||||
|
||||
Returns:
|
||||
The next value from the iterator, or the default value
|
||||
if the iterator is exhausted.
|
||||
|
||||
Raises:
|
||||
TypeError: If the iterator is not an async iterator.
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -71,7 +83,7 @@ def py_anext(
|
||||
|
||||
|
||||
class NoLock:
|
||||
"""Dummy lock that provides the proper interface but no protection"""
|
||||
"""Dummy lock that provides the proper interface but no protection."""
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
pass
|
||||
@@ -88,7 +100,21 @@ async def tee_peer(
|
||||
peers: List[Deque[T]],
|
||||
lock: AsyncContextManager[Any],
|
||||
) -> AsyncGenerator[T, None]:
|
||||
"""An individual iterator of a :py:func:`~.tee`"""
|
||||
"""An individual iterator of a :py:func:`~.tee`.
|
||||
|
||||
This function is a generator that yields items from the shared iterator
|
||||
``iterator``. It buffers items until the least advanced iterator has
|
||||
yielded them as well. The buffer is shared with all other peers.
|
||||
|
||||
Args:
|
||||
iterator: The shared iterator.
|
||||
buffer: The buffer for this peer.
|
||||
peers: The buffers of all peers.
|
||||
lock: The lock to synchronise access to the shared buffers.
|
||||
|
||||
Yields:
|
||||
The next item from the shared iterator.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
if not buffer:
|
||||
@@ -204,6 +230,7 @@ class Tee(Generic[T]):
|
||||
return False
|
||||
|
||||
async def aclose(self) -> None:
|
||||
"""Async close all child iterators."""
|
||||
for child in self._children:
|
||||
await child.aclose()
|
||||
|
||||
@@ -258,7 +285,7 @@ async def abatch_iterate(
|
||||
iterable: The async iterable to batch.
|
||||
|
||||
Returns:
|
||||
An async iterator over the batches
|
||||
An async iterator over the batches.
|
||||
"""
|
||||
batch: List[T] = []
|
||||
async for element in iterable:
|
||||
|
||||
@@ -36,7 +36,7 @@ def get_from_dict_or_env(
|
||||
env_key: The environment variable to look up if the key is not
|
||||
in the dictionary.
|
||||
default: The default value to return if the key is not in the dictionary
|
||||
or the environment.
|
||||
or the environment. Defaults to None.
|
||||
"""
|
||||
if isinstance(key, (list, tuple)):
|
||||
for k in key:
|
||||
@@ -56,7 +56,22 @@ def get_from_dict_or_env(
|
||||
|
||||
|
||||
def get_from_env(key: str, env_key: str, default: Optional[str] = None) -> str:
|
||||
"""Get a value from a dictionary or an environment variable."""
|
||||
"""Get a value from a dictionary or an environment variable.
|
||||
|
||||
Args:
|
||||
key: The key to look up in the dictionary.
|
||||
env_key: The environment variable to look up if the key is not
|
||||
in the dictionary.
|
||||
default: The default value to return if the key is not in the dictionary
|
||||
or the environment. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: The value of the key.
|
||||
|
||||
Raises:
|
||||
ValueError: If the key is not in the dictionary and no default value is
|
||||
provided or if the environment variable is not set.
|
||||
"""
|
||||
if env_key in os.environ and os.environ[env_key]:
|
||||
return os.environ[env_key]
|
||||
elif default is not None:
|
||||
|
||||
@@ -10,7 +10,19 @@ class StrictFormatter(Formatter):
|
||||
def vformat(
|
||||
self, format_string: str, args: Sequence, kwargs: Mapping[str, Any]
|
||||
) -> str:
|
||||
"""Check that no arguments are provided."""
|
||||
"""Check that no arguments are provided.
|
||||
|
||||
Args:
|
||||
format_string: The format string.
|
||||
args: The arguments.
|
||||
kwargs: The keyword arguments.
|
||||
|
||||
Returns:
|
||||
The formatted string.
|
||||
|
||||
Raises:
|
||||
ValueError: If any arguments are provided.
|
||||
"""
|
||||
if len(args) > 0:
|
||||
raise ValueError(
|
||||
"No arguments should be provided, "
|
||||
@@ -21,6 +33,15 @@ class StrictFormatter(Formatter):
|
||||
def validate_input_variables(
|
||||
self, format_string: str, input_variables: List[str]
|
||||
) -> None:
|
||||
"""Check that all input variables are used in the format string.
|
||||
|
||||
Args:
|
||||
format_string: The format string.
|
||||
input_variables: The input variables.
|
||||
|
||||
Raises:
|
||||
ValueError: If any input variables are not used in the format string.
|
||||
"""
|
||||
dummy_inputs = {input_variable: "foo" for input_variable in input_variables}
|
||||
super().format(format_string, **dummy_inputs)
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ class ToolDescription(TypedDict):
|
||||
"""Representation of a callable function to the OpenAI API."""
|
||||
|
||||
type: Literal["function"]
|
||||
"""The type of the tool."""
|
||||
function: FunctionDescription
|
||||
"""The function description."""
|
||||
|
||||
|
||||
def _rm_titles(kv: dict, prev_key: str = "") -> dict:
|
||||
@@ -85,7 +87,19 @@ def convert_pydantic_to_openai_function(
|
||||
description: Optional[str] = None,
|
||||
rm_titles: bool = True,
|
||||
) -> FunctionDescription:
|
||||
"""Converts a Pydantic model to a function description for the OpenAI API."""
|
||||
"""Converts a Pydantic model to a function description for the OpenAI API.
|
||||
|
||||
Args:
|
||||
model: The Pydantic model to convert.
|
||||
name: The name of the function. If not provided, the title of the schema will be
|
||||
used.
|
||||
description: The description of the function. If not provided, the description
|
||||
of the schema will be used.
|
||||
rm_titles: Whether to remove titles from the schema. Defaults to True.
|
||||
|
||||
Returns:
|
||||
The function description.
|
||||
"""
|
||||
schema = dereference_refs(model.schema())
|
||||
schema.pop("definitions", None)
|
||||
title = schema.pop("title", "")
|
||||
@@ -108,7 +122,18 @@ def convert_pydantic_to_openai_tool(
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> ToolDescription:
|
||||
"""Converts a Pydantic model to a function description for the OpenAI API."""
|
||||
"""Converts a Pydantic model to a function description for the OpenAI API.
|
||||
|
||||
Args:
|
||||
model: The Pydantic model to convert.
|
||||
name: The name of the function. If not provided, the title of the schema will be
|
||||
used.
|
||||
description: The description of the function. If not provided, the description
|
||||
of the schema will be used.
|
||||
|
||||
Returns:
|
||||
The tool description.
|
||||
"""
|
||||
function = convert_pydantic_to_openai_function(
|
||||
model, name=name, description=description
|
||||
)
|
||||
@@ -133,6 +158,12 @@ def convert_python_function_to_openai_function(
|
||||
Assumes the Python function has type hints and a docstring with a description. If
|
||||
the docstring has Google Python style argument descriptions, these will be
|
||||
included as well.
|
||||
|
||||
Args:
|
||||
function: The Python function to convert.
|
||||
|
||||
Returns:
|
||||
The OpenAI function description.
|
||||
"""
|
||||
from langchain_core import tools
|
||||
|
||||
@@ -157,7 +188,14 @@ def convert_python_function_to_openai_function(
|
||||
removal="0.3.0",
|
||||
)
|
||||
def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
|
||||
"""Format tool into the OpenAI function API."""
|
||||
"""Format tool into the OpenAI function API.
|
||||
|
||||
Args:
|
||||
tool: The tool to format.
|
||||
|
||||
Returns:
|
||||
The function description.
|
||||
"""
|
||||
if tool.args_schema:
|
||||
return convert_pydantic_to_openai_function(
|
||||
tool.args_schema, name=tool.name, description=tool.description
|
||||
@@ -187,7 +225,14 @@ def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
|
||||
removal="0.3.0",
|
||||
)
|
||||
def format_tool_to_openai_tool(tool: BaseTool) -> ToolDescription:
|
||||
"""Format tool into the OpenAI function API."""
|
||||
"""Format tool into the OpenAI function API.
|
||||
|
||||
Args:
|
||||
tool: The tool to format.
|
||||
|
||||
Returns:
|
||||
The tool description.
|
||||
"""
|
||||
function = format_tool_to_openai_function(tool)
|
||||
return {"type": "function", "function": function}
|
||||
|
||||
@@ -206,6 +251,9 @@ def convert_to_openai_function(
|
||||
Returns:
|
||||
A dict version of the passed in function which is compatible with the
|
||||
OpenAI function-calling API.
|
||||
|
||||
Raises:
|
||||
ValueError: If the function is not in a supported format.
|
||||
"""
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
@@ -284,7 +332,7 @@ def tool_example_to_messages(
|
||||
BaseModels
|
||||
tool_outputs: Optional[List[str]], a list of tool call outputs.
|
||||
Does not need to be provided. If not provided, a placeholder value
|
||||
will be inserted.
|
||||
will be inserted. Defaults to None.
|
||||
|
||||
Returns:
|
||||
A list of messages
|
||||
|
||||
@@ -34,11 +34,11 @@ DEFAULT_LINK_REGEX = (
|
||||
def find_all_links(
|
||||
raw_html: str, *, pattern: Union[str, re.Pattern, None] = None
|
||||
) -> List[str]:
|
||||
"""Extract all links from a raw html string.
|
||||
"""Extract all links from a raw HTML string.
|
||||
|
||||
Args:
|
||||
raw_html: original html.
|
||||
pattern: Regex to use for extracting links from raw html.
|
||||
raw_html: original HTML.
|
||||
pattern: Regex to use for extracting links from raw HTML.
|
||||
|
||||
Returns:
|
||||
List[str]: all links
|
||||
@@ -57,20 +57,20 @@ def extract_sub_links(
|
||||
exclude_prefixes: Sequence[str] = (),
|
||||
continue_on_failure: bool = False,
|
||||
) -> List[str]:
|
||||
"""Extract all links from a raw html string and convert into absolute paths.
|
||||
"""Extract all links from a raw HTML string and convert into absolute paths.
|
||||
|
||||
Args:
|
||||
raw_html: original html.
|
||||
url: the url of the html.
|
||||
base_url: the base url to check for outside links against.
|
||||
pattern: Regex to use for extracting links from raw html.
|
||||
raw_html: original HTML.
|
||||
url: the url of the HTML.
|
||||
base_url: the base URL to check for outside links against.
|
||||
pattern: Regex to use for extracting links from raw HTML.
|
||||
prevent_outside: If True, ignore external links which are not children
|
||||
of the base url.
|
||||
of the base URL.
|
||||
exclude_prefixes: Exclude any URLs that start with one of these prefixes.
|
||||
continue_on_failure: If True, continue if parsing a specific link raises an
|
||||
exception. Otherwise, raise the exception.
|
||||
Returns:
|
||||
List[str]: sub links
|
||||
List[str]: sub links.
|
||||
"""
|
||||
base_url_to_use = base_url if base_url is not None else url
|
||||
parsed_base_url = urlparse(base_url_to_use)
|
||||
|
||||
@@ -3,12 +3,27 @@ import mimetypes
|
||||
|
||||
|
||||
def encode_image(image_path: str) -> str:
|
||||
"""Get base64 string from image URI."""
|
||||
"""Get base64 string from image URI.
|
||||
|
||||
Args:
|
||||
image_path: The path to the image.
|
||||
|
||||
Returns:
|
||||
The base64 string of the image.
|
||||
"""
|
||||
with open(image_path, "rb") as image_file:
|
||||
return base64.b64encode(image_file.read()).decode("utf-8")
|
||||
|
||||
|
||||
def image_to_data_url(image_path: str) -> str:
|
||||
"""Get data URL from image URI.
|
||||
|
||||
Args:
|
||||
image_path: The path to the image.
|
||||
|
||||
Returns:
|
||||
The data URL of the image.
|
||||
"""
|
||||
encoding = encode_image(image_path)
|
||||
mime_type = mimetypes.guess_type(image_path)[0]
|
||||
return f"data:{mime_type};base64,{encoding}"
|
||||
|
||||
@@ -14,7 +14,15 @@ _TEXT_COLOR_MAPPING = {
|
||||
def get_color_mapping(
|
||||
items: List[str], excluded_colors: Optional[List] = None
|
||||
) -> Dict[str, str]:
|
||||
"""Get mapping for items to a support color."""
|
||||
"""Get mapping for items to a support color.
|
||||
|
||||
Args:
|
||||
items: The items to map to colors.
|
||||
excluded_colors: The colors to exclude.
|
||||
|
||||
Returns:
|
||||
The mapping of items to colors.
|
||||
"""
|
||||
colors = list(_TEXT_COLOR_MAPPING.keys())
|
||||
if excluded_colors is not None:
|
||||
colors = [c for c in colors if c not in excluded_colors]
|
||||
@@ -23,20 +31,45 @@ def get_color_mapping(
|
||||
|
||||
|
||||
def get_colored_text(text: str, color: str) -> str:
|
||||
"""Get colored text."""
|
||||
"""Get colored text.
|
||||
|
||||
Args:
|
||||
text: The text to color.
|
||||
color: The color to use.
|
||||
|
||||
Returns:
|
||||
The colored text.
|
||||
"""
|
||||
color_str = _TEXT_COLOR_MAPPING[color]
|
||||
return f"\u001b[{color_str}m\033[1;3m{text}\u001b[0m"
|
||||
|
||||
|
||||
def get_bolded_text(text: str) -> str:
|
||||
"""Get bolded text."""
|
||||
"""Get bolded text.
|
||||
|
||||
Args:
|
||||
text: The text to bold.
|
||||
|
||||
Returns:
|
||||
The bolded text.
|
||||
"""
|
||||
return f"\033[1m{text}\033[0m"
|
||||
|
||||
|
||||
def print_text(
|
||||
text: str, color: Optional[str] = None, end: str = "", file: Optional[TextIO] = None
|
||||
) -> None:
|
||||
"""Print text with highlighting and no end characters."""
|
||||
"""Print text with highlighting and no end characters.
|
||||
|
||||
If a color is provided, the text will be printed in that color.
|
||||
If a file is provided, the text will be written to that file.
|
||||
|
||||
Args:
|
||||
text: The text to print.
|
||||
color: The color to use. Defaults to None.
|
||||
end: The end character to use. Defaults to "".
|
||||
file: The file to write to. Defaults to None.
|
||||
"""
|
||||
text_to_print = get_colored_text(text, color) if color else text
|
||||
print(text_to_print, end=end, file=file)
|
||||
if file:
|
||||
|
||||
@@ -22,7 +22,7 @@ T = TypeVar("T")
|
||||
|
||||
|
||||
class NoLock:
|
||||
"""Dummy lock that provides the proper interface but no protection"""
|
||||
"""Dummy lock that provides the proper interface but no protection."""
|
||||
|
||||
def __enter__(self) -> None:
|
||||
pass
|
||||
@@ -39,7 +39,21 @@ def tee_peer(
|
||||
peers: List[Deque[T]],
|
||||
lock: ContextManager[Any],
|
||||
) -> Generator[T, None, None]:
|
||||
"""An individual iterator of a :py:func:`~.tee`"""
|
||||
"""An individual iterator of a :py:func:`~.tee`.
|
||||
|
||||
This function is a generator that yields items from the shared iterator
|
||||
``iterator``. It buffers items until the least advanced iterator has
|
||||
yielded them as well. The buffer is shared with all other peers.
|
||||
|
||||
Args:
|
||||
iterator: The shared iterator.
|
||||
buffer: The buffer for this peer.
|
||||
peers: The buffers of all peers.
|
||||
lock: The lock to synchronise access to the shared buffers.
|
||||
|
||||
Yields:
|
||||
The next item from the shared iterator.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
if not buffer:
|
||||
@@ -118,6 +132,14 @@ class Tee(Generic[T]):
|
||||
*,
|
||||
lock: Optional[ContextManager[Any]] = None,
|
||||
):
|
||||
"""Create a new ``tee``.
|
||||
|
||||
Args:
|
||||
iterable: The iterable to split.
|
||||
n: The number of iterators to create. Defaults to 2.
|
||||
lock: The lock to synchronise access to the shared buffers.
|
||||
Defaults to None.
|
||||
"""
|
||||
self._iterator = iter(iterable)
|
||||
self._buffers: List[Deque[T]] = [deque() for _ in range(n)]
|
||||
self._children = tuple(
|
||||
@@ -170,8 +192,8 @@ def batch_iterate(size: Optional[int], iterable: Iterable[T]) -> Iterator[List[T
|
||||
size: The size of the batch. If None, returns a single batch.
|
||||
iterable: The iterable to batch.
|
||||
|
||||
Returns:
|
||||
An iterator over the batches.
|
||||
Yields:
|
||||
The batches of the iterable.
|
||||
"""
|
||||
it = iter(iterable)
|
||||
while True:
|
||||
|
||||
@@ -124,8 +124,7 @@ _json_markdown_re = re.compile(r"```(json)?(.*)", re.DOTALL)
|
||||
def parse_json_markdown(
|
||||
json_string: str, *, parser: Callable[[str], Any] = parse_partial_json
|
||||
) -> dict:
|
||||
"""
|
||||
Parse a JSON string from a Markdown string.
|
||||
"""Parse a JSON string from a Markdown string.
|
||||
|
||||
Args:
|
||||
json_string: The Markdown string.
|
||||
@@ -175,6 +174,10 @@ def parse_and_check_json_markdown(text: str, expected_keys: List[str]) -> dict:
|
||||
|
||||
Returns:
|
||||
The parsed JSON object as a Python dictionary.
|
||||
|
||||
Raises:
|
||||
OutputParserException: If the JSON string is invalid or does not contain
|
||||
the expected keys.
|
||||
"""
|
||||
try:
|
||||
json_obj = parse_json_markdown(text)
|
||||
|
||||
@@ -90,7 +90,16 @@ def dereference_refs(
|
||||
full_schema: Optional[dict] = None,
|
||||
skip_keys: Optional[Sequence[str]] = None,
|
||||
) -> dict:
|
||||
"""Try to substitute $refs in JSON Schema."""
|
||||
"""Try to substitute $refs in JSON Schema.
|
||||
|
||||
Args:
|
||||
schema_obj: The schema object to dereference.
|
||||
full_schema: The full schema object. Defaults to None.
|
||||
skip_keys: The keys to skip. Defaults to None.
|
||||
|
||||
Returns:
|
||||
The dereferenced schema object.
|
||||
"""
|
||||
|
||||
full_schema = full_schema or schema_obj
|
||||
skip_keys = (
|
||||
|
||||
@@ -42,7 +42,15 @@ class ChevronError(SyntaxError):
|
||||
|
||||
|
||||
def grab_literal(template: str, l_del: str) -> Tuple[str, str]:
|
||||
"""Parse a literal from the template."""
|
||||
"""Parse a literal from the template.
|
||||
|
||||
Args:
|
||||
template: The template to parse.
|
||||
l_del: The left delimiter.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: The literal and the template.
|
||||
"""
|
||||
|
||||
global _CURRENT_LINE
|
||||
|
||||
@@ -59,7 +67,16 @@ def grab_literal(template: str, l_del: str) -> Tuple[str, str]:
|
||||
|
||||
|
||||
def l_sa_check(template: str, literal: str, is_standalone: bool) -> bool:
|
||||
"""Do a preliminary check to see if a tag could be a standalone."""
|
||||
"""Do a preliminary check to see if a tag could be a standalone.
|
||||
|
||||
Args:
|
||||
template: The template. (Not used.)
|
||||
literal: The literal.
|
||||
is_standalone: Whether the tag is standalone.
|
||||
|
||||
Returns:
|
||||
bool: Whether the tag could be a standalone.
|
||||
"""
|
||||
|
||||
# If there is a newline, or the previous tag was a standalone
|
||||
if literal.find("\n") != -1 or is_standalone:
|
||||
@@ -77,7 +94,16 @@ def l_sa_check(template: str, literal: str, is_standalone: bool) -> bool:
|
||||
|
||||
|
||||
def r_sa_check(template: str, tag_type: str, is_standalone: bool) -> bool:
|
||||
"""Do a final check to see if a tag could be a standalone."""
|
||||
"""Do a final check to see if a tag could be a standalone.
|
||||
|
||||
Args:
|
||||
template: The template.
|
||||
tag_type: The type of the tag.
|
||||
is_standalone: Whether the tag is standalone.
|
||||
|
||||
Returns:
|
||||
bool: Whether the tag could be a standalone.
|
||||
"""
|
||||
|
||||
# Check right side if we might be a standalone
|
||||
if is_standalone and tag_type not in ["variable", "no escape"]:
|
||||
@@ -95,7 +121,20 @@ def r_sa_check(template: str, tag_type: str, is_standalone: bool) -> bool:
|
||||
|
||||
|
||||
def parse_tag(template: str, l_del: str, r_del: str) -> Tuple[Tuple[str, str], str]:
|
||||
"""Parse a tag from a template."""
|
||||
"""Parse a tag from a template.
|
||||
|
||||
Args:
|
||||
template: The template.
|
||||
l_del: The left delimiter.
|
||||
r_del: The right delimiter.
|
||||
|
||||
Returns:
|
||||
Tuple[Tuple[str, str], str]: The tag and the template.
|
||||
|
||||
Raises:
|
||||
ChevronError: If the tag is unclosed.
|
||||
ChevronError: If the set delimiter tag is unclosed.
|
||||
"""
|
||||
global _CURRENT_LINE
|
||||
global _LAST_TAG_LINE
|
||||
|
||||
@@ -404,36 +443,36 @@ def render(
|
||||
|
||||
Arguments:
|
||||
|
||||
template -- A file-like object or a string containing the template
|
||||
template -- A file-like object or a string containing the template.
|
||||
|
||||
data -- A python dictionary with your data scope
|
||||
data -- A python dictionary with your data scope.
|
||||
|
||||
partials_path -- The path to where your partials are stored
|
||||
partials_path -- The path to where your partials are stored.
|
||||
If set to None, then partials won't be loaded from the file system
|
||||
(defaults to '.')
|
||||
(defaults to '.').
|
||||
|
||||
partials_ext -- The extension that you want the parser to look for
|
||||
(defaults to 'mustache')
|
||||
(defaults to 'mustache').
|
||||
|
||||
partials_dict -- A python dictionary which will be search for partials
|
||||
before the filesystem is. {'include': 'foo'} is the same
|
||||
as a file called include.mustache
|
||||
(defaults to {})
|
||||
(defaults to {}).
|
||||
|
||||
padding -- This is for padding partials, and shouldn't be used
|
||||
(but can be if you really want to)
|
||||
(but can be if you really want to).
|
||||
|
||||
def_ldel -- The default left delimiter
|
||||
("{{" by default, as in spec compliant mustache)
|
||||
("{{" by default, as in spec compliant mustache).
|
||||
|
||||
def_rdel -- The default right delimiter
|
||||
("}}" by default, as in spec compliant mustache)
|
||||
("}}" by default, as in spec compliant mustache).
|
||||
|
||||
scopes -- The list of scopes that get_key will look through
|
||||
scopes -- The list of scopes that get_key will look through.
|
||||
|
||||
warn -- Log a warning when a template substitution isn't found in the data
|
||||
|
||||
keep -- Keep unreplaced tags when a substitution isn't found in the data
|
||||
keep -- Keep unreplaced tags when a substitution isn't found in the data.
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -21,12 +21,27 @@ PYDANTIC_MAJOR_VERSION = get_pydantic_major_version()
|
||||
|
||||
# How to type hint this?
|
||||
def pre_init(func: Callable) -> Any:
|
||||
"""Decorator to run a function before model initialization."""
|
||||
"""Decorator to run a function before model initialization.
|
||||
|
||||
Args:
|
||||
func (Callable): The function to run before model initialization.
|
||||
|
||||
Returns:
|
||||
Any: The decorated function.
|
||||
"""
|
||||
|
||||
@root_validator(pre=True)
|
||||
@wraps(func)
|
||||
def wrapper(cls: Type[BaseModel], values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Decorator to run a function before model initialization."""
|
||||
"""Decorator to run a function before model initialization.
|
||||
|
||||
Args:
|
||||
cls (Type[BaseModel]): The model class.
|
||||
values (Dict[str, Any]): The values to initialize the model with.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The values to initialize the model with.
|
||||
"""
|
||||
# Insert default values
|
||||
fields = cls.__fields__
|
||||
for name, field_info in fields.items():
|
||||
|
||||
@@ -36,5 +36,12 @@ def stringify_dict(data: dict) -> str:
|
||||
|
||||
|
||||
def comma_list(items: List[Any]) -> str:
|
||||
"""Convert a list to a comma-separated string."""
|
||||
"""Convert a list to a comma-separated string.
|
||||
|
||||
Args:
|
||||
items: The list to convert.
|
||||
|
||||
Returns:
|
||||
str: The comma-separated string.
|
||||
"""
|
||||
return ", ".join(str(item) for item in items)
|
||||
|
||||
@@ -15,7 +15,18 @@ from langchain_core.pydantic_v1 import SecretStr
|
||||
|
||||
|
||||
def xor_args(*arg_groups: Tuple[str, ...]) -> Callable:
|
||||
"""Validate specified keyword args are mutually exclusive."""
|
||||
"""Validate specified keyword args are mutually exclusive."
|
||||
|
||||
Args:
|
||||
*arg_groups (Tuple[str, ...]): Groups of mutually exclusive keyword args.
|
||||
|
||||
Returns:
|
||||
Callable: Decorator that validates the specified keyword args
|
||||
are mutually exclusive
|
||||
|
||||
Raises:
|
||||
ValueError: If more than one arg in a group is defined.
|
||||
"""
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
@@ -41,7 +52,14 @@ def xor_args(*arg_groups: Tuple[str, ...]) -> Callable:
|
||||
|
||||
|
||||
def raise_for_status_with_text(response: Response) -> None:
|
||||
"""Raise an error with the response text."""
|
||||
"""Raise an error with the response text.
|
||||
|
||||
Args:
|
||||
response (Response): The response to check for errors.
|
||||
|
||||
Raises:
|
||||
ValueError: If the response has an error status code.
|
||||
"""
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except HTTPError as e:
|
||||
@@ -52,6 +70,12 @@ def raise_for_status_with_text(response: Response) -> None:
|
||||
def mock_now(dt_value): # type: ignore
|
||||
"""Context manager for mocking out datetime.now() in unit tests.
|
||||
|
||||
Args:
|
||||
dt_value: The datetime value to use for datetime.now().
|
||||
|
||||
Yields:
|
||||
datetime.datetime: The mocked datetime class.
|
||||
|
||||
Example:
|
||||
with mock_now(datetime.datetime(2011, 2, 3, 10, 11)):
|
||||
assert datetime.datetime.now() == datetime.datetime(2011, 2, 3, 10, 11)
|
||||
@@ -86,7 +110,21 @@ def guard_import(
|
||||
module_name: str, *, pip_name: Optional[str] = None, package: Optional[str] = None
|
||||
) -> Any:
|
||||
"""Dynamically import a module and raise an exception if the module is not
|
||||
installed."""
|
||||
installed.
|
||||
|
||||
Args:
|
||||
module_name (str): The name of the module to import.
|
||||
pip_name (str, optional): The name of the module to install with pip.
|
||||
Defaults to None.
|
||||
package (str, optional): The package to import the module from.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
Any: The imported module.
|
||||
|
||||
Raises:
|
||||
ImportError: If the module is not installed.
|
||||
"""
|
||||
try:
|
||||
module = importlib.import_module(module_name, package)
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
@@ -105,7 +143,22 @@ def check_package_version(
|
||||
gt_version: Optional[str] = None,
|
||||
gte_version: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Check the version of a package."""
|
||||
"""Check the version of a package.
|
||||
|
||||
Args:
|
||||
package (str): The name of the package.
|
||||
lt_version (str, optional): The version must be less than this.
|
||||
Defaults to None.
|
||||
lte_version (str, optional): The version must be less than or equal to this.
|
||||
Defaults to None.
|
||||
gt_version (str, optional): The version must be greater than this.
|
||||
Defaults to None.
|
||||
gte_version (str, optional): The version must be greater than or equal to this.
|
||||
Defaults to None.
|
||||
|
||||
Raises:
|
||||
ValueError: If the package version does not meet the requirements.
|
||||
"""
|
||||
imported_version = parse(version(package))
|
||||
if lt_version is not None and imported_version >= parse(lt_version):
|
||||
raise ValueError(
|
||||
@@ -133,7 +186,11 @@ def get_pydantic_field_names(pydantic_cls: Any) -> Set[str]:
|
||||
"""Get field names, including aliases, for a pydantic class.
|
||||
|
||||
Args:
|
||||
pydantic_cls: Pydantic class."""
|
||||
pydantic_cls: Pydantic class.
|
||||
|
||||
Returns:
|
||||
Set[str]: Field names.
|
||||
"""
|
||||
all_required_field_names = set()
|
||||
for field in pydantic_cls.__fields__.values():
|
||||
all_required_field_names.add(field.name)
|
||||
@@ -153,6 +210,13 @@ def build_extra_kwargs(
|
||||
extra_kwargs: Extra kwargs passed in by user.
|
||||
values: Values passed in by user.
|
||||
all_required_field_names: All required field names for the pydantic class.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Extra kwargs.
|
||||
|
||||
Raises:
|
||||
ValueError: If a field is specified in both values and extra_kwargs.
|
||||
ValueError: If a field is specified in model_kwargs.
|
||||
"""
|
||||
for field_name in list(values):
|
||||
if field_name in extra_kwargs:
|
||||
@@ -176,7 +240,14 @@ def build_extra_kwargs(
|
||||
|
||||
|
||||
def convert_to_secret_str(value: Union[SecretStr, str]) -> SecretStr:
|
||||
"""Convert a string to a SecretStr if needed."""
|
||||
"""Convert a string to a SecretStr if needed.
|
||||
|
||||
Args:
|
||||
value (Union[SecretStr, str]): The value to convert.
|
||||
|
||||
Returns:
|
||||
SecretStr: The SecretStr value.
|
||||
"""
|
||||
if isinstance(value, SecretStr):
|
||||
return value
|
||||
return SecretStr(value)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "langchain-core"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
description = "Building applications with LLMs through composability"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -279,7 +279,7 @@ class CustomChat(GenericFakeChatModel):
|
||||
async def test_can_swap_caches() -> None:
|
||||
"""Test that we can use a different cache object.
|
||||
|
||||
This test verifies that when we fetch teh llm_string representation
|
||||
This test verifies that when we fetch the llm_string representation
|
||||
of the chat model, we can swap the cache object and still get the same
|
||||
result.
|
||||
"""
|
||||
|
||||
@@ -127,7 +127,6 @@ _MESSAGES_TO_TRIM = [
|
||||
HumanMessage("This is a 4 token text.", id="third"),
|
||||
AIMessage("This is a 4 token text.", id="fourth"),
|
||||
]
|
||||
|
||||
_MESSAGES_TO_TRIM_COPY = [m.copy(deep=True) for m in _MESSAGES_TO_TRIM]
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from langchain_core.callbacks import (
|
||||
CallbackManagerForToolRun,
|
||||
)
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langchain_core.pydantic_v1 import BaseModel, ValidationError
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableConfig,
|
||||
@@ -1137,7 +1137,9 @@ def test_tool_call_input_tool_message_output() -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
tool = _MockStructuredTool()
|
||||
expected = ToolMessage("1 True {'img': 'base64string...'}", tool_call_id="123")
|
||||
expected = ToolMessage(
|
||||
"1 True {'img': 'base64string...'}", tool_call_id="123", name="structured_api"
|
||||
)
|
||||
actual = tool.invoke(tool_call)
|
||||
assert actual == expected
|
||||
|
||||
@@ -1176,7 +1178,9 @@ def test_tool_call_input_tool_message_with_artifact(tool: BaseTool) -> None:
|
||||
"id": "123",
|
||||
"type": "tool_call",
|
||||
}
|
||||
expected = ToolMessage("1 True", artifact=tool_call["args"], tool_call_id="123")
|
||||
expected = ToolMessage(
|
||||
"1 True", artifact=tool_call["args"], tool_call_id="123", name="structured_api"
|
||||
)
|
||||
actual = tool.invoke(tool_call)
|
||||
assert actual == expected
|
||||
|
||||
@@ -1218,10 +1222,22 @@ def test_convert_from_runnable_dict() -> None:
|
||||
assert as_tool.name == "my tool"
|
||||
assert as_tool.description == "test description"
|
||||
|
||||
# Dict without typed input-- must supply arg types
|
||||
# Dict without typed input-- must supply schema
|
||||
def g(x: Dict[str, Any]) -> str:
|
||||
return str(x["a"] * max(x["b"]))
|
||||
|
||||
# Specify via args_schema:
|
||||
class GSchema(BaseModel):
|
||||
"""Apply a function to an integer and list of integers."""
|
||||
|
||||
a: int = Field(..., description="Integer")
|
||||
b: List[int] = Field(..., description="List of ints")
|
||||
|
||||
runnable = RunnableLambda(g)
|
||||
as_tool = runnable.as_tool(GSchema)
|
||||
as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
|
||||
# Specify via arg_types:
|
||||
runnable = RunnableLambda(g)
|
||||
as_tool = runnable.as_tool(arg_types={"a": int, "b": List[int]})
|
||||
result = as_tool.invoke({"a": 3, "b": [1, 2]})
|
||||
|
||||
@@ -1,34 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from importlib import util
|
||||
from typing import Any, Optional
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from langchain_core._api import beta
|
||||
from langchain_core.language_models.chat_models import (
|
||||
from langchain_core.language_models import (
|
||||
BaseChatModel,
|
||||
LanguageModelInput,
|
||||
SimpleChatModel,
|
||||
)
|
||||
from langchain_core.language_models.chat_models import (
|
||||
agenerate_from_stream,
|
||||
generate_from_stream,
|
||||
)
|
||||
from langchain_core.messages import AnyMessage, BaseMessage
|
||||
from langchain_core.pydantic_v1 import BaseModel
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables.schema import StreamEvent
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.tracers import RunLog, RunLogPatch
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
__all__ = [
|
||||
"init_chat_model",
|
||||
# For backwards compatibility
|
||||
"BaseChatModel",
|
||||
"SimpleChatModel",
|
||||
"generate_from_stream",
|
||||
"agenerate_from_stream",
|
||||
"init_chat_model",
|
||||
]
|
||||
|
||||
|
||||
@overload
|
||||
def init_chat_model( # type: ignore[overload-overlap]
|
||||
model: str,
|
||||
*,
|
||||
model_provider: Optional[str] = None,
|
||||
configurable_fields: Literal[None] = None,
|
||||
config_prefix: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> BaseChatModel: ...
|
||||
|
||||
|
||||
@overload
|
||||
def init_chat_model(
|
||||
model: Literal[None] = None,
|
||||
*,
|
||||
model_provider: Optional[str] = None,
|
||||
configurable_fields: Literal[None] = None,
|
||||
config_prefix: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> _ConfigurableModel: ...
|
||||
|
||||
|
||||
@overload
|
||||
def init_chat_model(
|
||||
model: Optional[str] = None,
|
||||
*,
|
||||
model_provider: Optional[str] = None,
|
||||
configurable_fields: Union[Literal["any"], List[str], Tuple[str, ...]] = ...,
|
||||
config_prefix: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> _ConfigurableModel: ...
|
||||
|
||||
|
||||
# FOR CONTRIBUTORS: If adding support for a new provider, please append the provider
|
||||
# name to the supported list in the docstring below. Do *not* change the order of the
|
||||
# existing providers.
|
||||
@beta()
|
||||
def init_chat_model(
|
||||
model: str, *, model_provider: Optional[str] = None, **kwargs: Any
|
||||
) -> BaseChatModel:
|
||||
model: Optional[str] = None,
|
||||
*,
|
||||
model_provider: Optional[str] = None,
|
||||
configurable_fields: Optional[
|
||||
Union[Literal["any"], List[str], Tuple[str, ...]]
|
||||
] = None,
|
||||
config_prefix: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[BaseChatModel, _ConfigurableModel]:
|
||||
"""Initialize a ChatModel from the model name and provider.
|
||||
|
||||
Must have the integration package corresponding to the model provider installed.
|
||||
|
||||
.. versionadded:: 0.2.7
|
||||
|
||||
.. versionchanged:: 0.2.8
|
||||
|
||||
Args:
|
||||
model: The name of the model, e.g. "gpt-4o", "claude-3-opus-20240229".
|
||||
model_provider: The model provider. Supported model_provider values and the
|
||||
@@ -55,19 +128,43 @@ def init_chat_model(
|
||||
- gemini... -> google_vertexai
|
||||
- command... -> cohere
|
||||
- accounts/fireworks... -> fireworks
|
||||
configurable_fields: Which model parameters are
|
||||
configurable:
|
||||
- None: No configurable fields.
|
||||
- "any": All fields are configurable. *See Security Note below.*
|
||||
- Union[List[str], Tuple[str, ...]]: Specified fields are configurable.
|
||||
|
||||
Fields are assumed to have config_prefix stripped if there is a
|
||||
config_prefix. If model is specified, then defaults to None. If model is
|
||||
not specified, then defaults to ``("model", "model_provider")``.
|
||||
|
||||
***Security Note***: Setting ``configurable_fields="any"`` means fields like
|
||||
api_key, base_url, etc. can be altered at runtime, potentially redirecting
|
||||
model requests to a different service/user. Make sure that if you're
|
||||
accepting untrusted configurations that you enumerate the
|
||||
``configurable_fields=(...)`` explicitly.
|
||||
|
||||
config_prefix: If config_prefix is a non-empty string then model will be
|
||||
configurable at runtime via the
|
||||
``config["configurable"]["{config_prefix}_{param}"]`` keys. If
|
||||
config_prefix is an empty string then model will be configurable via
|
||||
``config["configurable"]["{param}"]``.
|
||||
kwargs: Additional keyword args to pass to
|
||||
``<<selected ChatModel>>.__init__(model=model_name, **kwargs)``.
|
||||
|
||||
Returns:
|
||||
The BaseChatModel corresponding to the model_name and model_provider specified.
|
||||
A BaseChatModel corresponding to the model_name and model_provider specified if
|
||||
configurability is inferred to be False. If configurable, a chat model emulator
|
||||
that initializes the underlying model at runtime once a config is passed in.
|
||||
|
||||
Raises:
|
||||
ValueError: If model_provider cannot be inferred or isn't supported.
|
||||
ImportError: If the model provider integration package is not installed.
|
||||
|
||||
Example:
|
||||
Initialize non-configurable models:
|
||||
.. code-block:: python
|
||||
|
||||
# pip install langchain langchain-openai langchain-anthropic langchain-google-vertexai
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
gpt_4o = init_chat_model("gpt-4o", model_provider="openai", temperature=0)
|
||||
@@ -77,7 +174,125 @@ def init_chat_model(
|
||||
gpt_4o.invoke("what's your name")
|
||||
claude_opus.invoke("what's your name")
|
||||
gemini_15.invoke("what's your name")
|
||||
|
||||
|
||||
Create a partially configurable model with no default model:
|
||||
.. code-block:: python
|
||||
|
||||
# pip install langchain langchain-openai langchain-anthropic
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
# We don't need to specify configurable=True if a model isn't specified.
|
||||
configurable_model = init_chat_model(temperature=0)
|
||||
|
||||
configurable_model.invoke(
|
||||
"what's your name",
|
||||
config={"configurable": {"model": "gpt-4o"}}
|
||||
)
|
||||
# GPT-4o response
|
||||
|
||||
configurable_model.invoke(
|
||||
"what's your name",
|
||||
config={"configurable": {"model": "claude-3-5-sonnet-20240620"}}
|
||||
)
|
||||
# claude-3.5 sonnet response
|
||||
|
||||
Create a fully configurable model with a default model and a config prefix:
|
||||
.. code-block:: python
|
||||
|
||||
# pip install langchain langchain-openai langchain-anthropic
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
configurable_model_with_default = init_chat_model(
|
||||
"gpt-4o",
|
||||
model_provider="openai",
|
||||
configurable_fields="any", # this allows us to configure other params like temperature, max_tokens, etc at runtime.
|
||||
config_prefix="foo",
|
||||
temperature=0
|
||||
)
|
||||
|
||||
configurable_model_with_default.invoke("what's your name")
|
||||
# GPT-4o response with temperature 0
|
||||
|
||||
configurable_model_with_default.invoke(
|
||||
"what's your name",
|
||||
config={
|
||||
"configurable": {
|
||||
"foo_model": "claude-3-5-sonnet-20240620",
|
||||
"foo_model_provider": "anthropic",
|
||||
"foo_temperature": 0.6
|
||||
}
|
||||
}
|
||||
)
|
||||
# Claude-3.5 sonnet response with temperature 0.6
|
||||
|
||||
Bind tools to a configurable model:
|
||||
You can call any ChatModel declarative methods on a configurable model in the
|
||||
same way that you would with a normal model.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# pip install langchain langchain-openai langchain-anthropic
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field
|
||||
|
||||
class GetWeather(BaseModel):
|
||||
'''Get the current weather in a given location'''
|
||||
|
||||
location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
|
||||
|
||||
class GetPopulation(BaseModel):
|
||||
'''Get the current population in a given location'''
|
||||
|
||||
location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
|
||||
|
||||
configurable_model = init_chat_model(
|
||||
"gpt-4o",
|
||||
configurable_fields=("model", "model_provider"),
|
||||
temperature=0
|
||||
)
|
||||
|
||||
configurable_model_with_tools = configurable_model.bind_tools([GetWeather, GetPopulation])
|
||||
configurable_model_with_tools.invoke(
|
||||
"Which city is hotter today and which is bigger: LA or NY?"
|
||||
)
|
||||
# GPT-4o response with tool calls
|
||||
|
||||
configurable_model_with_tools.invoke(
|
||||
"Which city is hotter today and which is bigger: LA or NY?",
|
||||
config={"configurable": {"model": "claude-3-5-sonnet-20240620"}}
|
||||
)
|
||||
# Claude-3.5 sonnet response with tools
|
||||
""" # noqa: E501
|
||||
if not model and not configurable_fields:
|
||||
configurable_fields = ("model", "model_provider")
|
||||
config_prefix = config_prefix or ""
|
||||
if config_prefix and not configurable_fields:
|
||||
warnings.warn(
|
||||
f"{config_prefix=} has been set but no fields are configurable. Set "
|
||||
f"`configurable_fields=(...)` to specify the model params that are "
|
||||
f"configurable."
|
||||
)
|
||||
|
||||
if not configurable_fields:
|
||||
return _init_chat_model_helper(
|
||||
cast(str, model), model_provider=model_provider, **kwargs
|
||||
)
|
||||
else:
|
||||
if model:
|
||||
kwargs["model"] = model
|
||||
if model_provider:
|
||||
kwargs["model_provider"] = model_provider
|
||||
return _ConfigurableModel(
|
||||
default_config=kwargs,
|
||||
config_prefix=config_prefix,
|
||||
configurable_fields=configurable_fields,
|
||||
)
|
||||
|
||||
|
||||
def _init_chat_model_helper(
|
||||
model: str, *, model_provider: Optional[str] = None, **kwargs: Any
|
||||
) -> BaseChatModel:
|
||||
model_provider = model_provider or _attempt_infer_model_provider(model)
|
||||
if not model_provider:
|
||||
raise ValueError(
|
||||
@@ -200,3 +415,386 @@ def _check_pkg(pkg: str) -> None:
|
||||
f"Unable to import {pkg_kebab}. Please install with "
|
||||
f"`pip install -U {pkg_kebab}`"
|
||||
)
|
||||
|
||||
|
||||
def _remove_prefix(s: str, prefix: str) -> str:
|
||||
if s.startswith(prefix):
|
||||
s = s[len(prefix) :]
|
||||
return s
|
||||
|
||||
|
||||
_DECLARATIVE_METHODS = ("bind_tools", "with_structured_output")
|
||||
|
||||
|
||||
class _ConfigurableModel(Runnable[LanguageModelInput, Any]):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
default_config: Optional[dict] = None,
|
||||
configurable_fields: Union[Literal["any"], List[str], Tuple[str, ...]] = "any",
|
||||
config_prefix: str = "",
|
||||
queued_declarative_operations: Sequence[Tuple[str, Tuple, Dict]] = (),
|
||||
) -> None:
|
||||
self._default_config: dict = default_config or {}
|
||||
self._configurable_fields: Union[Literal["any"], List[str]] = (
|
||||
configurable_fields
|
||||
if configurable_fields == "any"
|
||||
else list(configurable_fields)
|
||||
)
|
||||
self._config_prefix = (
|
||||
config_prefix + "_"
|
||||
if config_prefix and not config_prefix.endswith("_")
|
||||
else config_prefix
|
||||
)
|
||||
self._queued_declarative_operations: List[Tuple[str, Tuple, Dict]] = list(
|
||||
queued_declarative_operations
|
||||
)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name in _DECLARATIVE_METHODS:
|
||||
# Declarative operations that cannot be applied until after an actual model
|
||||
# object is instantiated. So instead of returning the actual operation,
|
||||
# we record the operation and its arguments in a queue. This queue is
|
||||
# then applied in order whenever we actually instantiate the model (in
|
||||
# self._model()).
|
||||
def queue(*args: Any, **kwargs: Any) -> _ConfigurableModel:
|
||||
queued_declarative_operations = list(
|
||||
self._queued_declarative_operations
|
||||
)
|
||||
queued_declarative_operations.append((name, args, kwargs))
|
||||
return _ConfigurableModel(
|
||||
default_config=dict(self._default_config),
|
||||
configurable_fields=list(self._configurable_fields)
|
||||
if isinstance(self._configurable_fields, list)
|
||||
else self._configurable_fields,
|
||||
config_prefix=self._config_prefix,
|
||||
queued_declarative_operations=queued_declarative_operations,
|
||||
)
|
||||
|
||||
return queue
|
||||
elif self._default_config and (model := self._model()) and hasattr(model, name):
|
||||
return getattr(model, name)
|
||||
else:
|
||||
msg = f"{name} is not a BaseChatModel attribute"
|
||||
if self._default_config:
|
||||
msg += " and is not implemented on the default model"
|
||||
msg += "."
|
||||
raise AttributeError(msg)
|
||||
|
||||
def _model(self, config: Optional[RunnableConfig] = None) -> Runnable:
|
||||
params = {**self._default_config, **self._model_params(config)}
|
||||
model = _init_chat_model_helper(**params)
|
||||
for name, args, kwargs in self._queued_declarative_operations:
|
||||
model = getattr(model, name)(*args, **kwargs)
|
||||
return model
|
||||
|
||||
def _model_params(self, config: Optional[RunnableConfig]) -> dict:
|
||||
config = config or {}
|
||||
model_params = {
|
||||
_remove_prefix(k, self._config_prefix): v
|
||||
for k, v in config.get("configurable", {}).items()
|
||||
if k.startswith(self._config_prefix)
|
||||
}
|
||||
if self._configurable_fields != "any":
|
||||
model_params = {
|
||||
k: v for k, v in model_params.items() if k in self._configurable_fields
|
||||
}
|
||||
return model_params
|
||||
|
||||
def with_config(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Any,
|
||||
) -> _ConfigurableModel:
|
||||
"""Bind config to a Runnable, returning a new Runnable."""
|
||||
config = RunnableConfig(**(config or {}), **cast(RunnableConfig, kwargs))
|
||||
model_params = self._model_params(config)
|
||||
remaining_config = {k: v for k, v in config.items() if k != "configurable"}
|
||||
remaining_config["configurable"] = {
|
||||
k: v
|
||||
for k, v in config.get("configurable", {}).items()
|
||||
if _remove_prefix(k, self._config_prefix) not in model_params
|
||||
}
|
||||
queued_declarative_operations = list(self._queued_declarative_operations)
|
||||
if remaining_config:
|
||||
queued_declarative_operations.append(
|
||||
("with_config", (), {"config": remaining_config})
|
||||
)
|
||||
return _ConfigurableModel(
|
||||
default_config={**self._default_config, **model_params},
|
||||
configurable_fields=list(self._configurable_fields)
|
||||
if isinstance(self._configurable_fields, list)
|
||||
else self._configurable_fields,
|
||||
config_prefix=self._config_prefix,
|
||||
queued_declarative_operations=queued_declarative_operations,
|
||||
)
|
||||
|
||||
@property
|
||||
def InputType(self) -> TypeAlias:
|
||||
"""Get the input type for this runnable."""
|
||||
from langchain_core.prompt_values import (
|
||||
ChatPromptValueConcrete,
|
||||
StringPromptValue,
|
||||
)
|
||||
|
||||
# This is a version of LanguageModelInput which replaces the abstract
|
||||
# base class BaseMessage with a union of its subclasses, which makes
|
||||
# for a much better schema.
|
||||
return Union[
|
||||
str,
|
||||
Union[StringPromptValue, ChatPromptValueConcrete],
|
||||
List[AnyMessage],
|
||||
]
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
input: LanguageModelInput,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
return self._model(config).invoke(input, config=config, **kwargs)
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: LanguageModelInput,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
return await self._model(config).ainvoke(input, config=config, **kwargs)
|
||||
|
||||
def stream(
|
||||
self,
|
||||
input: LanguageModelInput,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Iterator[Any]:
|
||||
yield from self._model(config).stream(input, config=config, **kwargs)
|
||||
|
||||
async def astream(
|
||||
self,
|
||||
input: LanguageModelInput,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> AsyncIterator[Any]:
|
||||
async for x in self._model(config).astream(input, config=config, **kwargs):
|
||||
yield x
|
||||
|
||||
def batch(
|
||||
self,
|
||||
inputs: List[LanguageModelInput],
|
||||
config: Optional[Union[RunnableConfig, List[RunnableConfig]]] = None,
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Optional[Any],
|
||||
) -> List[Any]:
|
||||
config = config or None
|
||||
# If <= 1 config use the underlying models batch implementation.
|
||||
if config is None or isinstance(config, dict) or len(config) <= 1:
|
||||
if isinstance(config, list):
|
||||
config = config[0]
|
||||
return self._model(config).batch(
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
)
|
||||
# If multiple configs default to Runnable.batch which uses executor to invoke
|
||||
# in parallel.
|
||||
else:
|
||||
return super().batch(
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
)
|
||||
|
||||
async def abatch(
|
||||
self,
|
||||
inputs: List[LanguageModelInput],
|
||||
config: Optional[Union[RunnableConfig, List[RunnableConfig]]] = None,
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Optional[Any],
|
||||
) -> List[Any]:
|
||||
config = config or None
|
||||
# If <= 1 config use the underlying models batch implementation.
|
||||
if config is None or isinstance(config, dict) or len(config) <= 1:
|
||||
if isinstance(config, list):
|
||||
config = config[0]
|
||||
return await self._model(config).abatch(
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
)
|
||||
# If multiple configs default to Runnable.batch which uses executor to invoke
|
||||
# in parallel.
|
||||
else:
|
||||
return await super().abatch(
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
)
|
||||
|
||||
def batch_as_completed(
|
||||
self,
|
||||
inputs: Sequence[LanguageModelInput],
|
||||
config: Optional[Union[RunnableConfig, Sequence[RunnableConfig]]] = None,
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[Tuple[int, Union[Any, Exception]]]:
|
||||
config = config or None
|
||||
# If <= 1 config use the underlying models batch implementation.
|
||||
if config is None or isinstance(config, dict) or len(config) <= 1:
|
||||
if isinstance(config, list):
|
||||
config = config[0]
|
||||
yield from self._model(cast(RunnableConfig, config)).batch_as_completed( # type: ignore[call-overload]
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
)
|
||||
# If multiple configs default to Runnable.batch which uses executor to invoke
|
||||
# in parallel.
|
||||
else:
|
||||
yield from super().batch_as_completed( # type: ignore[call-overload]
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
)
|
||||
|
||||
async def abatch_as_completed(
|
||||
self,
|
||||
inputs: Sequence[LanguageModelInput],
|
||||
config: Optional[Union[RunnableConfig, Sequence[RunnableConfig]]] = None,
|
||||
*,
|
||||
return_exceptions: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[Tuple[int, Any]]:
|
||||
config = config or None
|
||||
# If <= 1 config use the underlying models batch implementation.
|
||||
if config is None or isinstance(config, dict) or len(config) <= 1:
|
||||
if isinstance(config, list):
|
||||
config = config[0]
|
||||
async for x in self._model(
|
||||
cast(RunnableConfig, config)
|
||||
).abatch_as_completed( # type: ignore[call-overload]
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
):
|
||||
yield x
|
||||
# If multiple configs default to Runnable.batch which uses executor to invoke
|
||||
# in parallel.
|
||||
else:
|
||||
async for x in super().abatch_as_completed( # type: ignore[call-overload]
|
||||
inputs, config=config, return_exceptions=return_exceptions, **kwargs
|
||||
):
|
||||
yield x
|
||||
|
||||
def transform(
|
||||
self,
|
||||
input: Iterator[LanguageModelInput],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Iterator[Any]:
|
||||
for x in self._model(config).transform(input, config=config, **kwargs):
|
||||
yield x
|
||||
|
||||
async def atransform(
|
||||
self,
|
||||
input: AsyncIterator[LanguageModelInput],
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> AsyncIterator[Any]:
|
||||
async for x in self._model(config).atransform(input, config=config, **kwargs):
|
||||
yield x
|
||||
|
||||
@overload
|
||||
def astream_log(
|
||||
self,
|
||||
input: Any,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
diff: Literal[True] = True,
|
||||
with_streamed_output_list: bool = True,
|
||||
include_names: Optional[Sequence[str]] = None,
|
||||
include_types: Optional[Sequence[str]] = None,
|
||||
include_tags: Optional[Sequence[str]] = None,
|
||||
exclude_names: Optional[Sequence[str]] = None,
|
||||
exclude_types: Optional[Sequence[str]] = None,
|
||||
exclude_tags: Optional[Sequence[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[RunLogPatch]: ...
|
||||
|
||||
@overload
|
||||
def astream_log(
|
||||
self,
|
||||
input: Any,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
diff: Literal[False],
|
||||
with_streamed_output_list: bool = True,
|
||||
include_names: Optional[Sequence[str]] = None,
|
||||
include_types: Optional[Sequence[str]] = None,
|
||||
include_tags: Optional[Sequence[str]] = None,
|
||||
exclude_names: Optional[Sequence[str]] = None,
|
||||
exclude_types: Optional[Sequence[str]] = None,
|
||||
exclude_tags: Optional[Sequence[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[RunLog]: ...
|
||||
|
||||
async def astream_log(
|
||||
self,
|
||||
input: Any,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
diff: bool = True,
|
||||
with_streamed_output_list: bool = True,
|
||||
include_names: Optional[Sequence[str]] = None,
|
||||
include_types: Optional[Sequence[str]] = None,
|
||||
include_tags: Optional[Sequence[str]] = None,
|
||||
exclude_names: Optional[Sequence[str]] = None,
|
||||
exclude_types: Optional[Sequence[str]] = None,
|
||||
exclude_tags: Optional[Sequence[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[AsyncIterator[RunLogPatch], AsyncIterator[RunLog]]:
|
||||
async for x in self._model(config).astream_log( # type: ignore[call-overload, misc]
|
||||
input,
|
||||
config=config,
|
||||
diff=diff,
|
||||
with_streamed_output_list=with_streamed_output_list,
|
||||
include_names=include_names,
|
||||
include_types=include_types,
|
||||
include_tags=include_tags,
|
||||
exclude_tags=exclude_tags,
|
||||
exclude_types=exclude_types,
|
||||
exclude_names=exclude_names,
|
||||
**kwargs,
|
||||
):
|
||||
yield x
|
||||
|
||||
async def astream_events(
|
||||
self,
|
||||
input: Any,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
version: Literal["v1", "v2"],
|
||||
include_names: Optional[Sequence[str]] = None,
|
||||
include_types: Optional[Sequence[str]] = None,
|
||||
include_tags: Optional[Sequence[str]] = None,
|
||||
exclude_names: Optional[Sequence[str]] = None,
|
||||
exclude_types: Optional[Sequence[str]] = None,
|
||||
exclude_tags: Optional[Sequence[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[StreamEvent]:
|
||||
async for x in self._model(config).astream_events(
|
||||
input,
|
||||
config=config,
|
||||
version=version,
|
||||
include_names=include_names,
|
||||
include_types=include_types,
|
||||
include_tags=include_tags,
|
||||
exclude_tags=exclude_tags,
|
||||
exclude_types=exclude_types,
|
||||
exclude_names=exclude_names,
|
||||
**kwargs,
|
||||
):
|
||||
yield x
|
||||
|
||||
# Explicitly added to satisfy downstream linters.
|
||||
def bind_tools(
|
||||
self,
|
||||
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
return self.__getattr__("bind_tools")(tools, **kwargs)
|
||||
|
||||
# Explicitly added to satisfy downstream linters.
|
||||
def with_structured_output(
|
||||
self, schema: Union[Dict, Type[BaseModel]], **kwargs: Any
|
||||
) -> Runnable[LanguageModelInput, Union[Dict, BaseModel]]:
|
||||
return self.__getattr__("with_structured_output")(schema, **kwargs)
|
||||
|
||||
87
libs/langchain/poetry.lock
generated
87
libs/langchain/poetry.lock
generated
@@ -1760,7 +1760,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.2.13"
|
||||
version = "0.2.19"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = ">=3.8.1,<4.0"
|
||||
@@ -1784,7 +1784,7 @@ url = "../core"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
description = "An integration package connecting OpenAI and LangChain"
|
||||
optional = true
|
||||
python-versions = ">=3.8.1,<4.0"
|
||||
@@ -1792,7 +1792,7 @@ files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = "^0.2.13"
|
||||
langchain-core = "^0.2.17"
|
||||
openai = "^1.32.0"
|
||||
tiktoken = ">=0.7,<1"
|
||||
|
||||
@@ -1800,6 +1800,24 @@ tiktoken = ">=0.7,<1"
|
||||
type = "directory"
|
||||
url = "../partners/openai"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-standard-tests"
|
||||
version = "0.1.1"
|
||||
description = "Standard tests for LangChain implementations"
|
||||
optional = false
|
||||
python-versions = ">=3.8.1,<4.0"
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
httpx = "^0.27.0"
|
||||
langchain-core = ">=0.1.40,<0.3"
|
||||
pytest = ">=7,<9"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../standard-tests"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-text-splitters"
|
||||
version = "0.2.2"
|
||||
@@ -2490,8 +2508,8 @@ files = [
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.20.3", markers = "python_version < \"3.10\""},
|
||||
{version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
|
||||
{version = ">=1.23.2", markers = "python_version >= \"3.11\""},
|
||||
{version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
|
||||
]
|
||||
python-dateutil = ">=2.8.2"
|
||||
pytz = ">=2020.1"
|
||||
@@ -4111,20 +4129,6 @@ files = [
|
||||
cryptography = ">=35.0.0"
|
||||
types-pyOpenSSL = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.31.0.6"
|
||||
description = "Typing stubs for requests"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"},
|
||||
{file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
types-urllib3 = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.32.0.20240622"
|
||||
@@ -4161,17 +4165,6 @@ files = [
|
||||
{file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-urllib3"
|
||||
version = "1.26.25.14"
|
||||
description = "Typing stubs for urllib3"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"},
|
||||
{file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
@@ -4208,22 +4201,6 @@ files = [
|
||||
[package.extras]
|
||||
dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.19"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
files = [
|
||||
{file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"},
|
||||
{file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
||||
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.2.2"
|
||||
@@ -4241,6 +4218,23 @@ h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "vcrpy"
|
||||
version = "4.3.0"
|
||||
description = "Automatically mock your HTTP interactions to simplify and speed up testing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "vcrpy-4.3.0-py2.py3-none-any.whl", hash = "sha256:8fbd4be412e8a7f35f623dd61034e6380a1c8dbd0edf6e87277a3289f6e98093"},
|
||||
{file = "vcrpy-4.3.0.tar.gz", hash = "sha256:49c270ce67e826dba027d83e20d25b67a5885487697e97bca6dbdf53d750a0ac"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
PyYAML = "*"
|
||||
six = ">=1.5"
|
||||
wrapt = "*"
|
||||
yarl = "*"
|
||||
|
||||
[[package]]
|
||||
name = "vcrpy"
|
||||
version = "6.0.1"
|
||||
@@ -4253,7 +4247,6 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
PyYAML = "*"
|
||||
urllib3 = {version = "<2", markers = "platform_python_implementation == \"PyPy\" or python_version < \"3.10\""}
|
||||
wrapt = "*"
|
||||
yarl = "*"
|
||||
|
||||
@@ -4568,4 +4561,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.8.1,<4.0"
|
||||
content-hash = "30237e9280ade99d7c7741aec1b3d38a8e1ccb24a3d0c4380d48ae80ab86a136"
|
||||
content-hash = "dbfb4729eead4be01e0cfb99e4a4a4969e1bf5c9cf7a752d8fdc53593808948c"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "langchain"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
description = "Building applications with LLMs through composability"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -29,7 +29,7 @@ langchain-server = "langchain.server:main"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8.1,<4.0"
|
||||
langchain-core = "^0.2.12"
|
||||
langchain-core = "^0.2.19"
|
||||
langchain-text-splitters = "^0.2.0"
|
||||
langsmith = "^0.1.17"
|
||||
pydantic = ">=1,<3"
|
||||
@@ -123,6 +123,10 @@ jupyter = "^1.0.0"
|
||||
playwright = "^1.28.0"
|
||||
setuptools = "^67.6.1"
|
||||
|
||||
[tool.poetry.group.test.dependencies.langchain-standard-tests]
|
||||
path = "../standard-tests"
|
||||
develop = true
|
||||
|
||||
[tool.poetry.group.test.dependencies.langchain-core]
|
||||
path = "../core"
|
||||
develop = true
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from typing import Type, cast
|
||||
|
||||
import pytest
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.pydantic_v1 import BaseModel
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_standard_tests.integration_tests import ChatModelIntegrationTests
|
||||
|
||||
from langchain.chat_models import init_chat_model
|
||||
|
||||
|
||||
class multiply(BaseModel):
|
||||
"""Product of two ints."""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
@pytest.mark.requires("langchain_openai", "langchain_anthropic")
|
||||
async def test_init_chat_model_chain() -> None:
|
||||
model = init_chat_model("gpt-4o", configurable_fields="any", config_prefix="bar")
|
||||
model_with_tools = model.bind_tools([multiply])
|
||||
|
||||
model_with_config = model_with_tools.with_config(
|
||||
RunnableConfig(tags=["foo"]),
|
||||
configurable={"bar_model": "claude-3-sonnet-20240229"},
|
||||
)
|
||||
prompt = ChatPromptTemplate.from_messages([("system", "foo"), ("human", "{input}")])
|
||||
chain = prompt | model_with_config
|
||||
output = chain.invoke({"input": "bar"})
|
||||
assert isinstance(output, AIMessage)
|
||||
events = []
|
||||
async for event in chain.astream_events({"input": "bar"}, version="v2"):
|
||||
events.append(event)
|
||||
assert events
|
||||
|
||||
|
||||
class TestStandard(ChatModelIntegrationTests):
|
||||
@property
|
||||
def chat_model_class(self) -> Type[BaseChatModel]:
|
||||
return cast(Type[BaseChatModel], init_chat_model)
|
||||
|
||||
@property
|
||||
def chat_model_params(self) -> dict:
|
||||
return {"model": "gpt-4o", "configurable_fields": "any"}
|
||||
|
||||
@property
|
||||
def supports_image_inputs(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def has_tool_calling(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def has_structured_output(self) -> bool:
|
||||
return True
|
||||
@@ -1,4 +1,11 @@
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_core.runnables import RunnableConfig, RunnableSequence
|
||||
|
||||
from langchain.chat_models.base import __all__, init_chat_model
|
||||
|
||||
@@ -34,14 +41,156 @@ def test_all_imports() -> None:
|
||||
],
|
||||
)
|
||||
def test_init_chat_model(model_name: str, model_provider: str) -> None:
|
||||
init_chat_model(model_name, model_provider=model_provider, api_key="foo")
|
||||
_: BaseChatModel = init_chat_model(
|
||||
model_name, model_provider=model_provider, api_key="foo"
|
||||
)
|
||||
|
||||
|
||||
def test_init_missing_dep() -> None:
|
||||
with pytest.raises(ImportError):
|
||||
init_chat_model("gpt-4o", model_provider="openai")
|
||||
init_chat_model("mixtral-8x7b-32768", model_provider="groq")
|
||||
|
||||
|
||||
def test_init_unknown_provider() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
init_chat_model("foo", model_provider="bar")
|
||||
|
||||
|
||||
@pytest.mark.requires("langchain_openai")
|
||||
@mock.patch.dict(
|
||||
os.environ, {"OPENAI_API_KEY": "foo", "ANTHROPIC_API_KEY": "foo"}, clear=True
|
||||
)
|
||||
def test_configurable() -> None:
|
||||
model = init_chat_model()
|
||||
|
||||
for method in (
|
||||
"invoke",
|
||||
"ainvoke",
|
||||
"batch",
|
||||
"abatch",
|
||||
"stream",
|
||||
"astream",
|
||||
"batch_as_completed",
|
||||
"abatch_as_completed",
|
||||
):
|
||||
assert hasattr(model, method)
|
||||
|
||||
# Doesn't have access non-configurable, non-declarative methods until a config is
|
||||
# provided.
|
||||
for method in ("get_num_tokens", "get_num_tokens_from_messages"):
|
||||
with pytest.raises(AttributeError):
|
||||
getattr(model, method)
|
||||
|
||||
# Can call declarative methods even without a default model.
|
||||
model_with_tools = model.bind_tools(
|
||||
[{"name": "foo", "description": "foo", "parameters": {}}]
|
||||
)
|
||||
|
||||
# Check that original model wasn't mutated by declarative operation.
|
||||
assert model._queued_declarative_operations == []
|
||||
|
||||
# Can iteratively call declarative methods.
|
||||
model_with_config = model_with_tools.with_config(
|
||||
RunnableConfig(tags=["foo"]), configurable={"model": "gpt-4o"}
|
||||
)
|
||||
assert model_with_config.model_name == "gpt-4o" # type: ignore[attr-defined]
|
||||
|
||||
for method in ("get_num_tokens", "get_num_tokens_from_messages"):
|
||||
assert hasattr(model_with_config, method)
|
||||
|
||||
assert model_with_config.dict() == { # type: ignore[attr-defined]
|
||||
"name": None,
|
||||
"bound": {
|
||||
"model_name": "gpt-4o",
|
||||
"model": "gpt-4o",
|
||||
"stream": False,
|
||||
"n": 1,
|
||||
"temperature": 0.7,
|
||||
"presence_penalty": None,
|
||||
"frequency_penalty": None,
|
||||
"seed": None,
|
||||
"top_p": None,
|
||||
"logprobs": False,
|
||||
"top_logprobs": None,
|
||||
"logit_bias": None,
|
||||
"_type": "openai-chat",
|
||||
},
|
||||
"kwargs": {
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": "foo", "description": "foo", "parameters": {}},
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {"tags": ["foo"], "configurable": {}},
|
||||
"config_factories": [],
|
||||
"custom_input_type": None,
|
||||
"custom_output_type": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.requires("langchain_openai", "langchain_anthropic")
|
||||
@mock.patch.dict(
|
||||
os.environ, {"OPENAI_API_KEY": "foo", "ANTHROPIC_API_KEY": "foo"}, clear=True
|
||||
)
|
||||
def test_configurable_with_default() -> None:
|
||||
model = init_chat_model("gpt-4o", configurable_fields="any", config_prefix="bar")
|
||||
for method in (
|
||||
"invoke",
|
||||
"ainvoke",
|
||||
"batch",
|
||||
"abatch",
|
||||
"stream",
|
||||
"astream",
|
||||
"batch_as_completed",
|
||||
"abatch_as_completed",
|
||||
):
|
||||
assert hasattr(model, method)
|
||||
|
||||
# Does have access non-configurable, non-declarative methods since default params
|
||||
# are provided.
|
||||
for method in ("get_num_tokens", "get_num_tokens_from_messages", "dict"):
|
||||
assert hasattr(model, method)
|
||||
|
||||
assert model.model_name == "gpt-4o" # type: ignore[attr-defined]
|
||||
|
||||
model_with_tools = model.bind_tools(
|
||||
[{"name": "foo", "description": "foo", "parameters": {}}]
|
||||
)
|
||||
|
||||
model_with_config = model_with_tools.with_config(
|
||||
RunnableConfig(tags=["foo"]),
|
||||
configurable={"bar_model": "claude-3-sonnet-20240229"},
|
||||
)
|
||||
|
||||
assert model_with_config.model == "claude-3-sonnet-20240229" # type: ignore[attr-defined]
|
||||
# Anthropic defaults to using `transformers` for token counting.
|
||||
with pytest.raises(ImportError):
|
||||
model_with_config.get_num_tokens_from_messages([(HumanMessage("foo"))]) # type: ignore[attr-defined]
|
||||
|
||||
assert model_with_config.dict() == { # type: ignore[attr-defined]
|
||||
"name": None,
|
||||
"bound": {
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"max_tokens": 1024,
|
||||
"temperature": None,
|
||||
"top_k": None,
|
||||
"top_p": None,
|
||||
"model_kwargs": {},
|
||||
"streaming": False,
|
||||
"max_retries": 2,
|
||||
"default_request_timeout": None,
|
||||
"_type": "anthropic-chat",
|
||||
},
|
||||
"kwargs": {
|
||||
"tools": [{"name": "foo", "description": "foo", "input_schema": {}}]
|
||||
},
|
||||
"config": {"tags": ["foo"], "configurable": {}},
|
||||
"config_factories": [],
|
||||
"custom_input_type": None,
|
||||
"custom_output_type": None,
|
||||
}
|
||||
prompt = ChatPromptTemplate.from_messages([("system", "foo")])
|
||||
chain = prompt | model_with_config
|
||||
assert isinstance(chain, RunnableSequence)
|
||||
|
||||
@@ -79,6 +79,7 @@ def test_test_group_dependencies(poetry_conf: Mapping[str, Any]) -> None:
|
||||
"duckdb-engine",
|
||||
"freezegun",
|
||||
"langchain-core",
|
||||
"langchain-standard-tests",
|
||||
"langchain-text-splitters",
|
||||
"langchain-openai",
|
||||
"lark",
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestFireworksStandard(ChatModelIntegrationTests):
|
||||
@property
|
||||
def chat_model_params(self) -> dict:
|
||||
return {
|
||||
"model": "accounts/fireworks/models/firefunction-v1",
|
||||
"model": "accounts/fireworks/models/firefunction-v2",
|
||||
"temperature": 0,
|
||||
}
|
||||
|
||||
|
||||
@@ -193,22 +193,16 @@ class ChatModelIntegrationTests(ChatModelTests):
|
||||
pytest.skip("Test requires tool calling.")
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages(
|
||||
[
|
||||
("system", "Repeat what the user says in the style of {answer_style}."),
|
||||
("human", "{user_input}"),
|
||||
]
|
||||
[("human", "Hello. Please respond in the style of {answer_style}.")]
|
||||
)
|
||||
llm = GenericFakeChatModel(messages=iter(["hello matey"]))
|
||||
chain = prompt | llm | StrOutputParser()
|
||||
tool_ = chain.as_tool(
|
||||
name="repeat_in_answer_style",
|
||||
description="Repeat the user_input in a particular style of speaking.",
|
||||
name="greeting_generator",
|
||||
description="Generate a greeting in a particular style of speaking.",
|
||||
)
|
||||
model_with_tools = model.bind_tools([tool_])
|
||||
query = (
|
||||
"Using the repeat_in_answer_style tool, ask a Pirate how they would say "
|
||||
"hello."
|
||||
)
|
||||
query = "Using the tool, generate a Pirate greeting."
|
||||
result = model_with_tools.invoke(query)
|
||||
assert isinstance(result, AIMessage)
|
||||
assert result.tool_calls
|
||||
|
||||
@@ -345,7 +345,7 @@ class RecursiveCharacterTextSplitter(TextSplitter):
|
||||
]
|
||||
elif language == Language.ELIXIR:
|
||||
return [
|
||||
# Split along method function and module definiton
|
||||
# Split along method function and module definition
|
||||
"\ndef ",
|
||||
"\ndefp ",
|
||||
"\ndefmodule ",
|
||||
|
||||
Reference in New Issue
Block a user