Skip to content
This repository was archived by the owner on Jun 22, 2026. It is now read-only.
Closed
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
6 changes: 5 additions & 1 deletion core/jivas/agent/action/action.jac
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ node Action(GraphNode) {
# override to execute operations upon registration of action
def on_register() { }

def run(frame_node:Frame, interaction_node:Interaction){}
# overide to execute operations upon running of action
def run(frame_node:Frame, interaction_node:Interaction){ }

# overide to execute operations upon denying access of action
def deny(interaction_node:Interaction){ }

# override to execute operations upon the reload of action
def on_reload() { }
Expand Down
224 changes: 224 additions & 0 deletions core/jivas/agent/action/retrieval_action.jac
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import json;
import logging;
import traceback;
import from typing { Optional, Union }
import from logging { Logger }
import from jivas.agent.action.action { Action }
import from jivas.agent.action.model_action { ModelAction, ModelActionResult }

node RetrievalAction(Action) {
# Integrates with vector database for retrieval augmented generation tasks

# set up logger
static has logger:Logger = logging.getLogger(__name__);

# the directive template for RAG
has directive:str = """
Use CONTEXT as your knowledge base, intelligently assess the user question, review CONTEXT for context and finally produce an informative and accurate response.
Do not include any information outside of the CONTEXT. If relevant content is not available in CONTEXT, advise the user that you do not have the relevant information at this time.

CONTEXT:
{context}

""";

# the null directive template for RAG
has null_directive:str = "No context information was retrieved based on user utterance. If the user utterance is a question which relates to your knowledge, advise them that you do not have the relevant information at this time to answer their question.\n";

# context_rewriting_prompt
has query_completion_prompt:str = """
Based on the conversation history, perform the following tasks:

1. **Analyze Context and Intent**:
- Review the conversation history to establish context
- Determine if the user's message is a query requiring information
- Skip refinement for small talk, greetings, or acknowledgments

2. **Query Refinement** (if applicable):
- Enhance the query by incorporating key context from conversation history
- Make implicit references explicit using historical context
- Ensure the query is specific, clear, and self-contained
- Remove ambiguous pronouns or references
- Maintain original intent while improving clarity

3. **Output Format**:
Return a JSON object (no delimiters!) with the following keys:
- "query": "the refined query or original message",
- "is_query": true/false (true if message requires context search, false otherwise)

Note: Focus solely on query clarification and refinement.
The 'query' field should contain only the refined query or original message without commentary.
The 'is_query' field should be true for information-seeking questions and false for casual conversation.

""";

# the number of results
has k:int = 3;
# the score threshold (smaller numbers are usually more accurate)
has score_threshold:float = 0.3;
# max marginal relevance search
has mmr:bool = False;
# whether to return metadata with context or not
has metadata:bool = False;
# the vector store action name bound to this retrieval action
has vector_store_action:str = "";
has history_size:int = 3;
has max_statement_length:int = 400;
has model_action:str = "LangChainModelAction";
has model_name:str = "gpt-4o";
has model_temperature:float = 0.2;
has model_max_tokens:int = 10000;

def on_register() {

# load the agent's default vector store action if none is specified
if not self.vector_store_action {
self.vector_store_action = (self.get_agent()).vector_store_action;
}
}

def run(frame_node: Frame, interaction_node: Interaction) {

# first prepare the query with context completion
# prepare query using conversation history or fallback to original utterance
query = self.process_query(frame_node, interaction_node);

if(not query) {
# if no query is generated, return early
return;
}

if not query.get("is_query", False) {
# if the query is not a query, return early
return;
}

# update interaction node with query and context data
interaction_context = interaction_node.data_get(key=self.get_type());
if not interaction_context {
interaction_context = {};
}
interaction_context['query'] = query.get("query", interaction_node.utterance);

# handle context, if any and queue directive
if(context_data := self.retrieve_context(interaction_context['query'])) {

context_directive = None;
# add raw context to the interaction node
interaction_context['context'] = context_data;
# convert context data to JSON for composing the directive
context_json = json.dumps(context_data);
# prepare context directive
context_directive = self.directive.format(context=context_json);
# add the context directive to the interaction node
interaction_node.add_directive(directive = context_directive);
} else {
directives = interaction_node.get_directives();
if(not directives) {
interaction_node.add_directive(directive = self.null_directive);
}
}

interaction_node.data_set(key=self.get_type(), value=interaction_context);

}

def process_query(frame_node: Frame, interaction_node: Interaction) -> dict {

query = {};

# grab the history, if any
if (statements := frame_node.get_transcript_statements(interactions = self.history_size, max_statement_length = self.max_statement_length)) {

prompt_messages = [];
prompt_messages.extend(statements);
prompt_messages.extend([{"human": interaction_node.utterance}]);
prompt_messages.extend([{"system": self.query_completion_prompt}]);

result = None;

if(model_action := self.get_agent().get_action(action_label=self.model_action)) {

if( model_action_result := model_action.call_model(
prompt_messages = prompt_messages,
prompt_variables = {},
interaction_node = interaction_node,
model_name=self.model_name,
model_temperature=self.model_temperature,
model_max_tokens=self.model_max_tokens
)) {
# add the resulting intent, if any to the interaction to trigger the relevant action(s)
query = model_action_result.get_json_result();
}
}
}

return query;
}


def retrieve_context(query:str, filter:Optional[str] = "") -> list {
# override to implement custom retrieval operation

# """
# retrieves document for context

# :param interaction_node (Interaction) – interaction node containing utterance, etc.

# :returns context data relevant for RAG or [] if no context is found
# """
context_data = [];

if(vector_store_action := self.get_agent().get_action(action_label=self.vector_store_action)) {

if(self.mmr) {
if(documents := vector_store_action.max_marginal_relevance_search(query=query, k=self.k)) {
for doc in documents {
context_item = {
"content": doc.page_content
};
if(self.metadata) {
context_item["metadata"] = doc.metadata;
}
context_data.append(context_item);
}
if context_data {
return json.dumps(context_data);
}
}
} else {
# perform similarity search
if(documents_and_score := vector_store_action.similarity_search_with_score(query=query, k=self.k, filter=filter)) {
for (doc, score) in documents_and_score {
if(score <= self.score_threshold) {
context_item = {
"content": doc.page_content
};
if(self.metadata) {
context_item["metadata"] = doc.metadata;
}
context_data.append(context_item);
}
}
}
}
}

return context_data;
}

def healthcheck() -> Union[bool, dict] {

vector_store_action = self.get_agent().get_action(action_label=self.vector_store_action);
if(not vector_store_action) {
return {
"status": False,
"message": f"Unable to find a valid vector store action. Check your configuration and try again.",
"severity": "error"
};
}

return True;
}

}
15 changes: 13 additions & 2 deletions core/jivas/agent/action/subgraph_action/state.jac
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ node State(GraphNode) {
has history: bool = True;
has history_size: int = 3;
has max_statement_length: int = 2048;
has directive_template:str = """
Tailor your response to get the information needed based on the following description:
{description}

Take note of the following additional instructions while responding to the user but do not mention them unless it is needed:
{instructions}
E.g. {question}
""";

has extraction_prompt:str = """
Review the user's message and the conversation history to accurately extract the following entities.
Expand Down Expand Up @@ -97,13 +105,16 @@ node State(GraphNode) {
prompt = self.generate_extraction_prompt();

extraction_result = self.call_llm(prompt=prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node);

if required is True and not extraction_result{

question = self.state_info.get("question", "");
constraints = self.state_info.get("constraints", {});
description = constraints.get("description", "");
additional_instructions = constraints.get("additional_instructions", "");

directive = "Tailor your response to get the information needed based on the following description: \n" + description + "\n Eg." + question;
directive = self.directive_template.replace("{description}", description);
directive = directive.replace("{instructions}", additional_instructions);
directive = directive.replace("{question}", question);

if(options:= constraints.get("options", "")){
directive = directive + "\n They can choose from the list of options below\n" + str(options);
Expand Down