Skip to content

Commit 88a5ab4

Browse files
committed
example: update from main repo
1 parent 1f3c7cf commit 88a5ab4

File tree

5 files changed

+480
-12
lines changed

5 files changed

+480
-12
lines changed

examples/basic/chat.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def main(
4141
model: str = typer.Option("", "--model", "-m", help="model name"),
4242
no_stream: bool = typer.Option(False, "--nostream", "-ns", help="no streaming"),
4343
nocache: bool = typer.Option(False, "--nocache", "-nc", help="don't use cache"),
44-
query: str = typer.Option("", "--query", "-q", help="initial user query or msg"),
4544
sys_msg: str = typer.Option(
4645
"You are a helpful assistant. Be concise in your answers.",
4746
"--sysmsg",
@@ -83,14 +82,7 @@ def main(
8382
)
8483
agent = ChatAgent(config)
8584
task = Task(agent)
86-
# OpenAI models are ok with just a system msg,
87-
# but in some scenarios, other (e.g. llama) models
88-
# seem to do better when kicked off with a sys msg and a user msg.
89-
# In those cases we may want to do task.run("hello") instead.
90-
if query:
91-
task.run(query)
92-
else:
93-
task.run()
85+
task.run("hello")
9486

9587

9688
if __name__ == "__main__":
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""
2+
Function-calling example using a local LLM, with ollama.
3+
4+
"Function-calling" refers to the ability of the LLM to generate
5+
a structured response, typically a JSON object, instead of a plain text response,
6+
which is then interpreted by your code to perform some action.
7+
This is also referred to in various scenarios as "Tools", "Actions" or "Plugins".
8+
See more here: https://langroid.github.io/langroid/quick-start/chat-agent-tool/
9+
10+
Run like this (to run with llama-3.1-8b-instant via groq):
11+
12+
python3 examples/basic/text-to-structured.py -m groq/llama-3.1-8b-instant
13+
14+
Other models to try it with:
15+
- ollama/qwen2.5-coder
16+
- ollama/qwen2.5
17+
18+
19+
See here for how to set up a Local LLM to work with Langroid:
20+
https://langroid.github.io/langroid/tutorials/local-llm-setup/
21+
22+
23+
"""
24+
25+
import os
26+
from typing import List, Literal
27+
import fire
28+
import json
29+
from rich.prompt import Prompt
30+
31+
from langroid.pydantic_v1 import BaseModel, Field
32+
import langroid as lr
33+
from langroid.utils.configuration import settings
34+
from langroid.agent.tool_message import ToolMessage
35+
from langroid.agent.tools.orchestration import ResultTool
36+
import langroid.language_models as lm
37+
38+
# for best results:
39+
DEFAULT_LLM = lm.OpenAIChatModel.GPT4o
40+
41+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
42+
43+
# (1) Define the desired structure via Pydantic.
44+
# The "Field" annotations are optional, and are included in the system message
45+
# if provided, and help with generation accuracy.
46+
47+
48+
class Wifi(BaseModel):
49+
name: str
50+
51+
52+
class HomeSettings(BaseModel):
53+
App: List[str] = Field(..., description="List of apps found in text")
54+
wifi: List[Wifi] = Field(..., description="List of wifi networks found in text")
55+
brightness: Literal["low", "medium", "high"] = Field(
56+
..., description="Brightness level found in text"
57+
)
58+
59+
60+
# (2) Define the Tool class for the LLM to use, to produce the above structure.
61+
class HomeAutomationTool(lr.agent.ToolMessage):
62+
"""Tool to extract Home Automation structure from text"""
63+
64+
request: str = "home_automation_tool"
65+
purpose: str = """
66+
To extract <home_settings> structure from a given text.
67+
"""
68+
home_settings: HomeSettings = Field(
69+
..., description="Home Automation settings from given text"
70+
)
71+
72+
def handle(self) -> str:
73+
"""Handle LLM's structured output if it matches HomeAutomationTool structure"""
74+
print(
75+
f"""
76+
SUCCESS! Got Valid Home Automation Settings:
77+
{json.dumps(self.home_settings.dict(), indent=2)}
78+
"""
79+
)
80+
return ResultTool(settings=self.home_settings)
81+
82+
@classmethod
83+
def examples(cls) -> List["ToolMessage"]:
84+
# Used to provide few-shot examples in the system prompt
85+
return [
86+
(
87+
"""
88+
I have extracted apps Spotify and Netflix,
89+
wifi HomeWifi, and brightness medium
90+
""",
91+
cls(
92+
home_settings=HomeSettings(
93+
App=["Spotify", "Netflix"],
94+
wifi=[Wifi(name="HomeWifi")],
95+
brightness="medium",
96+
)
97+
),
98+
)
99+
]
100+
101+
102+
def app(
103+
m: str = DEFAULT_LLM, # model
104+
d: bool = False, # pass -d to enable debug mode (see prompts etc)
105+
nc: bool = False, # pass -nc to disable cache-retrieval (i.e. get fresh answers)
106+
):
107+
settings.debug = d
108+
settings.cache = not nc
109+
# create LLM config
110+
llm_cfg = lm.OpenAIGPTConfig(
111+
chat_model=m or DEFAULT_LLM,
112+
chat_context_length=4096, # set this based on model
113+
max_output_tokens=100,
114+
temperature=0.2,
115+
stream=True,
116+
timeout=45,
117+
)
118+
119+
tool_name = HomeAutomationTool.default_value("request")
120+
config = lr.ChatAgentConfig(
121+
llm=llm_cfg,
122+
system_message=f"""
123+
You are an expert in extracting home automation settings from text.
124+
When user gives a piece of text, use the TOOL `{tool_name}`
125+
to present the extracted structured information.
126+
""",
127+
)
128+
129+
agent = lr.ChatAgent(config)
130+
131+
# (4) Enable the Tool for this agent --> this auto-inserts JSON instructions
132+
# and few-shot examples (specified in the tool defn above) into the system message
133+
agent.enable_message(HomeAutomationTool)
134+
135+
# (5) Create task and run it to start an interactive loop
136+
# Specialize the task to return a ResultTool object
137+
task = lr.Task(agent, interactive=False)[ResultTool]
138+
139+
# set up a loop to extract Home Automation settings from text
140+
while True:
141+
text = Prompt.ask("[blue]Enter text (or q/x to exit)")
142+
if not text or text.lower() in ["x", "q"]:
143+
break
144+
result = task.run(text)
145+
assert isinstance(result, ResultTool)
146+
assert isinstance(result.settings, HomeSettings)
147+
148+
149+
if __name__ == "__main__":
150+
fire.Fire(app)

examples/docqa/doc-aware-chat.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""
2+
Single Agent for Doc-aware chat with user.
3+
4+
- user asks question
5+
- LLM decides whether to:
6+
- ask user for follow-up/clarifying information, or
7+
- retrieve relevant passages from documents, or
8+
- provide a final answer, if it has enough information from user and documents.
9+
10+
To reduce response latency, in the DocChatAgentConfig,
11+
you can set the `relevance_extractor_config=None`,
12+
to turn off the relevance_extraction step, which uses the LLM
13+
to extract verbatim relevant portions of retrieved chunks.
14+
15+
Run like this:
16+
17+
python3 examples/docqa/doc-aware-chat.py
18+
"""
19+
20+
from typing import Optional, Any
21+
22+
from rich import print
23+
from rich.prompt import Prompt
24+
import os
25+
26+
from langroid import ChatDocument
27+
from langroid.agent.special.doc_chat_agent import (
28+
DocChatAgent,
29+
DocChatAgentConfig,
30+
)
31+
import langroid.language_models as lm
32+
from langroid.mytypes import Entity
33+
from langroid.parsing.parser import ParsingConfig, PdfParsingConfig, Splitter
34+
from langroid.agent.chat_agent import ChatAgent
35+
from langroid.agent.task import Task
36+
from langroid.agent.tools.orchestration import ForwardTool
37+
from langroid.agent.tools.retrieval_tool import RetrievalTool
38+
from langroid.utils.configuration import set_global, Settings
39+
from fire import Fire
40+
41+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
42+
43+
44+
class DocAwareChatAgent(DocChatAgent):
45+
def __init__(self, config: DocChatAgentConfig):
46+
super().__init__(config)
47+
self.enable_message(RetrievalTool)
48+
49+
def retrieval_tool(self, msg: RetrievalTool) -> str:
50+
results = super().retrieval_tool(msg)
51+
return f"""
52+
53+
RELEVANT PASSAGES:
54+
=====
55+
{results}
56+
====
57+
58+
59+
BASED on these RELEVANT PASSAGES, DECIDE:
60+
- If this is sufficient to provide the user a final answer specific to
61+
their situation, do so.
62+
- Otherwise,
63+
- ASK the user for more information to get a better understanding
64+
of their situation or context, OR
65+
- use this tool again to get more relevant passages.
66+
"""
67+
68+
def llm_response(
69+
self,
70+
query: None | str | ChatDocument = None,
71+
) -> Optional[ChatDocument]:
72+
# override DocChatAgent's default llm_response
73+
return ChatAgent.llm_response(self, query)
74+
75+
def handle_message_fallback(self, msg: str | ChatDocument) -> Any:
76+
# we are here if there is no tool in the msg
77+
if isinstance(msg, ChatDocument) and msg.metadata.sender == Entity.LLM:
78+
# Any non-tool message must be meant for user, so forward it to user
79+
return ForwardTool(agent="User")
80+
81+
82+
def main(
83+
debug: bool = False,
84+
nocache: bool = False,
85+
model: str = lm.OpenAIChatModel.GPT4o,
86+
) -> None:
87+
llm_config = lm.OpenAIGPTConfig(chat_model=model)
88+
config = DocChatAgentConfig(
89+
llm=llm_config,
90+
n_query_rephrases=0,
91+
hypothetical_answer=False,
92+
relevance_extractor_config=None,
93+
# this turns off standalone-query reformulation; set to False to enable it.
94+
assistant_mode=True,
95+
n_neighbor_chunks=2,
96+
parsing=ParsingConfig( # modify as needed
97+
splitter=Splitter.TOKENS,
98+
chunk_size=100, # aim for this many tokens per chunk
99+
n_neighbor_ids=5,
100+
overlap=20, # overlap between chunks
101+
max_chunks=10_000,
102+
# aim to have at least this many chars per chunk when
103+
# truncating due to punctuation
104+
min_chunk_chars=200,
105+
discard_chunk_chars=5, # discard chunks with fewer than this many chars
106+
n_similar_docs=5,
107+
# NOTE: PDF parsing is extremely challenging, each library has its own
108+
# strengths and weaknesses. Try one that works for your use case.
109+
pdf=PdfParsingConfig(
110+
# alternatives: "unstructured", "pdfplumber", "fitz"
111+
library="fitz",
112+
),
113+
),
114+
)
115+
116+
set_global(
117+
Settings(
118+
debug=debug,
119+
cache=not nocache,
120+
)
121+
)
122+
123+
doc_agent = DocAwareChatAgent(config)
124+
print("[blue]Welcome to the document chatbot!")
125+
url = Prompt.ask("[blue]Enter the URL of a document")
126+
doc_agent.ingest_doc_paths([url])
127+
128+
# For a more flexible/elaborate user doc-ingest dialog, use this:
129+
# doc_agent.user_docs_ingest_dialog()
130+
131+
doc_task = Task(
132+
doc_agent,
133+
interactive=False,
134+
name="DocAgent",
135+
system_message=f"""
136+
You are a DOCUMENT-AWARE-GUIDE, but you do NOT have direct access to documents.
137+
Instead you can use the `retrieval_tool` to get passages from the documents
138+
that are relevant to a certain query or search phrase or topic.
139+
DO NOT ATTEMPT TO ANSWER THE USER'S QUESTION WITHOUT RETRIEVING RELEVANT
140+
PASSAGES FROM THE DOCUMENTS. DO NOT use your own existing knowledge!!
141+
Everything you tell the user MUST be based on the documents.
142+
143+
The user will ask you a question that you will NOT be able to answer
144+
immediately, because you are MISSING some information about:
145+
- the user or their context or situation, etc
146+
- the documents relevant to the question
147+
148+
At each turn you must decide among these possible ACTIONS:
149+
- use the `{RetrievalTool.name()}` to get more relevant passages from the
150+
documents, OR
151+
- ANSWER the user if you think you have enough information
152+
from the user AND the documents, to answer the question.
153+
154+
You can use the `{RetrievalTool.name()}` multiple times to get more
155+
relevant passages, if you think the previous ones were not sufficient.
156+
157+
REMEMBER - your goal is to be VERY HELPFUL to the user; this means
158+
you should NOT OVERWHELM them by throwing them a lot of information and
159+
ask them to figure things out. Instead, you must GUIDE them
160+
by asking SIMPLE QUESTIONS, ONE at at time, and finally provide them
161+
a clear, DIRECTLY RELEVANT answer that is specific to their situation.
162+
""",
163+
)
164+
165+
print("[cyan]Enter x or q to quit, or ? for evidence")
166+
167+
doc_task.run("Can you help me with some questions?")
168+
169+
170+
if __name__ == "__main__":
171+
Fire(main)

0 commit comments

Comments
 (0)