From 8ee1007098618bc059a66f59b4436fce6705d240 Mon Sep 17 00:00:00 2001 From: Isoke Date: Mon, 6 Oct 2025 10:32:23 -0400 Subject: [PATCH 01/14] Add run method to Action node and remove unused import from Frame node --- core/jivas/agent/action/action.jac | 3 +++ core/jivas/agent/memory/frame.jac | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/jivas/agent/action/action.jac b/core/jivas/agent/action/action.jac index 3b2e99b..ee0149b 100644 --- a/core/jivas/agent/action/action.jac +++ b/core/jivas/agent/action/action.jac @@ -4,6 +4,7 @@ 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 } node Action(GraphNode) { # represents an execution on the agent action graph @@ -28,6 +29,8 @@ node Action(GraphNode) { # override to execute operations upon registration of action def on_register() { } + def run(frame_node:Frame){} + # override to execute operations upon the reload of action def on_reload() { } diff --git a/core/jivas/agent/memory/frame.jac b/core/jivas/agent/memory/frame.jac index d16c0d8..93df73d 100644 --- a/core/jivas/agent/memory/frame.jac +++ b/core/jivas/agent/memory/frame.jac @@ -7,7 +7,6 @@ import from jivas.agent.modules.text.parsing { extract_first_name } import from jivas.agent.memory.interaction { Interaction } import from jivas.agent.memory.interaction_response { InteractionResponse, TextInteractionMessage } import from jivas.agent.core.graph_node { GraphNode } -import from jivas.agent.action.interact_action { InteractAction } import from jac_cloud.plugin.jaseci { JacPlugin as Jac } node Frame(GraphNode) { From 2865183c9856a55a500663b13894eaef13108984 Mon Sep 17 00:00:00 2001 From: Isoke Date: Tue, 21 Oct 2025 15:00:15 -0400 Subject: [PATCH 02/14] added states and subgraph action --- .../subgraph_action/completed_state.jac | 220 +++++++++ .../subgraph_action/confirmed_state.jac | 45 ++ .../action/subgraph_action/initial_state.jac | 54 +++ .../action/subgraph_action/list_states.jac | 50 ++ .../action/subgraph_action/revision_state.jac | 199 ++++++++ .../agent/action/subgraph_action/state.jac | 221 +++++++++ .../subgraph_action/subgraph_action.jac | 427 ++++++++++++++++++ .../action/subgraph_action/update_state.jac | 42 ++ core/jivas/agent/lib.jac | 5 + 9 files changed, 1263 insertions(+) create mode 100644 core/jivas/agent/action/subgraph_action/completed_state.jac create mode 100644 core/jivas/agent/action/subgraph_action/confirmed_state.jac create mode 100644 core/jivas/agent/action/subgraph_action/initial_state.jac create mode 100644 core/jivas/agent/action/subgraph_action/list_states.jac create mode 100644 core/jivas/agent/action/subgraph_action/revision_state.jac create mode 100644 core/jivas/agent/action/subgraph_action/state.jac create mode 100644 core/jivas/agent/action/subgraph_action/subgraph_action.jac create mode 100644 core/jivas/agent/action/subgraph_action/update_state.jac diff --git a/core/jivas/agent/action/subgraph_action/completed_state.jac b/core/jivas/agent/action/subgraph_action/completed_state.jac new file mode 100644 index 0000000..bc8b353 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/completed_state.jac @@ -0,0 +1,220 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.core.graph_node { GraphNode } +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 as well as 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; + + self.set_vars(); + + states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + print("got_states_data"); + revised = states_data.get("revised", False); + + if self.auto_confirm { + branch_choice_response ={"confirm_response": True}; + self.update_responses(branch_choice_response); + } + elif not revised{ + print("revised is False"); + + # 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"); + print(f"Interview session data: {responses}"); + + if responses and isinstance(responses, dict) and len(responses) > 0 { + print("has valid data"); + summary_lines = []; + for (field, value) in responses.items() { + summary_lines.append(f"- **{field}**: {value}"); + } + summary = "\n".join(summary_lines); + } else { + summary = ""; + } + print(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{ + print("we have a confirmation response"); + return True; + } + elif not confirmed{ + print("user has not confirmed\n"); + return False; + } + } + + def set_vars(){ + # setting any custom prompts or directives + if ( prompt := self.state_info.get("prompt", "") ){ + print("completed state var setting"); + self.prompt = prompt; + print("new prompt", prompt); + } + if ( directive := self.state_info.get("directive", "") ){ + print("completed state var settting"); + self.directive = directive; + print("new directive", directive); + } + } + + 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 + print("\nCalling LLM with prompt"); + 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"); + print(f"Visitor utterance: {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; + } + + } + print(f"Model action: {self.model_action}"); + + model_action = agent_node.get_action(action_label=self.model_action); + + if model_action { + # print("\n Has model action, calling model\n", 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 = {}; + } + + print(f"Stored responses before update: {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); + print(f"Stored responses after update: {frame.frame_node.data_get(key=f'{frame.action_label}_results')}"); + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/confirmed_state.jac b/core/jivas/agent/action/subgraph_action/confirmed_state.jac new file mode 100644 index 0000000..5feea15 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/confirmed_state.jac @@ -0,0 +1,45 @@ +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 { + print(f"Touching CONFIRMED state: {self.label}"); + # 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; + } + + # Retrieve the interview session from the frame node + interview_session = frame_node.data_get(key=f"{frame.action_label}_results"); + print(f"Interview session data: {interview_session}"); + + + return self.directive; + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/initial_state.jac b/core/jivas/agent/action/subgraph_action/initial_state.jac new file mode 100644 index 0000000..35618dc --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/initial_state.jac @@ -0,0 +1,54 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.action.state { State } +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 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; + + # Retrieve the interview session from the frame node + 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; + } + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/list_states.jac b/core/jivas/agent/action/subgraph_action/list_states.jac new file mode 100644 index 0000000..2790530 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/list_states.jac @@ -0,0 +1,50 @@ +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 actions.jivas.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 = ""; + + # 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); + } + + + 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; + } + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/revision_state.jac b/core/jivas/agent/action/subgraph_action/revision_state.jac new file mode 100644 index 0000000..783fd6c --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/revision_state.jac @@ -0,0 +1,199 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.core.graph_node { GraphNode } +import from jivas.agent.memory.frame { Frame } + + + +node RevisionState(GraphNode) { + # Represents an execution on a subgraph on the agent action graph + has collection_id: str = ""; + has label: str = ""; + has state_info:dict = {}; + has states:list = []; + 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 directive: str = "Encourage the user to suggest any changes to the information provided."; + has enabled: bool = True; + + has prompt:str = """ + Review the user's message and the recorded responses to accurately extract the following entities and update the relevant recorded responses. + Be strict on the constraints specified for each entity. Return a JSON object with keys exactly as listed below. + Include only keys for which you could extract a valid revised value adhering to all constraints. + Do not extract value if it is the same as the one in responses. + + Entities to extract: + {entities} + + Recorded responses: + {responses} + + Return ONLY the JSON object with the revised entities, no delimiters. Do not include any other text or explanation. + Return an empty JSON if no changes were made. + The JSON must have the following structure (only include keys with valid values): + {sample_json} + """; + + # 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 { + states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + state_value = states_data.get("confirm_response", ""); + if state_value{ + return False; + } + else{ + return True; + } + } + + def run(frame:Frame) -> Union[str, bool] { + frame_node = frame.frame_node; + agent_node = frame.agent_node; + + # Check for any changes in the latest user message + prompt = self.generate_revision_extraction_prompt(frame); + extraction_result = self.call_llm(prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node); + + # If there is no confirmation or abortion + if extraction_result{ + # store the result in the frame node + self.update_responses(extraction_result, frame); + + #clearing confirm response so that completed state can processs it + confirm_response = {"confirm_response":{}, "revised":True}; + self.update_responses(confirm_response, frame); + return True; + } + else{ + directive = self.directive; + return directive; + } + } + + def generate_revision_extraction_prompt(frame:Frame) -> str { + # prepares the revision extraction prompt + + entities_list = []; + sample_json_lines = []; + + for state_info in self.states { + constraints = state_info.get('constraints', {}); + + if not constraints { + continue; + } + + desc = constraints.get('description', ''); + other_constraints = {k: v for (k, v) in constraints.items() if k != 'description'}; + constraint_strs = [f"{k}: {v}" for (k, v) in other_constraints.items()]; + constraint_part = f" ({', '.join(constraint_strs)})" if constraint_strs else ""; + entities_list.append(f"- {state_info.get("name")}: {desc}{constraint_part}"); + sample_json_lines.append(f" '{state_info.get("name")}': ''"); + } + + responses = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + # Convert the responses dict to a markdown bulleted list + if responses and isinstance(responses, dict) and len(responses) > 0 { + summary_lines = []; + for (field, value) in responses.items() { + summary_lines.append(f"- **{field}**: {value}"); + } + responses = "\n".join(summary_lines); + } + + entities = "\n".join(entities_list); + sample_json = '{\n' + ',\n'.join(sample_json_lines) + '\n}'; + # prepate the prompt + prompt = self.prompt.format(entities=entities, responses=responses, sample_json=sample_json); + # escape the conflicting symbols + prompt = prompt.replace('{', '{{').replace('}','}}'); + + return prompt; + } + + 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; + } + } + + # return None; + } + + def update_responses(responses:dict, frame:Frame) { + 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); + } +} \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/state.jac b/core/jivas/agent/action/subgraph_action/state.jac new file mode 100644 index 0000000..44d93b5 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/state.jac @@ -0,0 +1,221 @@ +import logging; +import traceback; +import from typing { Union } +import from logging { Logger } +import from jivas.agent.core.graph_node { GraphNode } +import from jivas.agent.memory.frame { Frame } + +node State(GraphNode) { + # represents an execution on a subgraph on the agent action graph + + has collection_id:str = ""; + has label: str = ""; + has description: str = "specialized state"; + has enabled: bool = True; + has state_info: dict = {}; + + + #llm model parameters + 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 extraction_prompt:str = """ + Review the user's message and the conversation history to accurately extract the following entities. + Only extract data that has not been specifically cancelled. + Be strict on the constraints specified for each entity. Return a JSON object with keys exactly as listed below. + Include only keys for which you could extract a valid value adhering to all constraints. + + Entities to extract: + {entities} + + Return ONLY the JSON object with the extracted entities, no delimiters. Do not include any other text or explanation. + The JSON must have the following structure (only include keys with valid values): + {sample_json} + """; + + # set up logger + static has logger:Logger = logging.getLogger(__name__); + + + def postinit { + super.postinit(); + # list of node attributes which are protected from update operation + self.protected_attrs += ['_package', 'label', 'version', 'agent_id']; + # list of node attributes which should be excluded from export + self.transient_attrs += ['agent_id']; + } + + # 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 { + frame_node = frame.frame_node; + + state_data = frame_node.data_get(key=f"{frame.action_label}_results"); + if state_data{ + state_value = state_data.get(self.label, None); + } + else{ + state_value = {}; + } + + + if state_value { + return False; + } + else { + # authorize the walker to run this action + return True; + } + } + + def run(frame:Frame)->Union[str, bool]{ + + agent_node = frame.agent_node; + frame_node = frame.frame_node; + required = self.state_info.get("required",""); + + 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", ""); + + directive = "Tailor your response to get the information needed based on the following description: \n" + description + "\n Eg." + question; + + if(options:= constraints.get("options", "")){ + directive = directive + "\n They can choose from the list of options below\n" + str(options); + } + frame_node.data_set(key="directive", value=directive); + return directive; + } + elif required == False { + if not extraction_result{ + # set extraction result to N/A + extraction_result = {self.label:"N/A"}; + } + } + if extraction_result{ + # store the result in the frame node + self.update_responses(extraction_result, frame); + return True; + } + } + + def generate_extraction_prompt() -> str { + # accepts the question index schema and prepares an extraction prompt + entities_list = []; + sample_json_lines = []; + + if(constraints := self.state_info.get('constraints', {})){ + + desc = constraints.get('description', ''); + other_constraints = {k: v for (k, v) in constraints.items() if k != 'description'}; + constraint_strs = [f"{k}: {v}" for (k, v) in other_constraints.items()]; + constraint_part = f" ({', '.join(constraint_strs)})" if constraint_strs else ""; + entities_list.append(f"- {self.state_info.get("name")}: {desc}{constraint_part}"); + sample_json_lines.append(f" '{self.state_info.get("name")}': ''"); + + + entities = "\n".join(entities_list); + sample_json = '{\n' + ',\n'.join(sample_json_lines) + '\n}'; + # prepate the prompt + prompt = self.extraction_prompt.format(entities=entities, sample_json=sample_json); + # escape the conflicting symbols + prompt = prompt.replace('{', '{{').replace('}','}}'); + + return prompt; + } + else { + return ""; + } + } + + 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; + } + } + + # return None; + } + + def update_responses(responses:dict, frame:Frame) { + 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); + } + + +} + diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac new file mode 100644 index 0000000..fdab0c1 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -0,0 +1,427 @@ +import logging; +import traceback; +import from enum { unique } +import from typing { Union } +import from logging { Logger } +import from jivas.agent.action.action { Action } +import from jivas.agent.modules.data.node_get { node_get } +import from jivas.agent.modules.system.common { node_obj } +import from jac_cloud.core.archetype {BaseCollection, NodeAnchor} +import from jivas.agent.modules.data.node_pager { NodePager } +import from jivas.agent.action.subgraph_action.state { State } +import from jivas.agent.action.subgraph_action.completed_state { CompletedState } +import from jivas.agent.action.subgraph_action.confirmed_state { ConfirmedState } +import from jivas.agent.action.subgraph_action.revision_state { RevisionState } +import from jivas.agent.action.subgraph_action.initial_state { InitialState } +import from jivas.agent.memory.collection { Collection } +import from jivas.agent.memory.frame { Frame } +import from jivas.agent.core.graph_node { GraphNode } + + +node SubgraphAction(Action) { + # represents an execution on a subgraph on the agent action graph + has persona_action:str = "PersonaInteractAction"; + has label: str = ""; + has description: str = "subgraph action that executes a series of states"; + has enabled: bool = True; + has states: list = []; + has initial_state:dict = {}; + has completed_state: dict = {}; + has confirmed_state:dict = {}; + has revision_state: dict = {}; + + + # set up logger + static has logger:Logger = logging.getLogger(__name__); + + def postinit { + super.postinit(); + # list of node attributes which are protected from update operation + self.protected_attrs += ['label', 'version', 'agent_id']; + # list of node attributes which should be excluded from export + self.transient_attrs += ['agent_id']; + } + + # creates the subgraph based on data in 'states' + def on_register(){ + agent_node = &self.agent_id; + persona_action = agent_node.get_action(action_label=self.persona_action); + persona_action.import_parameters(self.parameters); + + self.get_agent().get_memory().purge_collection_memory(self.label); + + collection = self.get_collection(); + index = 1; + states_created = {}; + + # initialize initial_state with optional prompt/directive if present + if ("prompt" in self.initial_state and "directive" in self.initial_state) { + initial_state = InitialState( + collection_id=collection.id, + prompt=self.initial_state.get("prompt"), + directive=self.initial_state.get("directive"), + label="initial_state" + ); + } + elif ("prompt" in self.initial_state) { + initial_state = InitialState( + collection_id=collection.id, + prompt=self.initial_state.get("prompt"), + label="initial_state" + ); + } + elif ("directive" in self.initial_state) { + initial_state = InitialState( + collection_id=collection.id, + directive=self.initial_state.get("directive"), + label="initial_state" + ); + } + else { + initial_state = InitialState( + collection_id=collection.id, + label="initial_state" + ); + } + + collection ++> initial_state; + + for state_info in self.states { + states_created[f"state{index}"] = State(state_info=state_info, collection_id=collection.id, label=state_info.get("name", f"state{index}")); + + if(index == 1){ + initial_state ++> states_created[f"state{index}"]; + } + else { + states_created[f"state{index - 1}"] ++> states_created[f"state{index}"]; + } + index += 1; + } + + # initialize completed_state with optional prompt/directive if present + if ("prompt" in self.completed_state and "directive" in self.completed_state) { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + prompt=self.completed_state.get("prompt"), + directive=self.completed_state.get("directive"), + label="completed_state" + ); + } + elif ("prompt" in self.completed_state) { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + prompt=self.completed_state.get("prompt"), + label="completed_state" + ); + } + elif ("directive" in self.completed_state) { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + directive=self.completed_state.get("directive"), + label="completed_state" + ); + } + else { + completed_state = CompletedState( + collection_id=collection.id, + auto_confirm=self.completed_state.get("auto_confirm", False), + label="completed_state" + ); + } + + if ("directive" in self.confirmed_state) { + confirmed_state = ConfirmedState( + collection_id=collection.id, + directive=self.confirmed_state.get("directive"), + label="confirmed_state" + ); + }else{ + confirmed_state = ConfirmedState( + collection_id=collection.id, + label="confirmed_state" + ); + } + + # initialize revision_state with optional prompt/directive if present + if ("prompt" in self.revision_state and "directive" in self.revision_state) { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + prompt=self.revision_state.get("prompt"), + directive=self.revision_state.get("directive"), + label="revision_state" + ); + } + elif ("prompt" in self.revision_state) { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + prompt=self.revision_state.get("prompt"), + label="revision_state" + ); + } + elif ("directive" in self.revision_state) { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + directive=self.revision_state.get("directive"), + label="revision_state" + ); + } + else { + revision_state = RevisionState( + collection_id=collection.id, + states=self.states, + label="revision_state" + ); + } + + states_created[f"state{index - 1}"] ++> completed_state; + completed_state ++> confirmed_state; + completed_state ++> revision_state; + + self.logger.info(f"Created states in subgraph"); + } + + def run(frame_node:Frame) -> str { + + self.pre_process(frame_node); + + walker_result = self.walk_subgraph(frame_node); + directives = walker_result.directives.copy(); + walker_result.directives = {}; + + responses = frame_node.data_get(key=f"{self.label}_results") or {}; + + self.process_responses(frame_node, responses); + + if (responses.get("confirm_response", "")){ + frame_node.data_set(key=f"{self.label}_results", value={}); + } + return directives; + + } + + def walk_subgraph(frame_node:Frame) -> Union[str, dict] { + collection = self.get_collection(); + walker_result = collection spawn _subgraph(frame_node=frame_node, agent_node=&self.agent_id, action_label=self.label); + return walker_result; + } + + def get_states(){ + collection = self.get_collection(); + + states_list = node_get({ + "archetype.collection_id": collection.id, + }); + return states_list; + } + + def list_states(page:int=1, limit:int=20) -> list[dict]{ + collection = self.get_collection(); + + # Initialize pager + pager = NodePager(NodeAnchor.Collection, page_size=limit, current_page=page); + + # Get a page of results + items = pager.get_page({ + "$or": [ + {"$and": [{"name": "InitialState"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "State"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "CompletedState"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "ConfirmedState"}, {"archetype.collection_id": collection.id}]}, + {"$and": [{"name": "RevisionState"}, {"archetype.collection_id": collection.id}]} + ] + + }); + + if not items { + return {}; + } + + # call export on each and convert it to a dict + items = [item.export() for item in items]; + + # get all info as a dict + pagination_info = pager.to_dict(); + print("items\n", items); + + return { + "page": page, + "limit": limit, + "items": items + }; + } + + def get_state(name:str, label:str){ + collection = self.get_collection(); + + state_node = node_obj(node_get({ + "archetype.collection_id": collection.id, + "archetype.label": label + })); + + return state_node; + } + + def update_state(id:str, data:dict) -> GraphNode { + collection = self.get_collection(); + + state_node = node_obj(node_get({ + "archetype.collection_id": collection.id, + "archetype.id": id + })); + + # updates an state node; expects a dict of attribute names mapped to values for updating + # overridden to respond to enable / disable updates + enabled_changed = False; + non_enabled_changed = False; + + if (data) { + for attr in data.keys() { + if (attr not in state_node.protected_attrs) { + # check if attribute is a node attribute + if (hasattr(state_node, attr)) { + # handle changes in disabled/enabled status + if attr == 'enabled' { + current_val = getattr(state_node, 'enabled'); + if current_val != data[attr] { + enabled_changed = True; + if data[attr] == True { + state_node.on_enable(); + } else { + state_node.on_disable(); + } + setattr(state_node, attr, data[attr]); + } + } else { + current_val = getattr(state_node, attr); + if current_val != data[attr] { + non_enabled_changed = True; + } + setattr(state_node, attr, data[attr]); + } + } else { + # Handle context attributes; only mark as changed if the value is new or different + if (attr in state_node._context) { + if state_node._context[attr] != data[attr] { + non_enabled_changed = True; + state_node._context[attr] = data[attr]; + } + } else { + non_enabled_changed = True; + state_node._context[attr] = data[attr]; + } + } + } + } + } + + # Conditionally trigger post_update only when: + # 1. Node is enabled AND + # 2. There were non-enabled changes OR no enabled changes occurred + if state_node.enabled and (non_enabled_changed or not enabled_changed) { + state_node.post_update(); + } + + return state_node; + } + + def process_responses(frame_node:Frame, responses:dict) abs; + + def pre_process(frame_node:Frame) abs; + +} + +walker _subgraph { + # set up logger + static has logger: Logger = logging.getLogger(__name__); + + has directives: list = []; + has action_label: str = ""; + has agent_node: GraphNode = None; + has frame_node: Frame = None; + + obj __specs__ { + static has private:bool = True; + } + + can on_collection with Collection entry { + visit [-->](`?State) else { + disengage; + } + + } + + can on_state with State entry { + if here.touch(self) and here.enabled { + # execute the action + response = here.run(self); + if(type(response) is bool) { + visit [-->]else { + disengage; + } + } + elif(type(response) is str) { + self.set_directives(response); + disengage; + } + } + else { + visit [-->] else { + disengage; + } + } + + } + can on_completed_state with CompletedState entry { + if here.touch(self) { + # execute the action + response=here.run(self); + if response == True { + visit[-->](`?ConfirmedState) else { + disengage; + } + } + elif response == False{ + visit[-->](`?RevisionState)else{ + disengage; + } + } + elif(type(response) is str) { + self.set_directives(response); + disengage; + } + } + } + can on_confirmed_state with ConfirmedState entry{ + response = here.run(self); + self.set_directives(response); + } + def set_directives(directive:str){ + self.directives.append(directive); + } + can on_revision_state with RevisionState entry{ + if here.touch(self) { + # execute the action + response=here.run(self); + if response is True { + visit [<--](`?CompletedState) else { + disengage; + } + } + elif(type(response) is str) { + self.set_directives(response); + disengage; + } + } + } + def set_directives(directive:str){ + self.directives.append(directive); + } +} + + diff --git a/core/jivas/agent/action/subgraph_action/update_state.jac b/core/jivas/agent/action/subgraph_action/update_state.jac new file mode 100644 index 0000000..238b694 --- /dev/null +++ b/core/jivas/agent/action/subgraph_action/update_state.jac @@ -0,0 +1,42 @@ +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 actions.jivas.subgraph_action.subgraph_action { SubgraphAction } +import from jivas.agent.action.agent_graph_walker { agent_graph_walker } + + +walker update_state(agent_graph_walker) { + # action endpoint for listing all documents processed by the deepdoc service + + has state_id:str = ""; + has data:dict = {}; + has response:dict = {}; + has reporting:bool = True; + + # 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); + } + + + can on_action with Action entry { + self.response = here.update_state(id = self.state_id, data = self.data); + + if self.reporting { + report self.response; + } + } +} \ No newline at end of file diff --git a/core/jivas/agent/lib.jac b/core/jivas/agent/lib.jac index e671644..cc5ca94 100644 --- a/core/jivas/agent/lib.jac +++ b/core/jivas/agent/lib.jac @@ -25,6 +25,11 @@ import from jivas.agent.action { stt } +import from jivas.agent.action.subgraph_action { + list_states, + update_state +} + import from jivas.agent.memory { add_frame, get_frames, From 6452002055c391a0b014718b2c79d1a7d7d1dbf5 Mon Sep 17 00:00:00 2001 From: Isoke Date: Tue, 21 Oct 2025 16:33:12 -0400 Subject: [PATCH 03/14] added update_responses ability to subgraph action --- .../action/subgraph_action/subgraph_action.jac | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index fdab0c1..b3bb4b7 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -211,6 +211,21 @@ node SubgraphAction(Action) { return walker_result; } + def update_responses(responses:dict, frame:Frame) { + 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); + } + def get_states(){ collection = self.get_collection(); From 8cc4284a1d54b983b0d7d493ced09ad2dbf083f6 Mon Sep 17 00:00:00 2001 From: Isoke Date: Wed, 22 Oct 2025 16:41:21 -0400 Subject: [PATCH 04/14] bug fixes --- .../subgraph_action/completed_state.jac | 25 +------------------ .../subgraph_action/confirmed_state.jac | 6 ----- .../action/subgraph_action/initial_state.jac | 2 +- .../subgraph_action/subgraph_action.jac | 11 ++++---- 4 files changed, 8 insertions(+), 36 deletions(-) diff --git a/core/jivas/agent/action/subgraph_action/completed_state.jac b/core/jivas/agent/action/subgraph_action/completed_state.jac index bc8b353..421d908 100644 --- a/core/jivas/agent/action/subgraph_action/completed_state.jac +++ b/core/jivas/agent/action/subgraph_action/completed_state.jac @@ -65,19 +65,15 @@ node CompletedState(GraphNode) { frame_node = frame.frame_node; agent_node = frame.agent_node; - self.set_vars(); - states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results"); print("got_states_data"); revised = states_data.get("revised", False); if self.auto_confirm { branch_choice_response ={"confirm_response": True}; - self.update_responses(branch_choice_response); + self.update_responses(branch_choice_response, frame); } elif not revised{ - print("revised is False"); - # 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); @@ -93,10 +89,8 @@ node CompletedState(GraphNode) { if not branch_choice_response { # Retrieve the interview session from the frame node responses = frame_node.data_get(key=f"{frame.action_label}_results"); - print(f"Interview session data: {responses}"); if responses and isinstance(responses, dict) and len(responses) > 0 { - print("has valid data"); summary_lines = []; for (field, value) in responses.items() { summary_lines.append(f"- **{field}**: {value}"); @@ -105,7 +99,6 @@ node CompletedState(GraphNode) { } else { summary = ""; } - print(summary); directive = self.directive.replace("{summary}", summary); @@ -115,29 +108,13 @@ node CompletedState(GraphNode) { return directive; } elif confirmed{ - print("we have a confirmation response"); return True; } elif not confirmed{ - print("user has not confirmed\n"); return False; } } - def set_vars(){ - # setting any custom prompts or directives - if ( prompt := self.state_info.get("prompt", "") ){ - print("completed state var setting"); - self.prompt = prompt; - print("new prompt", prompt); - } - if ( directive := self.state_info.get("directive", "") ){ - print("completed state var settting"); - self.directive = directive; - print("new directive", directive); - } - } - 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 print("\nCalling LLM with prompt"); diff --git a/core/jivas/agent/action/subgraph_action/confirmed_state.jac b/core/jivas/agent/action/subgraph_action/confirmed_state.jac index 5feea15..f456db1 100644 --- a/core/jivas/agent/action/subgraph_action/confirmed_state.jac +++ b/core/jivas/agent/action/subgraph_action/confirmed_state.jac @@ -34,12 +34,6 @@ node ConfirmedState(State) { if ( directive := self.state_info.get("directive", "") ){ self.directive = directive; } - - # Retrieve the interview session from the frame node - interview_session = frame_node.data_get(key=f"{frame.action_label}_results"); - print(f"Interview session data: {interview_session}"); - - return self.directive; } } \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/initial_state.jac b/core/jivas/agent/action/subgraph_action/initial_state.jac index 35618dc..33c6fec 100644 --- a/core/jivas/agent/action/subgraph_action/initial_state.jac +++ b/core/jivas/agent/action/subgraph_action/initial_state.jac @@ -2,7 +2,7 @@ import logging; import traceback; import from typing { Union } import from logging { Logger } -import from jivas.agent.action.state { State } +import from jivas.agent.action.subgraph_action.state { State } import from jivas.agent.memory.frame { Frame } diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index b3bb4b7..c001ced 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -196,13 +196,15 @@ node SubgraphAction(Action) { responses = frame_node.data_get(key=f"{self.label}_results") or {}; - self.process_responses(frame_node, responses); + additional_directives = self.process_responses(frame_node, responses); + if additional_directives{ + directives.append(additional_directives); + } if (responses.get("confirm_response", "")){ frame_node.data_set(key=f"{self.label}_results", value={}); } return directives; - } def walk_subgraph(frame_node:Frame) -> Union[str, dict] { @@ -212,7 +214,7 @@ node SubgraphAction(Action) { } def update_responses(responses:dict, frame:Frame) { - stored_responses = frame.frame_node.data_get(key=f"{frame.action_label}_results"); + stored_responses = frame.data_get(key=f"{self.label}_results"); if type(stored_responses) is not dict or not stored_responses { stored_responses = {}; @@ -223,7 +225,7 @@ node SubgraphAction(Action) { stored_responses[key] = value; } - frame.frame_node.data_set(key=f"{frame.action_label}_results", value=stored_responses); + frame.data_set(key=f"{self.label}_results", value=stored_responses); } def get_states(){ @@ -262,7 +264,6 @@ node SubgraphAction(Action) { # get all info as a dict pagination_info = pager.to_dict(); - print("items\n", items); return { "page": page, From 0234792afa0161436ad9ef2aba2a82ae87312937 Mon Sep 17 00:00:00 2001 From: Isoke Date: Fri, 24 Oct 2025 16:10:26 -0400 Subject: [PATCH 05/14] bug fixes --- .../subgraph_action/completed_state.jac | 12 +++------ .../subgraph_action/confirmed_state.jac | 1 - .../action/subgraph_action/initial_state.jac | 1 + .../action/subgraph_action/list_states.jac | 2 +- .../action/subgraph_action/revision_state.jac | 3 ++- .../agent/action/subgraph_action/state.jac | 6 +++-- .../subgraph_action/subgraph_action.jac | 27 +++++++++++-------- .../action/subgraph_action/update_state.jac | 4 +-- 8 files changed, 30 insertions(+), 26 deletions(-) diff --git a/core/jivas/agent/action/subgraph_action/completed_state.jac b/core/jivas/agent/action/subgraph_action/completed_state.jac index 421d908..06af7d4 100644 --- a/core/jivas/agent/action/subgraph_action/completed_state.jac +++ b/core/jivas/agent/action/subgraph_action/completed_state.jac @@ -3,6 +3,7 @@ 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 } @@ -31,7 +32,9 @@ node CompletedState(GraphNode) { - 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 as well as inform the user that they can request changes or cancel altogether. + 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 = """ @@ -66,7 +69,6 @@ node CompletedState(GraphNode) { agent_node = frame.agent_node; states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results"); - print("got_states_data"); revised = states_data.get("revised", False); if self.auto_confirm { @@ -117,7 +119,6 @@ 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] { # performs function tool calling for extracting question responses based on question - print("\nCalling LLM with prompt"); prompt_messages = []; if not prompt { @@ -129,7 +130,6 @@ node CompletedState(GraphNode) { use_history = history; } visitor_utterance = frame_node.data_get(key="visitor_utterance"); - print(f"Visitor utterance: {visitor_utterance}"); if not visitor_utterance { return None; @@ -150,12 +150,10 @@ node CompletedState(GraphNode) { } } - print(f"Model action: {self.model_action}"); model_action = agent_node.get_action(action_label=self.model_action); if model_action { - # print("\n Has model action, calling model\n", model_action); model_action_result = model_action.call_model( prompt_messages=prompt_messages, prompt_variables={}, @@ -185,13 +183,11 @@ node CompletedState(GraphNode) { stored_responses = {}; } - print(f"Stored responses before update: {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); - print(f"Stored responses after update: {frame.frame_node.data_get(key=f'{frame.action_label}_results')}"); } } \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/confirmed_state.jac b/core/jivas/agent/action/subgraph_action/confirmed_state.jac index f456db1..c686d2f 100644 --- a/core/jivas/agent/action/subgraph_action/confirmed_state.jac +++ b/core/jivas/agent/action/subgraph_action/confirmed_state.jac @@ -21,7 +21,6 @@ node ConfirmedState(State) { def on_disable() { } def touch(frame:Frame) -> bool { - print(f"Touching CONFIRMED state: {self.label}"); # Always allow entering the COMPLETED state return True; } diff --git a/core/jivas/agent/action/subgraph_action/initial_state.jac b/core/jivas/agent/action/subgraph_action/initial_state.jac index 33c6fec..c457d0f 100644 --- a/core/jivas/agent/action/subgraph_action/initial_state.jac +++ b/core/jivas/agent/action/subgraph_action/initial_state.jac @@ -3,6 +3,7 @@ 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 } diff --git a/core/jivas/agent/action/subgraph_action/list_states.jac b/core/jivas/agent/action/subgraph_action/list_states.jac index 2790530..0d127dc 100644 --- a/core/jivas/agent/action/subgraph_action/list_states.jac +++ b/core/jivas/agent/action/subgraph_action/list_states.jac @@ -3,7 +3,7 @@ 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 actions.jivas.subgraph_action.subgraph_action { SubgraphAction } +import from jivas.agent.action.subgraph_action.subgraph_action { SubgraphAction } import from jivas.agent.action.agent_graph_walker { agent_graph_walker } diff --git a/core/jivas/agent/action/subgraph_action/revision_state.jac b/core/jivas/agent/action/subgraph_action/revision_state.jac index 783fd6c..764659f 100644 --- a/core/jivas/agent/action/subgraph_action/revision_state.jac +++ b/core/jivas/agent/action/subgraph_action/revision_state.jac @@ -3,6 +3,7 @@ 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 } @@ -27,7 +28,7 @@ node RevisionState(GraphNode) { Review the user's message and the recorded responses to accurately extract the following entities and update the relevant recorded responses. Be strict on the constraints specified for each entity. Return a JSON object with keys exactly as listed below. Include only keys for which you could extract a valid revised value adhering to all constraints. - Do not extract value if it is the same as the one in responses. + Do not extract value if it is the same as the one in responses but always extract if the user states a different value from the one in the recorded responses. Entities to extract: {entities} diff --git a/core/jivas/agent/action/subgraph_action/state.jac b/core/jivas/agent/action/subgraph_action/state.jac index 44d93b5..a92239e 100644 --- a/core/jivas/agent/action/subgraph_action/state.jac +++ b/core/jivas/agent/action/subgraph_action/state.jac @@ -4,6 +4,7 @@ import from typing { Union } import from logging { Logger } import from jivas.agent.core.graph_node { GraphNode } import from jivas.agent.memory.frame { Frame } +import from jivas.agent.action.action { Action } node State(GraphNode) { # represents an execution on a subgraph on the agent action graph @@ -62,13 +63,14 @@ node State(GraphNode) { state_data = frame_node.data_get(key=f"{frame.action_label}_results"); if state_data{ state_value = state_data.get(self.label, None); + confirmed = state_data.get("confirm_response", False); } else{ state_value = {}; + confirmed = False; } - - if state_value { + if state_value or confirmed{ return False; } else { diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index c001ced..9840cfb 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -15,6 +15,7 @@ import from jivas.agent.action.subgraph_action.revision_state { RevisionState } import from jivas.agent.action.subgraph_action.initial_state { InitialState } import from jivas.agent.memory.collection { Collection } import from jivas.agent.memory.frame { Frame } +import from jivas.agent.core.graph_walker { graph_walker } import from jivas.agent.core.graph_node { GraphNode } @@ -196,16 +197,20 @@ node SubgraphAction(Action) { responses = frame_node.data_get(key=f"{self.label}_results") or {}; - additional_directives = self.process_responses(frame_node, responses); - if additional_directives{ - directives.append(additional_directives); + custom_directives = self.process_responses(frame_node, responses); + if custom_directives{ + directives = custom_directives; } if (responses.get("confirm_response", "")){ frame_node.data_set(key=f"{self.label}_results", value={}); } return directives; - } + } + + def set_directives(directive:str){ + self.directives.append(directive); + } def walk_subgraph(frame_node:Frame) -> Union[str, dict] { collection = self.get_collection(); @@ -252,7 +257,6 @@ node SubgraphAction(Action) { {"$and": [{"name": "ConfirmedState"}, {"archetype.collection_id": collection.id}]}, {"$and": [{"name": "RevisionState"}, {"archetype.collection_id": collection.id}]} ] - }); if not items { @@ -366,10 +370,9 @@ walker _subgraph { } can on_collection with Collection entry { - visit [-->](`?State) else { + visit [-->] (`?State) else { disengage; - } - + } } can on_state with State entry { @@ -377,7 +380,7 @@ walker _subgraph { # execute the action response = here.run(self); if(type(response) is bool) { - visit [-->]else { + visit [-->] else { disengage; } } @@ -390,9 +393,9 @@ walker _subgraph { visit [-->] else { disengage; } - } - + } } + can on_completed_state with CompletedState entry { if here.touch(self) { # execute the action @@ -435,6 +438,8 @@ walker _subgraph { } } } + can on_exit with exit { + } def set_directives(directive:str){ self.directives.append(directive); } diff --git a/core/jivas/agent/action/subgraph_action/update_state.jac b/core/jivas/agent/action/subgraph_action/update_state.jac index 238b694..66a51d6 100644 --- a/core/jivas/agent/action/subgraph_action/update_state.jac +++ b/core/jivas/agent/action/subgraph_action/update_state.jac @@ -3,12 +3,12 @@ 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 actions.jivas.subgraph_action.subgraph_action { SubgraphAction } +import from jivas.agent.action.subgraph_action.subgraph_action { SubgraphAction } import from jivas.agent.action.agent_graph_walker { agent_graph_walker } walker update_state(agent_graph_walker) { - # action endpoint for listing all documents processed by the deepdoc service + # action endpoint for updating states in a subgraph has state_id:str = ""; has data:dict = {}; From bb6841e0b52af9ad4abe0bf43dc7874d6e945267 Mon Sep 17 00:00:00 2001 From: Isoke Date: Mon, 27 Oct 2025 01:25:15 -0400 Subject: [PATCH 06/14] removed trailing whitespaces --- .../subgraph_action/completed_state.jac | 16 +++++----- .../subgraph_action/confirmed_state.jac | 6 ++-- .../action/subgraph_action/initial_state.jac | 4 +-- .../action/subgraph_action/list_states.jac | 2 +- .../action/subgraph_action/revision_state.jac | 8 ++--- .../agent/action/subgraph_action/state.jac | 22 ++++++------- .../subgraph_action/subgraph_action.jac | 32 +++++++++---------- .../action/subgraph_action/update_state.jac | 2 +- 8 files changed, 44 insertions(+), 48 deletions(-) diff --git a/core/jivas/agent/action/subgraph_action/completed_state.jac b/core/jivas/agent/action/subgraph_action/completed_state.jac index 06af7d4..7d0ddbc 100644 --- a/core/jivas/agent/action/subgraph_action/completed_state.jac +++ b/core/jivas/agent/action/subgraph_action/completed_state.jac @@ -9,7 +9,7 @@ import from jivas.agent.memory.frame { Frame } node CompletedState(GraphNode) { - # Represents an execution on a subgraph on the agent action graph + # 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"; @@ -33,7 +33,7 @@ node CompletedState(GraphNode) { - 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 request to know if the presented details are accurate - a message to inform the user that they can request changes or cancel altogether. """; @@ -58,8 +58,8 @@ node CompletedState(GraphNode) { def on_enable() { } # override to execute operations upon disabling of action - def on_disable() { } - + def on_disable() { } + def touch(frame:Frame) -> bool { return True; } @@ -75,7 +75,7 @@ node CompletedState(GraphNode) { branch_choice_response ={"confirm_response": True}; self.update_responses(branch_choice_response, frame); } - elif not revised{ + 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); @@ -86,7 +86,7 @@ node CompletedState(GraphNode) { 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 @@ -107,7 +107,7 @@ node CompletedState(GraphNode) { # Change revised in frame to false revised_state = {"revised":False}; self.update_responses(revised_state, frame); - return directive; + return directive; } elif confirmed{ return True; @@ -171,7 +171,7 @@ node CompletedState(GraphNode) { } else { return None; - } + } } } diff --git a/core/jivas/agent/action/subgraph_action/confirmed_state.jac b/core/jivas/agent/action/subgraph_action/confirmed_state.jac index c686d2f..2dd4a6f 100644 --- a/core/jivas/agent/action/subgraph_action/confirmed_state.jac +++ b/core/jivas/agent/action/subgraph_action/confirmed_state.jac @@ -7,7 +7,7 @@ import from jivas.agent.memory.frame { Frame } node ConfirmedState(State) { - # Represents an execution on a subgraph on the agent action graph + # Represents an execution on a subgraph on the agent action graph has collection_id: str = ""; has enabled: bool = True; @@ -18,7 +18,7 @@ node ConfirmedState(State) { def on_enable() { } # override to execute operations upon disabling of action - def on_disable() { } + def on_disable() { } def touch(frame:Frame) -> bool { # Always allow entering the COMPLETED state @@ -28,7 +28,7 @@ node ConfirmedState(State) { 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; diff --git a/core/jivas/agent/action/subgraph_action/initial_state.jac b/core/jivas/agent/action/subgraph_action/initial_state.jac index c457d0f..4563c49 100644 --- a/core/jivas/agent/action/subgraph_action/initial_state.jac +++ b/core/jivas/agent/action/subgraph_action/initial_state.jac @@ -8,7 +8,7 @@ import from jivas.agent.memory.frame { Frame } node InitialState(State) { - # Represents an execution on a subgraph on the agent action graph + # Represents an execution on a subgraph on the agent action graph has collection_id: str = ""; has label: str = ""; @@ -30,7 +30,7 @@ node InitialState(State) { 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 { diff --git a/core/jivas/agent/action/subgraph_action/list_states.jac b/core/jivas/agent/action/subgraph_action/list_states.jac index 0d127dc..a58b8a3 100644 --- a/core/jivas/agent/action/subgraph_action/list_states.jac +++ b/core/jivas/agent/action/subgraph_action/list_states.jac @@ -32,7 +32,7 @@ walker list_states(agent_graph_walker) { can on_actions with Actions entry { visit [-->](`?SubgraphAction)(?enabled==True); } - + can on_action with Action entry { # get the list of documents from the manifest diff --git a/core/jivas/agent/action/subgraph_action/revision_state.jac b/core/jivas/agent/action/subgraph_action/revision_state.jac index 764659f..dc011ba 100644 --- a/core/jivas/agent/action/subgraph_action/revision_state.jac +++ b/core/jivas/agent/action/subgraph_action/revision_state.jac @@ -9,7 +9,7 @@ import from jivas.agent.memory.frame { Frame } node RevisionState(GraphNode) { - # Represents an execution on a subgraph on the agent action graph + # Represents an execution on a subgraph on the agent action graph has collection_id: str = ""; has label: str = ""; has state_info:dict = {}; @@ -27,7 +27,7 @@ node RevisionState(GraphNode) { has prompt:str = """ Review the user's message and the recorded responses to accurately extract the following entities and update the relevant recorded responses. Be strict on the constraints specified for each entity. Return a JSON object with keys exactly as listed below. - Include only keys for which you could extract a valid revised value adhering to all constraints. + Include only keys for which you could extract a valid revised value adhering to all constraints. Do not extract value if it is the same as the one in responses but always extract if the user states a different value from the one in the recorded responses. Entities to extract: @@ -46,7 +46,7 @@ node RevisionState(GraphNode) { def on_enable() { } # override to execute operations upon disabling of action - def on_disable() { } + def on_disable() { } def touch(frame:Frame) -> bool { states_data = frame.frame_node.data_get(key=f"{frame.action_label}_results"); @@ -62,7 +62,7 @@ node RevisionState(GraphNode) { def run(frame:Frame) -> Union[str, bool] { frame_node = frame.frame_node; agent_node = frame.agent_node; - + # Check for any changes in the latest user message prompt = self.generate_revision_extraction_prompt(frame); extraction_result = self.call_llm(prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node); diff --git a/core/jivas/agent/action/subgraph_action/state.jac b/core/jivas/agent/action/subgraph_action/state.jac index a92239e..4826ef7 100644 --- a/core/jivas/agent/action/subgraph_action/state.jac +++ b/core/jivas/agent/action/subgraph_action/state.jac @@ -7,14 +7,14 @@ import from jivas.agent.memory.frame { Frame } import from jivas.agent.action.action { Action } node State(GraphNode) { - # represents an execution on a subgraph on the agent action graph + # represents an execution on a subgraph on the agent action graph has collection_id:str = ""; has label: str = ""; has description: str = "specialized state"; has enabled: bool = True; has state_info: dict = {}; - + #llm model parameters has model_action: str = "LangChainModelAction"; @@ -37,8 +37,8 @@ node State(GraphNode) { Return ONLY the JSON object with the extracted entities, no delimiters. Do not include any other text or explanation. The JSON must have the following structure (only include keys with valid values): {sample_json} - """; - + """; + # set up logger static has logger:Logger = logging.getLogger(__name__); @@ -55,7 +55,7 @@ node State(GraphNode) { def on_enable() { } # override to execute operations upon disabling of action - def on_disable() { } + def on_disable() { } def touch(frame:Frame) -> bool { frame_node = frame.frame_node; @@ -78,9 +78,8 @@ node State(GraphNode) { return True; } } - + def run(frame:Frame)->Union[str, bool]{ - agent_node = frame.agent_node; frame_node = frame.frame_node; required = self.state_info.get("required",""); @@ -93,7 +92,7 @@ node State(GraphNode) { question = self.state_info.get("question", ""); constraints = self.state_info.get("constraints", {}); description = constraints.get("description", ""); - + directive = "Tailor your response to get the information needed based on the following description: \n" + description + "\n Eg." + question; if(options:= constraints.get("options", "")){ @@ -128,7 +127,7 @@ node State(GraphNode) { constraint_part = f" ({', '.join(constraint_strs)})" if constraint_strs else ""; entities_list.append(f"- {self.state_info.get("name")}: {desc}{constraint_part}"); sample_json_lines.append(f" '{self.state_info.get("name")}': ''"); - + entities = "\n".join(entities_list); sample_json = '{\n' + ',\n'.join(sample_json_lines) + '\n}'; @@ -197,7 +196,7 @@ node State(GraphNode) { } else { return None; - } + } } # return None; @@ -218,6 +217,5 @@ node State(GraphNode) { frame.frame_node.data_set(key=f"{frame.action_label}_results", value=stored_responses); } - -} +} diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index 9840cfb..ca481f1 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -20,7 +20,7 @@ import from jivas.agent.core.graph_node { GraphNode } node SubgraphAction(Action) { - # represents an execution on a subgraph on the agent action graph + # represents an execution on a subgraph on the agent action graph has persona_action:str = "PersonaInteractAction"; has label: str = ""; has description: str = "subgraph action that executes a series of states"; @@ -84,7 +84,7 @@ node SubgraphAction(Action) { label="initial_state" ); } - + collection ++> initial_state; for state_info in self.states { @@ -141,11 +141,11 @@ node SubgraphAction(Action) { ); }else{ confirmed_state = ConfirmedState( - collection_id=collection.id, + collection_id=collection.id, label="confirmed_state" ); } - + # initialize revision_state with optional prompt/directive if present if ("prompt" in self.revision_state and "directive" in self.revision_state) { revision_state = RevisionState( @@ -179,14 +179,14 @@ node SubgraphAction(Action) { label="revision_state" ); } - + states_created[f"state{index - 1}"] ++> completed_state; completed_state ++> confirmed_state; completed_state ++> revision_state; - self.logger.info(f"Created states in subgraph"); + self.logger.info(f"Created states in subgraph"); } - + def run(frame_node:Frame) -> str { self.pre_process(frame_node); @@ -202,15 +202,15 @@ node SubgraphAction(Action) { directives = custom_directives; } - if (responses.get("confirm_response", "")){ + if (responses.get("confirm_response", "")){ frame_node.data_set(key=f"{self.label}_results", value={}); } return directives; - } + } def set_directives(directive:str){ self.directives.append(directive); - } + } def walk_subgraph(frame_node:Frame) -> Union[str, dict] { collection = self.get_collection(); @@ -351,9 +351,9 @@ node SubgraphAction(Action) { } def process_responses(frame_node:Frame, responses:dict) abs; - + def pre_process(frame_node:Frame) abs; - + } walker _subgraph { @@ -372,13 +372,13 @@ walker _subgraph { can on_collection with Collection entry { visit [-->] (`?State) else { disengage; - } + } } can on_state with State entry { if here.touch(self) and here.enabled { # execute the action - response = here.run(self); + response = here.run(self); if(type(response) is bool) { visit [-->] else { disengage; @@ -414,7 +414,7 @@ walker _subgraph { self.set_directives(response); disengage; } - } + } } can on_confirmed_state with ConfirmedState entry{ response = here.run(self); @@ -444,5 +444,3 @@ walker _subgraph { self.directives.append(directive); } } - - diff --git a/core/jivas/agent/action/subgraph_action/update_state.jac b/core/jivas/agent/action/subgraph_action/update_state.jac index 66a51d6..13b63cf 100644 --- a/core/jivas/agent/action/subgraph_action/update_state.jac +++ b/core/jivas/agent/action/subgraph_action/update_state.jac @@ -30,7 +30,7 @@ walker update_state(agent_graph_walker) { can on_actions with Actions entry { visit [-->](`?SubgraphAction)(?enabled==True); } - + can on_action with Action entry { self.response = here.update_state(id = self.state_id, data = self.data); From 13d183dd313e260938657f0b6e4d3d07d15c9e4e Mon Sep 17 00:00:00 2001 From: Isoke Date: Fri, 31 Oct 2025 08:11:24 -0400 Subject: [PATCH 07/14] checks for conditionals --- .../jivas/agent/action/subgraph_action/state.jac | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/jivas/agent/action/subgraph_action/state.jac b/core/jivas/agent/action/subgraph_action/state.jac index 4826ef7..ce17000 100644 --- a/core/jivas/agent/action/subgraph_action/state.jac +++ b/core/jivas/agent/action/subgraph_action/state.jac @@ -73,10 +73,20 @@ node State(GraphNode) { if state_value or confirmed{ return False; } - else { - # authorize the walker to run this action - return True; + + if (conditional := self.state_info.get('conditional', {})) { + + for (field, expected_value) in conditional.items(){ + actual_value = state_data.get(field, None); + if actual_value != expected_value{ + return False; + } + } } + + # authorize the walker to run this action + return True; + } def run(frame:Frame)->Union[str, bool]{ From 75b1f08f44e566ce097bd8723d814401e5532167 Mon Sep 17 00:00:00 2001 From: Isoke Date: Fri, 31 Oct 2025 08:19:33 -0400 Subject: [PATCH 08/14] version update --- __init__.py | 2 +- core/jivas/agent/action/subgraph_action/subgraph_action.jac | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 3727c67..7c613da 100644 --- a/__init__.py +++ b/__init__.py @@ -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" diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index ca481f1..ce9c5af 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -218,7 +218,7 @@ node SubgraphAction(Action) { return walker_result; } - def update_responses(responses:dict, frame:Frame) { + def update_response(responses:dict, frame:Frame) { stored_responses = frame.data_get(key=f"{self.label}_results"); if type(stored_responses) is not dict or not stored_responses { From fe46bebd01f29d4848670a38fb5b9f99f32515f9 Mon Sep 17 00:00:00 2001 From: Isoke Date: Fri, 31 Oct 2025 09:46:28 -0400 Subject: [PATCH 09/14] name change --- core/jivas/agent/action/subgraph_action/subgraph_action.jac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index ce9c5af..56963ee 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -197,7 +197,7 @@ node SubgraphAction(Action) { responses = frame_node.data_get(key=f"{self.label}_results") or {}; - custom_directives = self.process_responses(frame_node, responses); + custom_directives = self.process_response(frame_node, responses); if custom_directives{ directives = custom_directives; } From 3d71572747a5d14dced8702dd2ef22c01a954706 Mon Sep 17 00:00:00 2001 From: Isoke Date: Fri, 31 Oct 2025 10:11:25 -0400 Subject: [PATCH 10/14] name change --- core/jivas/agent/action/subgraph_action/subgraph_action.jac | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index 56963ee..dda273e 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -197,7 +197,7 @@ node SubgraphAction(Action) { responses = frame_node.data_get(key=f"{self.label}_results") or {}; - custom_directives = self.process_response(frame_node, responses); + custom_directives = self.process_response(responses, frame_node); if custom_directives{ directives = custom_directives; } @@ -350,7 +350,7 @@ node SubgraphAction(Action) { return state_node; } - def process_responses(frame_node:Frame, responses:dict) abs; + def process_response(frame_node:Frame, responses:dict) abs; def pre_process(frame_node:Frame) abs; From b2330836e38b4f6cd097ff9444ed7ed9d2ea9acf Mon Sep 17 00:00:00 2001 From: Isoke Date: Fri, 31 Oct 2025 10:17:40 -0400 Subject: [PATCH 11/14] reorder arguments --- core/jivas/agent/action/subgraph_action/subgraph_action.jac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index dda273e..f926585 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -350,7 +350,7 @@ node SubgraphAction(Action) { return state_node; } - def process_response(frame_node:Frame, responses:dict) abs; + def process_response(responses:dict, frame_node:Frame) abs; def pre_process(frame_node:Frame) abs; From 49b4b5fd786b9c13a92ce8adecd70021176b76fc Mon Sep 17 00:00:00 2001 From: Isoke Date: Fri, 31 Oct 2025 21:09:59 -0400 Subject: [PATCH 12/14] added state label to walker --- core/jivas/agent/action/subgraph_action/list_states.jac | 3 ++- core/jivas/agent/action/subgraph_action/update_state.jac | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/jivas/agent/action/subgraph_action/list_states.jac b/core/jivas/agent/action/subgraph_action/list_states.jac index a58b8a3..26072d4 100644 --- a/core/jivas/agent/action/subgraph_action/list_states.jac +++ b/core/jivas/agent/action/subgraph_action/list_states.jac @@ -16,6 +16,7 @@ walker list_states(agent_graph_walker) { 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__); @@ -30,7 +31,7 @@ walker list_states(agent_graph_walker) { } can on_actions with Actions entry { - visit [-->](`?SubgraphAction)(?enabled==True); + visit [-->](`?SubgraphAction)(?enabled==True)(?label==self.label); } diff --git a/core/jivas/agent/action/subgraph_action/update_state.jac b/core/jivas/agent/action/subgraph_action/update_state.jac index 13b63cf..c8b0072 100644 --- a/core/jivas/agent/action/subgraph_action/update_state.jac +++ b/core/jivas/agent/action/subgraph_action/update_state.jac @@ -14,6 +14,7 @@ walker update_state(agent_graph_walker) { has data:dict = {}; has response:dict = {}; has reporting:bool = True; + has label:str = ""; # set up logger static has logger:Logger = logging.getLogger(__name__); @@ -28,7 +29,7 @@ walker update_state(agent_graph_walker) { } can on_actions with Actions entry { - visit [-->](`?SubgraphAction)(?enabled==True); + visit [-->](`?SubgraphAction)(?enabled==True)(?label==self.label); } From dffdc4979adb5d417cdd574bd965d6b2f7f05211 Mon Sep 17 00:00:00 2001 From: Isoke Date: Mon, 3 Nov 2025 15:46:01 -0400 Subject: [PATCH 13/14] Adding interaction node to run ability in action --- core/jivas/agent/action/action.jac | 3 +- .../action/subgraph_action/initial_state.jac | 21 ++++++++---- .../agent/action/subgraph_action/state.jac | 1 + .../subgraph_action/subgraph_action.jac | 33 ++++++++++++------- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/core/jivas/agent/action/action.jac b/core/jivas/agent/action/action.jac index ee0149b..8c9445c 100644 --- a/core/jivas/agent/action/action.jac +++ b/core/jivas/agent/action/action.jac @@ -5,6 +5,7 @@ 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 @@ -29,7 +30,7 @@ node Action(GraphNode) { # override to execute operations upon registration of action def on_register() { } - def run(frame_node:Frame){} + def run(frame_node:Frame, interaction_node:Interaction){} # override to execute operations upon the reload of action def on_reload() { } diff --git a/core/jivas/agent/action/subgraph_action/initial_state.jac b/core/jivas/agent/action/subgraph_action/initial_state.jac index 4563c49..ae4c95f 100644 --- a/core/jivas/agent/action/subgraph_action/initial_state.jac +++ b/core/jivas/agent/action/subgraph_action/initial_state.jac @@ -14,6 +14,7 @@ node InitialState(State) { 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 = """ @@ -42,14 +43,20 @@ node InitialState(State) { frame_node = frame.frame_node; agent_node = frame.agent_node; - # Retrieve the interview session from the frame node - response = self.call_llm(self.prompt, history=True, json_only=True, frame_node=frame_node, agent_node=agent_node); - if response.get("abort_response"){ + # check if abort was set to true + if self.abort{ frame_node.data_set(key=f"{frame.action_label}_results", value={}); - return self.directive; - } - else{ - return True; + 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; + } } } } \ No newline at end of file diff --git a/core/jivas/agent/action/subgraph_action/state.jac b/core/jivas/agent/action/subgraph_action/state.jac index ce17000..e5b39d5 100644 --- a/core/jivas/agent/action/subgraph_action/state.jac +++ b/core/jivas/agent/action/subgraph_action/state.jac @@ -115,6 +115,7 @@ node State(GraphNode) { if not extraction_result{ # set extraction result to N/A extraction_result = {self.label:"N/A"}; + self.update_responses(extraction_result, frame); } } if extraction_result{ diff --git a/core/jivas/agent/action/subgraph_action/subgraph_action.jac b/core/jivas/agent/action/subgraph_action/subgraph_action.jac index f926585..ff7fec4 100644 --- a/core/jivas/agent/action/subgraph_action/subgraph_action.jac +++ b/core/jivas/agent/action/subgraph_action/subgraph_action.jac @@ -1,7 +1,6 @@ import logging; import traceback; import from enum { unique } -import from typing { Union } import from logging { Logger } import from jivas.agent.action.action { Action } import from jivas.agent.modules.data.node_get { node_get } @@ -30,6 +29,7 @@ node SubgraphAction(Action) { has completed_state: dict = {}; has confirmed_state:dict = {}; has revision_state: dict = {}; + has parameters: list = []; # set up logger @@ -187,19 +187,25 @@ node SubgraphAction(Action) { self.logger.info(f"Created states in subgraph"); } - def run(frame_node:Frame) -> str { - - self.pre_process(frame_node); - + def run(frame_node:Frame, interaction_node:Interaction) -> str { + directives = []; + pre_process = self.pre_process(frame_node, interaction_node); + if type(pre_process) is str{ + directives = [pre_process]; + } + elif type(pre_process) is list{ + directives = pre_process; + } walker_result = self.walk_subgraph(frame_node); - directives = walker_result.directives.copy(); + directives.extend(walker_result.directives); + walker_result.directives = {}; responses = frame_node.data_get(key=f"{self.label}_results") or {}; - custom_directives = self.process_response(responses, frame_node); + custom_directives = self.process_response(responses, frame_node, interaction_node); if custom_directives{ - directives = custom_directives; + directives = (custom_directives); } if (responses.get("confirm_response", "")){ @@ -276,7 +282,7 @@ node SubgraphAction(Action) { }; } - def get_state(name:str, label:str){ + def get_state(label:str){ collection = self.get_collection(); state_node = node_obj(node_get({ @@ -350,9 +356,14 @@ node SubgraphAction(Action) { return state_node; } - def process_response(responses:dict, frame_node:Frame) abs; + def abort_process(value:bool = True){ + initial_state = self.get_state("initial_state"); + initial_state.abort = value; + } + + def process_response(responses:dict, frame_node:Frame, interaction_node:Interaction) abs; - def pre_process(frame_node:Frame) abs; + def pre_process(frame_node:Frame, interaction_node:Interaction) abs; } From 329f8d9fc1f13d6a839f16ef8885b3639ec42340 Mon Sep 17 00:00:00 2001 From: Gimel Dick Date: Tue, 4 Nov 2025 16:32:32 -0400 Subject: [PATCH 14/14] add frame filter and fix session id filter for get interaction logs; update template version; update jivas deps --- core/jivas/agent/analytics/get_interaction_logs.jac | 7 ++++++- core/setup.py | 6 +++--- jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/README.md | 0 .../templates/{2.1.22 => 2.1.23}/project/actions/README.md | 0 .../templates/{2.1.22 => 2.1.23}/project/daf/README.md | 0 .../jvcli/templates/{2.1.22 => 2.1.23}/project/env.example | 0 .../templates/{2.1.22 => 2.1.23}/project/gitignore.example | 0 .../jvcli/templates/{2.1.22 => 2.1.23}/project/globals.jac | 0 jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/main.jac | 0 .../templates/{2.1.22 => 2.1.23}/project/tests/README.md | 0 .../templates/{2.1.22 => 2.1.23}/sourcefiles/CHANGELOG.md | 0 .../templates/{2.1.22 => 2.1.23}/sourcefiles/README.md | 0 .../templates/{2.1.22 => 2.1.23}/sourcefiles/action_app.py | 0 .../{2.1.22 => 2.1.23}/sourcefiles/action_archetype.jac | 0 .../{2.1.22 => 2.1.23}/sourcefiles/action_info.yaml | 0 .../{2.1.22 => 2.1.23}/sourcefiles/action_lib.jac | 0 .../{2.1.22 => 2.1.23}/sourcefiles/agent_descriptor.yaml | 0 .../{2.1.22 => 2.1.23}/sourcefiles/agent_info.yaml | 0 .../{2.1.22 => 2.1.23}/sourcefiles/agent_knowledge.yaml | 0 .../{2.1.22 => 2.1.23}/sourcefiles/agent_memory.yaml | 0 .../sourcefiles/interact_action_archetype.jac | 0 21 files changed, 9 insertions(+), 4 deletions(-) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/README.md (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/actions/README.md (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/daf/README.md (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/env.example (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/gitignore.example (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/globals.jac (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/main.jac (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/project/tests/README.md (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/CHANGELOG.md (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/README.md (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/action_app.py (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/action_archetype.jac (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/action_info.yaml (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/action_lib.jac (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/agent_descriptor.yaml (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/agent_info.yaml (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/agent_knowledge.yaml (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/agent_memory.yaml (100%) rename jvcli/jvcli/templates/{2.1.22 => 2.1.23}/sourcefiles/interact_action_archetype.jac (100%) diff --git a/core/jivas/agent/analytics/get_interaction_logs.jac b/core/jivas/agent/analytics/get_interaction_logs.jac index b327134..750fa9d 100644 --- a/core/jivas/agent/analytics/get_interaction_logs.jac +++ b/core/jivas/agent/analytics/get_interaction_logs.jac @@ -13,6 +13,7 @@ walker get_interaction_logs(agent_graph_walker) { has start_date: str = ""; has end_date: str = ""; has session_id: str = ""; + has frame_id: str = ""; has channel: str = ""; has timezone: str = "UTC"; has page: int = 1; @@ -40,8 +41,12 @@ walker get_interaction_logs(agent_graph_walker) { } }; + if self.frame_id { + match_criteria["frame_id"] = self.frame_id; + } + if self.session_id { - match_criteria["session_id"] = self.session_id; + match_criteria["response.session_id"] = self.session_id; } if self.channel { diff --git a/core/setup.py b/core/setup.py index 5dded2d..f73376f 100644 --- a/core/setup.py +++ b/core/setup.py @@ -42,9 +42,9 @@ def get_version() -> str: package_data={"jivas": []}, python_requires=">=3.12.0", install_requires=[ - "jvcli<=2.1.20", - "jvclient<=2.1.20", - "jvserve<=2.1.20", + "jvcli", + "jvclient", + "jvserve", "jac-cloud==0.2.7", "jaclang==0.8.7", "pytz==2025.2", diff --git a/jvcli/jvcli/templates/2.1.22/project/README.md b/jvcli/jvcli/templates/2.1.23/project/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/README.md rename to jvcli/jvcli/templates/2.1.23/project/README.md diff --git a/jvcli/jvcli/templates/2.1.22/project/actions/README.md b/jvcli/jvcli/templates/2.1.23/project/actions/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/actions/README.md rename to jvcli/jvcli/templates/2.1.23/project/actions/README.md diff --git a/jvcli/jvcli/templates/2.1.22/project/daf/README.md b/jvcli/jvcli/templates/2.1.23/project/daf/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/daf/README.md rename to jvcli/jvcli/templates/2.1.23/project/daf/README.md diff --git a/jvcli/jvcli/templates/2.1.22/project/env.example b/jvcli/jvcli/templates/2.1.23/project/env.example similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/env.example rename to jvcli/jvcli/templates/2.1.23/project/env.example diff --git a/jvcli/jvcli/templates/2.1.22/project/gitignore.example b/jvcli/jvcli/templates/2.1.23/project/gitignore.example similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/gitignore.example rename to jvcli/jvcli/templates/2.1.23/project/gitignore.example diff --git a/jvcli/jvcli/templates/2.1.22/project/globals.jac b/jvcli/jvcli/templates/2.1.23/project/globals.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/globals.jac rename to jvcli/jvcli/templates/2.1.23/project/globals.jac diff --git a/jvcli/jvcli/templates/2.1.22/project/main.jac b/jvcli/jvcli/templates/2.1.23/project/main.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/main.jac rename to jvcli/jvcli/templates/2.1.23/project/main.jac diff --git a/jvcli/jvcli/templates/2.1.22/project/tests/README.md b/jvcli/jvcli/templates/2.1.23/project/tests/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/project/tests/README.md rename to jvcli/jvcli/templates/2.1.23/project/tests/README.md diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/CHANGELOG.md b/jvcli/jvcli/templates/2.1.23/sourcefiles/CHANGELOG.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/CHANGELOG.md rename to jvcli/jvcli/templates/2.1.23/sourcefiles/CHANGELOG.md diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/README.md b/jvcli/jvcli/templates/2.1.23/sourcefiles/README.md similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/README.md rename to jvcli/jvcli/templates/2.1.23/sourcefiles/README.md diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_app.py b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_app.py similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_app.py rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_app.py diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_archetype.jac b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_archetype.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_archetype.jac rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_archetype.jac diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_info.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_info.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_info.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_info.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/action_lib.jac b/jvcli/jvcli/templates/2.1.23/sourcefiles/action_lib.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/action_lib.jac rename to jvcli/jvcli/templates/2.1.23/sourcefiles/action_lib.jac diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_descriptor.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_descriptor.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_descriptor.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_descriptor.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_info.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_info.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_info.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_info.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_knowledge.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_knowledge.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_knowledge.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_knowledge.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/agent_memory.yaml b/jvcli/jvcli/templates/2.1.23/sourcefiles/agent_memory.yaml similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/agent_memory.yaml rename to jvcli/jvcli/templates/2.1.23/sourcefiles/agent_memory.yaml diff --git a/jvcli/jvcli/templates/2.1.22/sourcefiles/interact_action_archetype.jac b/jvcli/jvcli/templates/2.1.23/sourcefiles/interact_action_archetype.jac similarity index 100% rename from jvcli/jvcli/templates/2.1.22/sourcefiles/interact_action_archetype.jac rename to jvcli/jvcli/templates/2.1.23/sourcefiles/interact_action_archetype.jac