Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions examples/langchain/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import asyncio

from langchain import hub
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain_openai import ChatOpenAI

from npiai.tools.web import Chromium
from npiai.integration import langchain


async def main():
async with Chromium(headless=False) as chromium:
toolkit = langchain.create_tool(chromium)
tools = toolkit.get_tools()
print(tools)

instructions = chromium.system_prompt
base_prompt = hub.pull("langchain-ai/openai-functions-template")
prompt = base_prompt.partial(instructions=instructions)

llm = ChatOpenAI(temperature=0)

agent = create_openai_functions_agent(llm, toolkit.get_tools(), prompt)

agent_executor = AgentExecutor(
agent=agent,
tools=toolkit.get_tools(),
verbose=True,
)

await agent_executor.ainvoke(
{
"input": "Get top 10 posts at Hacker News."
}
)


if __name__ == '__main__':
asyncio.run(main())
24 changes: 11 additions & 13 deletions npiai/core/tool/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import List, overload

from fastapi import FastAPI, Request
from pydantic import create_model, Field

import uvicorn
from npiai.llm import LLM, OpenAI
Expand All @@ -15,6 +16,7 @@
from npiai.core.hitl import HITL
from npiai.core.tool.function import FunctionTool
from npiai.core.tool.browser import BrowserTool
from npiai.utils import sanitize_schema


class AgentTool(BaseAgentTool):
Expand All @@ -30,24 +32,20 @@ def __init__(self, tool: FunctionTool, llm: LLM = None):
def unpack_functions(self) -> List[FunctionRegistration]:
# Wrap the chat function of this agent to FunctionRegistration

def chat(message: str, ctx: Context):
return self.chat(message, ctx)
model = create_model(
f'{self.name}__agent_model',
message=(str, Field(
description=f'The task you want {self._tool.name} to do or the message you want to chat with {self._tool.name}'
))
)

fn_reg = FunctionRegistration(
fn=chat,
fn=self.chat,
name='chat',
ctx_param_name='ctx',
description=self._tool.description,
schema={
'type': 'object',
'properties': {
'message': {
'type': 'string',
'description': f'The task you want {self._tool.name} to do or the message you want to chat with {self._tool.name}'
},
},
'required': ['message'],
}
model=model,
schema=sanitize_schema(model),
)

return [fn_reg]
Expand Down
25 changes: 13 additions & 12 deletions npiai/core/tool/function.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
"""The basic interface for NPi Apps"""
import asyncio
import dataclasses
import functools
import inspect
import json
import os
import re
import signal
import sys
from dataclasses import asdict
from typing import Dict, List, Optional, Any
from typing import Dict, List, Optional, Any, Type
import logging

import yaml
Expand Down Expand Up @@ -37,6 +35,7 @@ def function(
name: Optional[str] = None,
description: Optional[str] = None,
schema: Dict[str, Any] = None,
model: Optional[Type[BaseTool]] = None,
few_shots: Optional[List[Shot]] = None,
):
"""
Expand All @@ -47,6 +46,7 @@ def function(
name: Tool name. The tool function name will be used if not given.
description: Tool description. This value will be inferred from the tool's docstring if not given.
schema: Tool parameters schema. This value will be inferred from the tool's type hints if not given.
model: Pydantic model for tool schema. This value will be built from the schema if not given.
few_shots: Predefined working examples.

Returns:
Expand All @@ -61,15 +61,12 @@ def decorator(fn: ToolFunction):
name=name,
description=description,
schema=schema,
model=model,
few_shots=few_shots,
)
)

@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)

return wrapper
return fn

# called as `@function`
if callable(tool_fn):
Expand Down Expand Up @@ -128,7 +125,7 @@ def unpack_functions(self) -> List[FunctionRegistration]:

def use_hitl(self, hitl: HITL):
"""
Attach the given HITL handler to this tools and all its sub apps
Attach the given HITL handler to this tool and all its sub apps

Args:
hitl: HITL handler
Expand Down Expand Up @@ -309,10 +306,11 @@ def _register_tools(self):
)

# parse schema
tool_model = tool_meta.model
tool_schema = tool_meta.schema
ctx_param_name = None

if not tool_schema and len(params) > 0:
if not tool_model and len(params) > 0:
# get parameter descriptions
param_descriptions = {}
for p in docstr.params:
Expand All @@ -329,8 +327,10 @@ def _register_tools(self):
description=param_descriptions.get(p.name, ''),
))

model = create_model(f'{tool_name}_model', **param_fields)
tool_schema = sanitize_schema(model)
tool_model = create_model(f'{tool_name}_model', **param_fields)

if not tool_schema and tool_model:
tool_schema = sanitize_schema(tool_model)

# parse examples
tool_few_shots = tool_meta.few_shots
Expand Down Expand Up @@ -361,6 +361,7 @@ def _register_tools(self):
ctx_param_name=ctx_param_name,
description=tool_desc.strip(),
schema=tool_schema,
model=tool_model,
few_shots=tool_few_shots,
)
)
Expand Down
4 changes: 4 additions & 0 deletions npiai/integration/langchain/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .create_tool import create_tool
from .toolkit import NPiLangChainToolkit

__all__ = ['NPiLangChainToolkit', 'create_tool']
6 changes: 6 additions & 0 deletions npiai/integration/langchain/create_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from npiai.core import BaseTool
from .toolkit import NPiLangChainToolkit


def create_tool(tool: BaseTool) -> NPiLangChainToolkit:
return NPiLangChainToolkit(tool)
62 changes: 62 additions & 0 deletions npiai/integration/langchain/toolkit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import functools
from typing import List, Callable

from langchain_core.tools import BaseToolkit, StructuredTool
from langchain_core.pydantic_v1 import create_model, PrivateAttr, Field

from npiai.core import BaseTool as NPiBaseTool
from npiai.context import Context


def unwrap_context(func: Callable, ctx_param_name: str | None) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
if ctx_param_name:
# create an empty context for tools
kwargs[ctx_param_name] = Context()
return await func(*args, **kwargs)

return wrapper


class NPiLangChainToolkit(BaseToolkit):
_tools: List[StructuredTool] = PrivateAttr()

def __init__(self, npi_tool: NPiBaseTool):
BaseToolkit.__init__(self)
self._tools = []

for fn_reg in npi_tool.unpack_functions():
# remove ctx param
fn = unwrap_context(fn_reg.fn, fn_reg.ctx_param_name)
# recreate pydantic-v1 compatible model
params = {}

if fn_reg.model:
for name, field in fn_reg.model.model_fields.items():
if field.default and not field.is_required():
field_v1 = Field(
default=field.default,
description=field.description,
)
else:
field_v1 = Field(description=field.description)
params[name] = (field.annotation, field_v1)

schema_model_v1 = create_model(
f'{fn_reg.name}__model',
**params,
)

self._tools.append(
StructuredTool.from_function(
coroutine=fn,
name=fn_reg.name,
description=fn_reg.description,
args_schema=schema_model_v1,
infer_schema=False,
)
)

def get_tools(self) -> List[StructuredTool]:
return self._tools
4 changes: 3 additions & 1 deletion npiai/types/function_registration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Callable, Optional, Awaitable, List, Dict, Any
from typing import Callable, Optional, Awaitable, List, Dict, Any, Type
from dataclasses import dataclass, asdict

from openai.types.chat import ChatCompletionToolParam
from pydantic import BaseModel

from npiai.types.shot import Shot

Expand All @@ -15,6 +16,7 @@ class FunctionRegistration:
name: str
ctx_param_name: str
schema: Optional[Dict[str, Any]] = None
model: Optional[Type[BaseModel]] = None
few_shots: Optional[List[Shot]] = None

def get_meta(self):
Expand Down
6 changes: 4 additions & 2 deletions npiai/types/tool_meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from typing import Optional, List, Dict, Any
from typing import Optional, List, Dict, Any, Type
from pydantic import BaseModel

from npiai.types.shot import Shot

Expand All @@ -8,5 +9,6 @@
class ToolMeta:
name: str
description: str
schema: Dict[str, Any] = None,
schema: Dict[str, Any] = None
model: Optional[Type[BaseModel]] = None
few_shots: Optional[List[Shot]] = None
Loading