Skip to content
This repository was archived by the owner on Jun 22, 2026. It is now read-only.
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
2 changes: 1 addition & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
JIVAS is an Agentic Framework for rapidly prototyping and deploying graph-based, AI solutions.
"""

__version__ = "2.1.23"
__version__ = "2.1.24"
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
221 changes: 221 additions & 0 deletions core/jivas/agent/action/retrieval_action.jac
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
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 there is no other information available in this prompt or the converation history that can help answer the user's question, advise the user 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 {
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;
}

}
19 changes: 10 additions & 9 deletions core/jivas/agent/action/subgraph_action/completed_state.jac
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import from logging { Logger }
import from jivas.agent.core.graph_node { GraphNode }
import from jivas.agent.action.subgraph_action.state { State }
import from jivas.agent.memory.frame { Frame }

import from jivas.agent.memory.interaction { Interaction }


node CompletedState(GraphNode) {
# Represents an execution on a subgraph on the agent action graph
has collection_id: str = "";
has auto_confirm: bool = False;
has model_action: str = "LangChainModelAction";
has model_name: str = "gpt-4o";
has model_name: str = "gpt-4.1";
has model_max_tokens:int = 4096;
has model_temperature: float = 0.3;
has history: bool = True;
Expand All @@ -29,7 +29,7 @@ node CompletedState(GraphNode) {
- Extract all user-provided submission details from:
{summary}
- Format them as a clear, bulleted list under a statement that indicates that it is a list of the information gathered.
- Do not put internal settings such as "revised" or "confirm response" in the displayed list
- Do not put internal settings such as "revised", "completed", "confirm_response" in the displayed list
- Do not put any items on the list with a value of N/A
b. Request Explicit Confirmation:
Present the key and summary followed by:
Expand All @@ -42,7 +42,7 @@ node CompletedState(GraphNode) {
Follow these rules:

# Confirmation Detection
Set "confirm_response" to true for: "yes", "sure", "confirmed", "yeah", "yep", "absolutely", "okay" + clear context.
Set "confirm_response" to true for: "yes", "sure", "confirmed", "yeah", "yep", "absolutely", "okay" + clear context. These should be in response to a request for confirmation of previously provided information.
Set "confirm_response" to false ONLY for explicit negative or revision signals, including:
- Direct negatives: "no"
- Revision or correction requests: "I'd like to make an adjustment", "need to make a change", "revision", "change my answer", "not correct", "incorrect", "needs update"
Expand All @@ -64,9 +64,10 @@ node CompletedState(GraphNode) {
return True;
}

def run(frame:Frame) -> Union[str, bool] {
def run(frame:Frame, interaction_node:Interaction) -> Union[str, bool] {
frame_node = frame.frame_node;
agent_node = frame.agent_node;
self.update_responses({"completed": True}, frame);

states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results");
revised = states_data.get("revised", False);
Expand All @@ -77,7 +78,7 @@ node CompletedState(GraphNode) {
}
elif not revised{
# Check for confirmation in the latest user message
branch_choice_response = self.call_llm(self.prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node);
branch_choice_response = self.call_llm(self.prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node, interaction_node=interaction_node);
self.update_responses(branch_choice_response, frame);
}
elif revised{
Expand Down Expand Up @@ -117,7 +118,7 @@ node CompletedState(GraphNode) {
}
}

def call_llm(prompt:str, history:Union[bool,None] = None, json_only:bool = False, frame_node:Frame, agent_node:GraphNode) -> Union[str, dict, None] {
def call_llm(prompt:str, history:Union[bool,None] = None, json_only:bool = False, frame_node:Frame, agent_node:GraphNode, interaction_node:Interaction) -> Union[str, dict, None] {
# performs function tool calling for extracting question responses based on question
prompt_messages = [];

Expand Down Expand Up @@ -159,7 +160,8 @@ node CompletedState(GraphNode) {
prompt_variables={},
model_name=self.model_name,
model_temperature=self.model_temperature,
model_max_tokens=self.model_max_tokens
model_max_tokens=self.model_max_tokens,
interaction_node=interaction_node
);

if model_action_result {
Expand All @@ -176,7 +178,6 @@ node CompletedState(GraphNode) {
}

def update_responses(responses:dict, frame:Frame) {
print(f"New responses to merge: {responses}");
stored_responses = frame.frame_node.data_get(key=f"{frame.action_label}_results");

if type(stored_responses) is not dict or not stored_responses {
Expand Down
4 changes: 2 additions & 2 deletions core/jivas/agent/action/subgraph_action/confirmed_state.jac
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import from typing { Union }
import from logging { Logger }
import from jivas.agent.action.subgraph_action.state { State }
import from jivas.agent.memory.frame { Frame }

import from jivas.agent.memory.interaction { Interaction }

node ConfirmedState(State) {
# Represents an execution on a subgraph on the agent action graph
Expand All @@ -25,7 +25,7 @@ node ConfirmedState(State) {
return True;
}

def run(frame:Frame) {
def run(frame:Frame, interaction_node:Interaction) -> Union[str, bool] {
frame_node = frame.frame_node;
agent_node = frame.agent_node;

Expand Down
9 changes: 5 additions & 4 deletions core/jivas/agent/action/subgraph_action/initial_state.jac
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import from logging { Logger }
import from jivas.agent.action.subgraph_action.state { State }
import from jivas.agent.core.graph_node { GraphNode }
import from jivas.agent.memory.frame { Frame }

import from jivas.agent.memory.interaction { Interaction }

node InitialState(State) {
# Represents an execution on a subgraph on the agent action graph
Expand Down Expand Up @@ -39,19 +39,20 @@ node InitialState(State) {
return True;
}

def run(frame:Frame) {
def run(frame:Frame, interaction_node:Interaction) -> Union[str, bool] {
frame_node = frame.frame_node;
agent_node = frame.agent_node;

interaction_node = frame.interaction_node;
# check if abort was set to true
if self.abort{
frame_node.data_set(key=f"{frame.action_label}_results", value={});
self.abort = False;
return "";
}else{
response = self.call_llm(self.prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node);
response = self.call_llm(self.prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node, interaction_node=interaction_node);
if response.get("abort_response"){
frame_node.data_set(key=f"{frame.action_label}_results", value={});
interaction_node.add_event("AI has aborted the current process by request of the user.");
return self.directive;
}
else{
Expand Down
Loading