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.22"
__version__ = "2.1.23"
4 changes: 4 additions & 0 deletions core/jivas/agent/action/action.jac
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import from typing { Union }
import from logging { Logger }
import from jivas.agent.core.graph_node { GraphNode }
import from jivas.agent.memory.collection { Collection }
import from jivas.agent.memory.frame { Frame }
import from jivas.agent.memory.interaction { Interaction }

node Action(GraphNode) {
# represents an execution on the agent action graph
Expand All @@ -28,6 +30,8 @@ node Action(GraphNode) {
# override to execute operations upon registration of action
def on_register() { }

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

# override to execute operations upon the reload of action
def on_reload() { }

Expand Down
193 changes: 193 additions & 0 deletions core/jivas/agent/action/subgraph_action/completed_state.jac
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import logging;
import traceback;
import from typing { Union }
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 }



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_max_tokens:int = 4096;
has model_temperature: float = 0.3;
has history: bool = True;
has history_size: int = 3;
has max_statement_length: int = 2048;
has label: str = "";
has state_info:dict = {};
has enabled: bool = True;

has directive: str = """
Perform the following steps to confirm user submission:
a. Summarize submission:
- 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 any items on the list with a value of N/A
b. Request Explicit Confirmation:
Present the key and summary followed by:
- a request to know if the presented details are accurate
- a message to inform the user that they can request changes or cancel altogether.
""";

has prompt:str = """
Analyze **ONLY the latest user message** the conversation history above. Detect ONLY explicit signals for confirmation (yes/affirmative)
Follow these rules:

# Confirmation Detection
Set "confirm_response" to true for: "yes", "sure", "confirmed", "yeah", "yep", "absolutely", "okay" + clear context.
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"
- Suggestions to edit or change: phrases like "actually, please change...", "no, can you change...", "can you update...", "please edit...", "I'd like to change...", "can you correct...", "let's fix...", "could you modify...", or similar expressions indicating a desire to alter or correct previous information.


Return ONLY a JSON structure with a single detected key (confirm_response) set to true,
or "confirm_response" set to false otherwise. No commentary.
If nothing is detected, return an empty JSON object. No delimiters!
""";

# override to execute operations upon enabling of action
def on_enable() { }

# override to execute operations upon disabling of action
def on_disable() { }

def touch(frame:Frame) -> bool {
return True;
}

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

states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results");
revised = states_data.get("revised", False);

if self.auto_confirm {
branch_choice_response ={"confirm_response": True};
self.update_responses(branch_choice_response, frame);
}
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);
self.update_responses(branch_choice_response, frame);
}
elif revised{
branch_choice_response = {};
}

confirmed = branch_choice_response.get('confirm_response', None);


# If there is no confirmation or abortion
if not branch_choice_response {
# Retrieve the interview session from the frame node
responses = frame_node.data_get(key=f"{frame.action_label}_results");

if responses and isinstance(responses, dict) and len(responses) > 0 {
summary_lines = [];
for (field, value) in responses.items() {
summary_lines.append(f"- **{field}**: {value}");
}
summary = "\n".join(summary_lines);
} else {
summary = "";
}

directive = self.directive.replace("{summary}", summary);

# Change revised in frame to false
revised_state = {"revised":False};
self.update_responses(revised_state, frame);
return directive;
}
elif confirmed{
return True;
}
elif not confirmed{
return False;
}
}

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

if not prompt {
return None;
}

use_history = self.history;
if history is not None {
use_history = history;
}
visitor_utterance = frame_node.data_get(key="visitor_utterance");

if not visitor_utterance {
return None;
}

prompt_messages = [
{"human":visitor_utterance},
{"system":prompt}
];

# prepare the final prompt with history.
if (use_history) {
statements = frame_node.get_transcript_statements(interactions = self.history_size, max_statement_length = self.max_statement_length, with_events = True);

if (statements) {
# prepend statements to the prompt messages
prompt_messages = statements + prompt_messages;
}

}

model_action = agent_node.get_action(action_label=self.model_action);

if model_action {
model_action_result = model_action.call_model(
prompt_messages=prompt_messages,
prompt_variables={},
model_name=self.model_name,
model_temperature=self.model_temperature,
model_max_tokens=self.model_max_tokens
);

if model_action_result {
if json_only {
return model_action_result.get_json_result();
} else {
return model_action_result.get_result();
}
}
else {
return None;
}
}
}

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 {
stored_responses = {};
}

# Merge new responses into stored_responses
for (key, value) in responses.items() {
stored_responses[key] = value;
}

frame.frame_node.data_set(key=f"{frame.action_label}_results", value=stored_responses);
}
}
38 changes: 38 additions & 0 deletions core/jivas/agent/action/subgraph_action/confirmed_state.jac
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import logging;
import traceback;
import from typing { Union }
import from logging { Logger }
import from jivas.agent.action.subgraph_action.state { State }
import from jivas.agent.memory.frame { Frame }


node ConfirmedState(State) {
# Represents an execution on a subgraph on the agent action graph

has collection_id: str = "";
has enabled: bool = True;
has state_info:dict = {};
has directive: str = "Tell the user you have completed the process";

# override to execute operations upon enabling of action
def on_enable() { }

# override to execute operations upon disabling of action
def on_disable() { }

def touch(frame:Frame) -> bool {
# Always allow entering the COMPLETED state
return True;
}

def run(frame:Frame) {
frame_node = frame.frame_node;
agent_node = frame.agent_node;

# set any custom directive from daf
if ( directive := self.state_info.get("directive", "") ){
self.directive = directive;
}
return self.directive;
}
}
62 changes: 62 additions & 0 deletions core/jivas/agent/action/subgraph_action/initial_state.jac
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import logging;
import traceback;
import from typing { Union }
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 }


node InitialState(State) {
# Represents an execution on a subgraph on the agent action graph

has collection_id: str = "";
has label: str = "";
has enabled: bool = True;
has state_info:dict = {};
has abort:bool = False;

has directive:str = "Tell the user that you will cancel the process";
has prompt: str = """
Analyze **ONLY the latest user message** the conversation history above. Detect ONLY explicit signals for
conversation termination (abort/stop), decline-to-answer (no answer/can't respond).
Follow these rules:
# Abort Detection
Set "abort_response" to true for: "stop", "cancel", "exit", "end chat", "nevermind", "abort", "terminate".
Do NOT include "abort_response" if not explicitly stated.
# Decline Detection
Set "decline_response" to true for: "no answer", "I don't know", "I have none", "no comment", "can't say", "nothing", "n/a", "decline to answer".
Do NOT include "decline_response" for partial answers, topic changes, or ambiguous non-responses.

Return ONLY a JSON structure with a single detected key (abort_response, decline_response) set to true.
If nothing is detected, return an empty JSON object. No delimiters!
No commentary. Never guess - ambiguous cases = empty JSON.

""";

def touch(frame:Frame) -> bool {
# Always allow entering the Initial state
return True;
}

def run(frame:Frame) {
frame_node = frame.frame_node;
agent_node = frame.agent_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);
if response.get("abort_response"){
frame_node.data_set(key=f"{frame.action_label}_results", value={});
return self.directive;
}
else{
return True;
}
}
}
}
51 changes: 51 additions & 0 deletions core/jivas/agent/action/subgraph_action/list_states.jac
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import logging;
import from logging { Logger }
import from jivas.agent.core.agent { Agent }
import from jivas.agent.action.action { Action }
import from jivas.agent.action.actions { Actions }
import from jivas.agent.action.subgraph_action.subgraph_action { SubgraphAction }
import from jivas.agent.action.agent_graph_walker { agent_graph_walker }


walker list_states(agent_graph_walker) {
# action endpoint for listing all documents processed by the deepdoc service

has page:int = 1;
has per_page:int = 10;
has all:bool = True; # new flag to indicate whether to return all documents
has response:list[dict] = [];
has reporting:bool = True;
has agent_id:str = "";
has label:str = "";

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

class __specs__ {
static has private: bool = False;
static has excluded: list[str] = ["response"]; # exclude response from the specs
}

can on_agent with Agent entry {
visit [-->](`?Actions);
}

can on_actions with Actions entry {
visit [-->](`?SubgraphAction)(?enabled==True)(?label==self.label);
}


can on_action with Action entry {
# get the list of documents from the manifest

if self.all {
self.response = here.list_states(page=self.page, limit=100); # fetch all documents
} else {
self.response = here.list_states(page=self.page, limit=self.per_page); # fetch paged documents
}

if self.reporting {
report self.response;
}
}
}
Loading