-: Hey there, Eden here, and in this video,
we're going to implement the actor agent,
so we're going to build what's called
the first_responder_chain,
which is going to be the first step of our graph,
which is going to take as input the user query
and generate the initial article.
Now, we're going to cover some cool prompting techniques.
We're going to use output parsers,
specifically leveraging function calling
in order to get structured output.
As always, the code is available in the course's resources.
Start with the imports,
and let's import datetime because we're going to pass
to our agent the current date and time.
As always, we also want to import from dotenv
the load_dotenv() function,
which is going to take the environment variables
from our dotenv file.
And we want to import from LangChain output parsers
some output parsers that would handle the output
from the function calling from OpenAI,
so I'm going to import JsonOutputToolsParser
and the PydanticToolsParser,
and both output parsers are going to take back the response
we get the LLM with the function-calling invocation,
and it's going to take the function-calling invocation
and it's going to either transform it
into a JSON to a dictionary
or it's going to transform it into Pydantic object.
Later in this video,
we'll see what Pydantic object it will return.
And we'll also import HumanMessage from LangChain
because we're going to pass that into our LLM.
We want to import the ChatPromptTemplate,
and this is going to hold all of our history
of our agent iterations,
so we're going to append messages through that.
And also, we want a message placeholder,
so that is also useful when we'll plug in the history
and the message placeholder is useful
to being a placeholder for new messages,
and we saw already how it is used
in the reflection agent in the previous section.
And let's also import ChatOpenAI
because we're going to be using GPT-4 Turbo for that.
All right, let's start writing our main prompt
that will be used by our agent.
And I remind you, the input for this agent
is going to be the topic that we want to write about,
and the agent is going to write us the initial response.
Now, in the answer of the agent,
we need the content, the first draft of the article,
we want to also have a critique,
so some criticism on the newly created article,
and some search terms that will help enhance the article.
So, before we begin,
I have to give a quick hint on the implementation.
We're going to be using the MessageGraph(),
so this means the state of the graph nodes
that will be changing upon every node
is going to be a simple list of messages.
All right, let's go and review the main prompt
of our actor agent.
So, I'm going to define a variable,
I'll call it actor_prompt_template,
and it's going to be a ChatPromptTemplate from the messages,
and the messages are going to be here, the main prompt,
and all of our history that we used to get so far.
Let's review the system prompt.
So, you are an expert researcher.
The Current time is {time},
and this is we're going to plug in dynamically.
And the prompt's output indicator contains three parts.
The first part is a placeholder of {first_instructions},
and here, we're going to plug in
to simply write a 250-word essay.
The second is going to be,
"Reflect and critique your answer.
Be severe to maximize improvement."
And this critique is going to be used later
by the reviser agent.
And the third part here is,
"Recommend search queries to research information
and improve your answer."
So this is only the search query,
so this is not yet the search result,
and we're going to use the search queries
in the tool execution node
where we'll be leveraging Tavily search engine.
So, this was the system prompt,
and now it's time to introduce
a prompt engineering technique
to reuse this prompt template.
This prompt template is also going to be used
by our reviser node, the node which is going
to take now all of the information
and it's going to rewrite the article.
So, because we're going to be using that
in our reviser agent, which is going
to keep revising and critiquing, revising and critiquing,
then we also want to pass in the message placeholder
of all of the history before that.
So, all the information of what to search
and what was critiqued is going to be here.
If you didn't fully understand what are we going
to do in the reviser agent, don't worry, that is okay.
We're going to cover this in depth in the next video.
However, what's now is important for you to understand
is the first prompt we're going to send to the LLM,
that instead of our {first_instruction},
we're going to plug in an instruction
to write a 250-words essay.
And then the agent will generate us the first draft
with the critique and with the search queries
that we need to search later.
We're almost done with our prompt template,
and now we want to use the partial() method
in order to populate some already known placeholders.
When we invoke this prompt template,
then we want to plug in here the current date.
So, we're simply going to use a lambda function
that will output us today's date,
and we're going to give it in the ISO format.
And this will be computed only
when we invoke this prompt template,
and this will be when we invoke our agent.
Righty, we're done with the prompt
that our agent is going to use,
but now we want to ensure
that the output we get from the LLM then
it is in a structured format,
so we want the format to be with a response field
that is having the original essay,
we want the critique field,
which is having the critique for that essay,
and we want a search field,
which will be a list of values that we should search for.
And for that, we're going to leverage function calling
and specifically function calling that would make sure
that the output format of the LLM
is going to be in a object we create.
Let's create a new file, and we'll call it schemas.py.
And this file is going to hold the schemas
for the output we want.
So, we'll import the List type hinting
and pydantic BaseModel and Field
to create the objects that we want to structure our output.
Let's define a new class which will inherit
from pydantic BaseModel.
Going to be the Reflection class,
which is going to have all the information
about our reflection, about our critique.
Want to address in our critique two main things.
One is missing information.
Missing information was important to note,
but the LLM didn't generate it for us.
And the second is superfluous information.
Now, I had to look up in the dictionary what is superfluous,
and superfluous means unnecessary information,
information that doesn't add up any value.
That was the Reflection class.
Now, an important note is that this Reflection class,
when it's going to be used
alongside with the function-calling feature,
then it would actually ground the response from the LLM
to fill up those values,
so it would actually give us very concise feedback
from the LLM.
After we finish the Reflection class,
now it's time to create the answer class,
we'll call it AnswerQuestion,
and it's going to have a couple of fields.
The first field is going to be answer,
and this is going to be a 250-word answer
to the original question.
Second field is going to be a Reflection object
that we specified before.
And you can see in the description,
"Your reflection on the initial answer."
Now, this is another cool trick
where we're actually prompting the LLM
through the description of the classes fields,
so this is something very interesting in my opinion,
so this would help the LLM ground the response.
And the third field of the answer class
is going to be search_queries.
And in the descriptions,
you can see it's one to three search queries
for researching improvements
to address the current critique of your response.
So, this was the class definitions
of the answers we want to output from our LLM calls
that our agent is going to be outputting us.
So let's go back to the chains.py file,
and now we want to make sure that we get from the LLM
this kind of structured output.
Let's go back to the chains.py file,
and we want to initialize a large language model
and we're going to set it to be GPT-4 turbo.
We're going to create two output parsers.
The first one is going to be a JsonOutputToolsParser,
which is simply going to return us the function call
we got back from the LLM and transform it into a dictionary.
And the second output parser
is going to be a PydanticToolsOutputParser,
which is going to take the response from the LLM,
it's going to search for the function-calling invocation,
and it's going to parse it and transform it
into an AnswerQuestion object.
So, it's going to take the answer from the LLM
and it's going to create an AnswerQuestion object
that we can easily work with.
All right, this is a long video,
but we're heading towards the end.
So, now, it's time to prepare our prompt
before we send it to the LLM,
so let's go and take the actor_prompt_template
and let's populate the first_instruction field with,
"Provide a detailed 250 word answer."
And this will be plugged in into our prompt template,
and this is what is going
to make our LLM generate the first answer.
Let's now create the first_responder chain,
which is going to take our prompt template
and it's going to pipe it into the LLM GPT-4 Turbo,
but not before we bind the AnswerQuestion object
as a tool for the function calling.
And by providing tool_choice="AnswerQuestion",
this will force the LLM
to always use the AnswerQuestion tool,
thus grounding the response to the object
that we want to receive.
And this is a cool technique where the grounding
of the LLM also comes from the Pydantic object we created.
So, this way, we are going to make the LLM
give us exactly the response that we want.
All righty, so let's now write the chain
that will run this baby.
I'm simply going to add if __name__ = "__main__"
to be able to run this file.
And let's create a new chain,
and I'm going to prompt this chain with the input of,
"Write about AI-powered SOC / autonomous SOC problem domain,
and list startups that have raised capital on this."
This is the prompt,
this is the input we're going to give this chain.
And the chain is going to be very similar
to the chain we wrote above, but at the end here,
we're going to use the parser_pydantic,
which is going to take the response
and parse it as a Pydantic object of AnswerQuestion.
Let's now invoke this chain,
and we're going to send the human_message
in the messages key
so that would plug the messages placeholder
that we initialized in our prompt,
and let's debug this and see what happens.
And we got an error saying
that we didn't provide the search_queries field
to AnswerQuestion object,
so this means that when the LLM produced the response,
it didn't actually give us a search_queries in the answer.
And this can be resolved
if we do some prompt engineering like saying,
"You must provide the search queries at all costs."
Or something like that, or even better,
maybe to try and split up this query
into a different prompt that will run independently.
But we're not going to perform all of this
because this is just a proof of concept,
and we'll just rerun it again
and we'll see that it would work out.
And let's go and evaluate res.
And we can see that we have an AnswerQuestion object,
and we can have a look at the answer.
"AI-powered SOC, or autonomous SOC,
represent a cutting-edge approach
in the cybersecurity domain,
leveraging AI and machine learning technologies
to enhance threat detection, response,
and overall security operation.
This innovation paradigm addresses the escalating complexity
and volume of cyber threats,
which often outpace the capabilities of traditions,
human-operated SOC.
AI-powered SOCS aim to automate the identification
and neutralization of threats,
reduce false positives,
and free up human analysts to focus on more strategic tasks.
The problem domain encompasses various challenges,
including the integration of AI technologies
with existing security infra,
ensuring the accuracy and reliability
of AI-driven threat detection,
and maintaining the privacy and security
of sensitive data processed by AI systems."
So now, we get here a list of startups
that raised capital on this,
and this response looks legit,
but it has a couple of things that it can be better.
For example, it had some redundant information.
And two, the information about the startups
that raised capital,
it came from the parametric knowledge of the LLM,
basically all the knowledge
that the LLM learned during the training,
so we can actually here ground this part in external data.
Let's go and let's check out the Reflection object
inside of our AnswerQuestion object.
And we can see here we have missing,
and let's see what information we're missing.
"The answer could benefit more precise data
on the amount of capital raised by each mentioned startup,
and would provide a clearer picture of the scale."
So, this is pretty interesting and it seems legit.
And let's take out the superfluous field.
"The detailed explanation on the problem
might be slightly redundant
for readers already familiar with the concept."
The reflection looks pretty good in my opinion.
All right, so now let's go
and take a look at the search queries,
and we can see we have here four search queries.
One is AI-powered SOC startup funding.
The other is Darktrace funding history,
Vectra capital raised, and Arctic investment routes.
And if we'll go to LangSmith to see the tracing,
we'll take the final trace,
and if we'll take and examine the prompt
that we sent OpenAI,
we can see we have here the input
and then we can see the answer
which we later parsed with the Pydantic output parser.
Wow, that was a long video,
but we finally wrote the logic for the responder agent,
and this is the logic which will be running
in the responder node,
which will give us the initial response
alongside with the critique
and the search term for web searches.
We covered the first_responder_chain,
we saw how we can get structured output
using function calling in output parsers,
and we saw some cool prompt engineering techniques
that will help the LLM ground our response exactly
with the information that we want.
In the next video, we're going to review the logic
that will happen in the reviser chain.