LLM Functions

logger = logging.getLogger('npcpy.llm_funcs') module-attribute

_build_jinx_schema(jinx_obj)

Build a self-describing schema string from a jinx's own metadata.

Only shows params the model must fill: the primary param (first with empty default) plus any param with a non-empty default. Optional params with empty defaults are skipped — _execute_jinx fills those from the jinx's own defaults so the model can't hallucinate values.

Source code in npcpy/llm_funcs.py
def _build_jinx_schema(jinx_obj):
    """Build a self-describing schema string from a jinx's own metadata.

    Only shows params the model must fill: the primary param (first with
    empty default) plus any param with a non-empty default.  Optional
    params with empty defaults are skipped — _execute_jinx fills those
    from the jinx's own defaults so the model can't hallucinate values.
    """
    desc = getattr(jinx_obj, 'description', '') or ''
    inp_list = getattr(jinx_obj, 'inputs', [])
    params = []
    has_primary = False
    for inp in inp_list:
        if isinstance(inp, str):
            params.append(f'"{inp}": "..."')
            has_primary = True
        elif isinstance(inp, dict):
            for k, v in inp.items():
                if isinstance(v, dict) and 'description' in v:
                    params.append(f'"{k}": "...({v["description"]})"')
                    has_primary = True
                elif v:
                    params.append(f'"{k}": "...(default: {v})"')
                else:
                    if not has_primary:
                        params.append(f'"{k}": "..."')
                        has_primary = True
    schema_str = '{' + ', '.join(params) + '}'
    return desc, schema_str

_execute_jinx(jinx, inputs, npc, team, messages, extra_globals)

Execute a jinx and return output.

Source code in npcpy/llm_funcs.py
def _execute_jinx(jinx, inputs, npc, team, messages, extra_globals):
    """Execute a jinx and return output."""
    try:
        jinja_env = None
        if npc and hasattr(npc, 'jinja_env'):
            jinja_env = npc.jinja_env
        elif team and hasattr(team, 'forenpc') and team.forenpc:
            jinja_env = getattr(team.forenpc, 'jinja_env', None)

        full_inputs = {}
        for inp in getattr(jinx, 'inputs', []):
            if isinstance(inp, dict):
                key = list(inp.keys())[0]
                default_val = inp[key]
                if isinstance(default_val, str) and default_val.startswith("~"):
                    default_val = os.path.expanduser(default_val)
                full_inputs[key] = default_val
            else:
                full_inputs[inp] = ""
        full_inputs.update(inputs or {})

        result = jinx.execute(
            input_values=full_inputs,
            npc=npc,
            messages=messages,
            extra_globals=extra_globals,
            jinja_env=jinja_env
        )
        if result is None:
            return "Executed with no output."
        if not isinstance(result, dict):
            return str(result)
        return result.get("output", str(result))
    except Exception as e:
        return f"Error: {e}"

_get_jinxes(npc, team)

Get available jinxes from npc (already filtered by jinxes_spec).

Source code in npcpy/llm_funcs.py
def _get_jinxes(npc, team):
    """Get available jinxes from npc (already filtered by jinxes_spec)."""
    jinxes = {}
    if npc and hasattr(npc, 'jinxes_dict'):
        jinxes.update(npc.jinxes_dict)
    return jinxes

_jinxes_to_tools(jinxes)

Convert jinxes to OpenAI-style tool definitions.

Source code in npcpy/llm_funcs.py
def _jinxes_to_tools(jinxes):
    """Convert jinxes to OpenAI-style tool definitions."""
    return [jinx.to_tool_def() for jinx in jinxes.values()]

abstract(groups, model, provider, npc=None, context=None, **kwargs)

Create more abstract terms from groups.

Source code in npcpy/llm_funcs.py
def abstract(groups, 
             model, 
             provider, 
             npc=None,
             context: str = None, 
             **kwargs):
    """
    Create more abstract terms from groups.
    """
    sample_groups = random.sample(groups, min(len(groups), max(3, len(groups) // 2)))

    groups_text_for_prompt = "\n".join([f'- "{g["name"]}"' for g in sample_groups])

    prompt = f"""
        Create more abstract categories from this list of groups.

        Groups:
        {groups_text_for_prompt}

        You will create higher-level concepts that interrelate between the given groups. 

        Create abstract categories that encompass multiple related facts, but do not unnecessarily combine facts with conjunctions. For example, do not try to combine "characters", "settings", and "physical reactions" into a
        compound group like "Characters, Setting, and Physical Reactions". This kind of grouping is not productive and only obfuscates true abstractions. 
        For example, a group that might encompass the three aforermentioned names might be "Literary Themes" or "Video Editing Functionis", depending on the context.
        Your aim is to abstract, not to just arbitrarily generate associations. 

        Group names should never be more than two words. They should not contain gerunds. They should never contain conjunctions like "AND" or "OR".
        Generate no more than 5 new concepts and no fewer than 2. 

        Respond with JSON:
        """ + '{"groups": [{"name": "abstract category name"}]}'

    response = get_llm_response(prompt,
                                model=model,
                                provider=provider,
                                format="json",
                                npc=npc,
                                context=context,
                                **kwargs)

    return response["response"].get("groups", [])

assign_groups_to_fact(fact, groups, model=None, provider=None, npc=None, context=None, **kwargs)

Assign facts to the identified groups

Source code in npcpy/llm_funcs.py
def assign_groups_to_fact(
    fact: str,
    groups: List[str],
    model = None,
    provider = None,
    npc = None, 
    context: str = None,
    **kwargs
) -> Dict[str, List[str]]:
    """Assign facts to the identified groups"""
    json_format = """Return a JSON object with the following structure:
        {
            "groups": ["list of group names"]
        }

    Do not include any additional markdown formatting or leading json characters."""

    prompt = f"""Given this fact, assign it to any relevant groups.

    A fact can belong to multiple groups if it fits.

    Here is the fact: {fact}

    Here are the groups: {groups}

    {json_format}
    """

    response = get_llm_response(
        prompt,
        model=model,
        provider=provider,
        format="json",
        npc=npc,
        context=context,
        **kwargs
    )
    return response["response"]

asymptotic_freedom(parent_concept, supporting_facts, model=None, provider=None, npc=None, context=None, **kwargs)

Given a concept and its facts, proposes an intermediate layer of sub-concepts.

Source code in npcpy/llm_funcs.py
def asymptotic_freedom(parent_concept, 
                       supporting_facts, 
                       model=None, 
                       provider=None, 
                       npc = None,
                       context: str = None, 
                       **kwargs):
    """Given a concept and its facts, proposes an intermediate layer of sub-concepts."""
    print(f"  Step Sleep-B: Attempting to deepen concept '{parent_concept['name']}'...")
    fact_statements = []
    for f in supporting_facts:
        fact_statements.append(f['statement'])

    prompt = f"""
    The concept "{parent_concept['name']}" is supported by many diverse facts.
    Propose a layer of 2-4 more specific sub-concepts to better organize these facts.
    These new concepts will exist as nodes that link to "{parent_concept['name']}".

    Supporting Facts: {json.dumps(fact_statements, indent=2)}
    Respond with JSON:
    """ + '{"new_sub_concepts": ["sub_layer1", "sub_layer2"]}'
    response = get_llm_response(prompt, 
                                model=model, 
                                provider=provider,
                                format="json", 
                                context=context, npc=npc,
                                **kwargs)
    return response['response'].get('new_sub_concepts', [])

bootstrap(prompt, model=None, provider=None, npc=None, team=None, sample_params=None, sync_strategy='consensus', context=None, n_samples=3, **kwargs)

Bootstrap by sampling multiple agents from team or varying parameters

Source code in npcpy/llm_funcs.py
def bootstrap(
    prompt: str,
    model: str = None,
    provider: str = None,
    npc: Any = None,
    team: Any = None,
    sample_params: Dict[str, Any] = None,
    sync_strategy: str = "consensus",
    context: str = None,
    n_samples: int = 3,
    **kwargs
) -> Dict[str, Any]:
    """Bootstrap by sampling multiple agents from team or varying parameters"""

    if team and hasattr(team, 'npcs') and len(team.npcs) >= n_samples:

        sampled_npcs = list(team.npcs.values())[:n_samples]
        results = []

        for i, agent in enumerate(sampled_npcs):
            response = get_llm_response(
                f"Sample {i+1}: {prompt}\nContext: {context}",
                npc=agent,
                context=context,
                **kwargs
            )
            results.append({
                'agent': agent.name,
                'response': response.get("response", "")
            })
    else:

        if sample_params is None:
            sample_params = {"temperature": [0.3, 0.7, 1.0]}

        results = []
        for i in range(n_samples):
            temp = sample_params.get('temperature', [0.7])[i % len(sample_params.get('temperature', [0.7]))]
            response = get_llm_response(
                f"Sample {i+1}: {prompt}\nContext: {context}",
                model=model,
                provider=provider,
                npc=npc,
                temperature=temp,
                context=context,
                **kwargs
            )
            results.append({
                'variation': f'temp_{temp}',
                'response': response.get("response", "")
            })


    response_texts = [r['response'] for r in results]
    return synthesize(response_texts, sync_strategy, model, provider, npc or (team.forenpc if team else None), context)

breathe(messages, model=None, provider=None, npc=None, context=None, **kwargs)

Condense the conversation context into a small set of key extractions.

Source code in npcpy/llm_funcs.py
def breathe(
    messages: List[Dict[str, str]],
    model: str = None,
    provider: str = None, 
    npc =  None,
    context: str = None,
    **kwargs: Any
) -> Dict[str, Any]:
    """Condense the conversation context into a small set of key extractions."""
    if not messages:
        return {"output": {}, "messages": []}

    if 'stream' in kwargs:
        kwargs['stream'] = False
    conversation_text = "\n".join([f"{m['role']}: {m['content']}" for m in messages])

    prompt = f'''
    Read the following conversation:

    {conversation_text}

    ''' +'''

    Now identify the following items:

    1. The high level objective
    2. The most recent task
    3. The accomplishments thus far
    4. The failures thus far

    Return a JSON like so:

    {
        "high_level_objective": "the overall goal so far for the user", 
        "most_recent_task": "The currently ongoing task", 
        "accomplishments": ["accomplishment1", "accomplishment2"], 
        "failures": ["falures1", "failures2"], 
    }

    '''


    result = get_llm_response(prompt, 
                           model=model, 
                           provider=provider, 
                           npc=npc, 
                           context=context, 
                           format='json', 
                           **kwargs)

    res = result.get('response', {})
    if isinstance(res, str):
        raise Exception
    format_output = f"""Here is a summary of the previous session. 
    The high level objective was: {res.get('high_level_objective')} \n The accomplishments were: {res.get('accomplishments')}, 
    the failures were: {res.get('failures')} and the most recent task was: {res.get('most_recent_task')}   """
    return {'output': format_output,
            'summary': res,
            'messages': [
                         {
                           'content': format_output,
                           'role': 'assistant'}
                           ]
                          }

check_llm_command(command, model=None, provider=None, api_url=None, api_key=None, npc=None, team=None, messages=None, images=None, stream=False, context=None, actions=None, extra_globals=None, max_iterations=5, jinxes=None, tool_capable=None, **kwargs)

Plan and execute: decide whether to answer directly or use jinxes, then do it.

When stream=True, returns a generator yielding (event_type, data) tuples. When stream=False, returns a dict.

Source code in npcpy/llm_funcs.py
def check_llm_command(
    command: str,
    model: str = None,
    provider: str = None,
    api_url: str = None,
    api_key: str = None,
    npc: Any = None,
    team: Any = None,
    messages: List[Dict[str, str]] = None,
    images: list = None,
    stream=False,
    context=None,
    actions: Dict[str, Dict] = None,
    extra_globals=None,
    max_iterations: int = 5,
    jinxes: Dict = None,
    tool_capable: bool = None,
    **kwargs,
):
    """Plan and execute: decide whether to answer directly or use jinxes, then do it.

    When stream=True, returns a generator yielding (event_type, data) tuples.
    When stream=False, returns a dict.
    """
    if messages is None:
        messages = []

    if jinxes is None:
        jinxes = _get_jinxes(npc, team)

    # No jinxes — just answer directly
    if not jinxes:
        print('no jinxes detected')

        response = get_llm_response(
            command,
            model=model,
            provider=provider,
            api_url=api_url,
            api_key=api_key,
            messages=messages[-10:],
            npc=npc,
            team=team,
            images=images,
            stream=False,
            context=context,
            **kwargs,
        )
        messages.append({"role": "user", "content": command})
        out = response.get("response", "")
        if out and isinstance(out, str):
            messages.append({"role": "assistant", "content": out})
        return {"messages": messages, "output": out, "usage": response.get("usage", {})}


    prompt = f"""


          A user submitted this request: {command}


          Determine the nature of the user's request:

          1. Should a jinx be invoked to fulfill the request? A jinx is a jinja-template execution script.

          2. Is it a general question that requires an informative answer or a highly specific question that
              requires information on the web?


              Use jinxes when it is obvious that the answer needs to be as up-to-date as possible. For example,
                  a question about where mount everest is does not necessarily need to be answered by a jinx call or an agent pass.

              If a user asks to explain the plot of the aeneid, this can be answered without a jinx call or agent pass.

              If a user were to ask for the current weather in tokyo or the current price of bitcoin or who the mayor of a city is,
                  then a jinx call is appropriate.

              If the user wants you to read a file, it must use a jinx to read the file.

              If the user asks you to edit a file, you must use a jinx to edit the file.

              If the user asks you to take a screenshot, you must to use a jinx to take the screenshot if available

              If a user asks you to search or to take a screenshot or to open a program or to write a program most likely it is
              appropriate to use a jinx. 


              remember, in your output, return only the action sequence. do not include and leading ```json or other markdown tags.

              """

    if messages:
        prompt += f"\nRecent conversation: {messages[-5:]}"

    response = get_llm_response(
        prompt,
        model=model,
        provider=provider,
        api_url=api_url,
        api_key=api_key,
        npc=npc,
        team=team,
        format="json",
        messages=[],
        context=context,
        **kwargs,
    )

    actions = response.get("response", {})
    #import pdb
    #pdb.set_trace()
    # Display plan
    print(actions)

    if not isinstance(actions,list) and isinstance(actions,dict): # the llm returned only one action
        actions = [actions]

    def _execute():
        step_outputs = []
        all_jinx_calls = []
        current_messages = messages.copy()
        last_jinx_output = None

        for i, action_data in enumerate(actions):
            render_markdown(f"- {action_data}")
            action_name = action_data.get("action", "")
            is_jinx = action_name == "invoke_jinx" or "jinx_name" in action_data
            jname = action_data.get("jinx_name", "")

            if stream and is_jinx and jname:
                yield ('tool_start', {'name': jname, 'args': action_data.get('inputs', {})})

            action_result = handle_action_choice(
                         command,
                         action_data,
                         jinxes,
                         model = model,
                         provider = provider,
                         api_url = api_url,
                         api_key = api_key,
                         npc = npc,
                         team = team,
                         messages = current_messages,
                         stream = stream,
                         extra_globals = extra_globals,
                         last_jinx_output = last_jinx_output,
                         step_outputs = step_outputs,
                         context = context,
                         **kwargs,
            )
            current_messages = action_result.get('messages', [])
            output = action_result.get('output', [])
            all_jinx_calls.extend(action_result.get('jinx_calls', []))
            if output == 'INVALID_ACTION':
                for evt in check_llm_command(
                    f"""In the previous attempt, the correct action name was not provided and a jinx could not be deciphered. only select from available jinxes.
                Original request: {command}""",
                    model=model, provider=provider, api_url=api_url, api_key=api_key,
                    npc=npc, team=team, messages=messages, stream=stream,
                    context=context, extra_globals=extra_globals, **kwargs,
                ):
                    yield evt
                return

            if stream:
                # Yield any sub-events from delegation before the tool_result
                for evt in action_result.get('stream_events', []):
                    yield evt
                if is_jinx and jname:
                    yield ('tool_result', {'name': jname, 'result': output, 'args': action_data.get('inputs', {})})
                else:
                    yield ('content', output)

            step_outputs.append(output)
            last_jinx_output = output

        final_output = step_outputs[0] if len(step_outputs) == 1 else "\n".join(str(o) for o in step_outputs)
        yield ('result', {
            "messages": current_messages,
            "output": final_output,
            "usage": response.get("usage", {}),
            "jinx_calls": all_jinx_calls,
        })

    if stream:
        return _execute()

    # Non-streaming: consume the generator and return the result dict
    result = None
    for event_type, data in _execute():
        if event_type == 'result':
            result = data
    return result or {"messages": messages, "output": "", "jinx_calls": []}

consolidate_facts_llm(new_fact, existing_facts, model, provider, npc=None, context=None, **kwargs)

Uses an LLM to decide if a new fact is novel or redundant.

Source code in npcpy/llm_funcs.py
def consolidate_facts_llm(new_fact, 
                          existing_facts, 
                          model, 
                          provider, 
                          npc=None,
                          context: str =None,
                          **kwargs):
    """
    Uses an LLM to decide if a new fact is novel or redundant.
    """
    prompt = f"""
        Analyze the "New Fact" in the context of the "Existing Facts" list.
        Your task is to determine if the new fact provides genuinely new information or if it is essentially a repeat or minor rephrasing of information already present.

        New Fact:
        "{new_fact['statement']}"

        Existing Facts:
        {json.dumps([f['statement'] for f in existing_facts], indent=2)}

        Possible decisions:
        - 'novel': The fact introduces new, distinct information not covered by the existing facts.
        - 'redundant': The fact repeats information already present in the existing facts.

        Respond with a JSON object:
        """ + '{"decision": "novel or redundant", "reason": "A brief explanation for your decision."}'
    response = get_llm_response(prompt,
                                model=model, 
                                provider=provider, 
                                format="json", 
                                npc=npc,
                                context=context,
                                **kwargs)
    return response['response']

criticize(prompt, model=None, provider=None, npc=None, team=None, context=None, **kwargs)

Provide critical analysis and constructive criticism

Source code in npcpy/llm_funcs.py
def criticize(
    prompt: str,
    model: str = None,
    provider: str = None,
    npc: Any = None,
    team: Any = None,
    context: str = None,
    **kwargs
) -> Dict[str, Any]:
    """Provide critical analysis and constructive criticism"""
    critique_prompt = f"""
    Provide a critical analysis and constructive criticism of the following:
    {prompt}

    Focus on identifying weaknesses, potential improvements, and alternative approaches.
    Be specific and provide actionable feedback.
    """

    return get_llm_response(
        critique_prompt,
        model=model,
        provider=provider,
        npc=npc,
        team=team,
        context=context,
        **kwargs
    )

execute_llm_command(command, model=None, provider=None, api_url=None, api_key=None, npc=None, messages=None, stream=False, context=None)

This function executes an LLM command. Args: command (str): The command to execute.

  • model (Optional[str]) –

    The model to use for executing the command.

  • provider (Optional[str]) –

    The provider to use for executing the command.

  • npc (Optional[Any]) –

    The NPC object.

  • messages (Optional[List[Dict[str, str]]) –

    The list of messages.

Returns: str: The result of the LLM command.

Source code in npcpy/llm_funcs.py
def execute_llm_command(
    command: str,
    model: Optional[str] = None,
    provider: Optional[str] = None,
    api_url: str = None,
    api_key: str = None,
    npc: Optional[Any] = None,
    messages: Optional[List[Dict[str, str]]] = None,
    stream=False,
    context=None,
) -> str:
    """This function executes an LLM command.
    Args:
        command (str): The command to execute.

    Keyword Args:
        model (Optional[str]): The model to use for executing the command.
        provider (Optional[str]): The provider to use for executing the command.
        npc (Optional[Any]): The NPC object.
        messages (Optional[List[Dict[str, str]]): The list of messages.
    Returns:
        str: The result of the LLM command.
    """
    if messages is None:
        messages = []
    max_attempts = 5
    attempt = 0
    subcommands = []


    context = ""
    while attempt < max_attempts:
        prompt = f"""
        A user submitted this query: {command}.
        You need to generate a bash command that will accomplish the user's intent.
        Respond ONLY with the bash command that should be executed. 
        Do not include markdown formatting
        """
        response = get_llm_response(
            prompt,
            model=model,
            provider=provider,
            api_url=api_url,
            api_key=api_key,
            messages=messages,
            npc=npc,
            context=context,
        )

        bash_command = response.get("response", {})

        print(f"LLM suggests the following bash command: {bash_command}")
        subcommands.append(bash_command)

        try:
            print(f"Running command: {bash_command}")
            result = subprocess.run(
                bash_command, shell=True, text=True, capture_output=True, check=True
            )
            print(f"Command executed with output: {result.stdout}")

            prompt = f"""
                Here was the output of the result for the {command} inquiry
                which ran this bash command {bash_command}:

                {result.stdout}

                Provide a simple response to the user that explains to them
                what you did and how it accomplishes what they asked for.
                """

            messages.append({"role": "user", "content": prompt})

            response = get_llm_response(
                prompt,
                model=model,
                provider=provider,
                api_url=api_url,
                api_key=api_key,
                npc=npc,
                messages=messages,
                context=context,
                stream =stream
            )

            return response
        except subprocess.CalledProcessError as e:
            print(f"Command failed with error:")
            print(e.stderr)

            error_prompt = f"""
            The command '{bash_command}' failed with the following error:
            {e.stderr}
            Please suggest a fix or an alternative command.
            Respond with a JSON object containing the key "bash_command" with the suggested command.
            Do not include any additional markdown formatting.

            """

            fix_suggestion = get_llm_response(
                error_prompt,
                model=model,
                provider=provider,
                npc=npc,
                api_url=api_url,
                api_key=api_key,
                format="json",
                messages=messages,
                context=context,
            )

            fix_suggestion_response = fix_suggestion.get("response", {})

            try:
                if isinstance(fix_suggestion_response, str):
                    fix_suggestion_response = json.loads(fix_suggestion_response)

                if (
                    isinstance(fix_suggestion_response, dict)
                    and "bash_command" in fix_suggestion_response
                ):
                    print(
                        f"LLM suggests fix: {fix_suggestion_response['bash_command']}"
                    )
                    command = fix_suggestion_response["bash_command"]
                else:
                    raise ValueError(
                        "Invalid response format from LLM for fix suggestion"
                    )
            except (json.JSONDecodeError, ValueError) as e:
                print(f"Error parsing LLM fix suggestion: {e}")

        attempt += 1

    return {
        "messages": messages,
        "output": "Max attempts reached. Unable to execute the command successfully.",
    }

Finds the best existing concept to link a new candidate concept to. This prompt now uses neutral "association" language.

Source code in npcpy/llm_funcs.py
def find_best_link_concept_llm(candidate_concept_name, 
                               existing_concept_names, 
                               model = None,
                               provider = None,
                               npc = None,
                               context: str = None,
                               **kwargs   ):
    """
    Finds the best existing concept to link a new candidate concept to.
    This prompt now uses neutral "association" language.
    """
    prompt = f"""
    Here is a new candidate concept: "{candidate_concept_name}"

    Which of the following existing concepts is it most closely related to? The relationship could be as a sub-category, a similar idea, or a related domain.

    Existing Concepts:
    {json.dumps(existing_concept_names, indent=2)}

    Respond with the single best-fit concept to link to from the list, or respond with "none" if it is a genuinely new root idea.
    """ + '{"best_link_concept": "The single best concept name OR none"}'
    response = get_llm_response(prompt, 
                                model=model, 
                                provider=provider, 
                                format="json", 
                                npc=npc,
                                context=context,
                                **kwargs)
    return response['response'].get('best_link_concept')

gen_image(prompt, model=None, provider=None, npc=None, height=1024, width=1024, n_images=1, input_images=None, save=False, filename='', api_key=None)

This function generates an image using the specified provider and model. Args: prompt (str): The prompt for generating the image. Keyword Args: model (str): The model to use for generating the image. provider (str): The provider to use for generating the image. filename (str): The filename to save the image to. npc (Any): The NPC object. api_key (str): The API key for the image generation service. Returns: List[PIL.Image.Image]: A list of generated PIL Image objects.

Source code in npcpy/llm_funcs.py
def gen_image(
    prompt: str,
    model: str = None,
    provider: str = None,
    npc: Any = None,
    height: int = 1024,
    width: int = 1024,
    n_images: int=1, 
    input_images: List[Union[str, bytes, PIL.Image.Image]] = None,
    save = False, 
    filename = '',
    api_key: str = None,
):
    """This function generates an image using the specified provider and model.
    Args:
        prompt (str): The prompt for generating the image.
    Keyword Args:
        model (str): The model to use for generating the image.
        provider (str): The provider to use for generating the image.
        filename (str): The filename to save the image to.
        npc (Any): The NPC object.
        api_key (str): The API key for the image generation service.
    Returns:
        List[PIL.Image.Image]: A list of generated PIL Image objects.
    """
    if model is not None and provider is not None:
        pass
    elif model is not None and provider is None:
        provider = lookup_provider(model)
    elif npc is not None:
        if npc.provider is not None:
            provider = npc.provider
        if npc.model is not None:
            model = npc.model
        if npc.api_url is not None:
            api_url = npc.api_url

    images = generate_image(
        prompt=prompt,
        model=model,
        provider=provider,
        height=height,
        width=width, 
        attachments=input_images,
        n_images=n_images, 
        api_key=api_key,

    )
    if save:
        if len(filename) == 0 :
            todays_date = datetime.now().strftime("%Y-%m-%d")
            filename = 'vixynt_gen'
        for i, image in enumerate(images):

            image.save(filename+'_'+str(i)+'.png')
    return images

gen_video(prompt, model=None, provider=None, npc=None, device='cpu', output_path='', num_inference_steps=10, num_frames=25, height=256, width=256, negative_prompt='', messages=None)

Function Description

This function generates a video using either Diffusers or Veo 3 via Gemini API.

Args: prompt (str): The prompt for generating the video. Keyword Args: model (str): The model to use for generating the video. provider (str): The provider to use for generating the video (gemini for Veo 3). device (str): The device to run the model on ('cpu' or 'cuda'). negative_prompt (str): What to avoid in the video (Veo 3 only). Returns: dict: Response with output path and messages.

Source code in npcpy/llm_funcs.py
def gen_video(
    prompt,
    model: str = None,
    provider: str = None,
    npc: Any = None,
    device: str = "cpu",
    output_path="",
    num_inference_steps=10,
    num_frames=25,
    height=256,
    width=256,
    negative_prompt="",
    messages: list = None,
):
    """
    Function Description:
        This function generates a video using either Diffusers or Veo 3 via Gemini API.
    Args:
        prompt (str): The prompt for generating the video.
    Keyword Args:
        model (str): The model to use for generating the video.
        provider (str): The provider to use for generating the video (gemini for Veo 3).
        device (str): The device to run the model on ('cpu' or 'cuda').
        negative_prompt (str): What to avoid in the video (Veo 3 only).
    Returns:
        dict: Response with output path and messages.
    """

    if provider == "gemini":

        try:
            output_path = generate_video_veo3(
                prompt=prompt,
                model=model,
                negative_prompt=negative_prompt,
                output_path=output_path,
            )
            return {
                "output": f"High-fidelity video with synchronized audio generated at {output_path}",
                "messages": messages
            }
        except Exception as e:
            print(f"Veo 3 generation failed: {e}")
            print("Falling back to diffusers...")
            provider = "diffusers"

    if provider == "diffusers" or provider is None:

        output_path = generate_video_diffusers(
            prompt,
            model,
            npc=npc,
            device=device,
            output_path=output_path,
            num_inference_steps=num_inference_steps,
            num_frames=num_frames,
            height=height,
            width=width,
        )
        return {
            "output": f"Video generated at {output_path}",
            "messages": messages
        }

    return {
        "output": f"Unsupported provider: {provider}",
        "messages": messages
    }

generate_group_candidates(items, item_type, model=None, provider=None, npc=None, context=None, n_passes=3, subset_size=10, **kwargs)

Generate candidate groups for items (facts or groups) based on core semantic meaning.

Source code in npcpy/llm_funcs.py
def generate_group_candidates(
    items: List[str],
    item_type: str,
    model: str = None,
    provider: str =None,
    npc = None,
    context: str = None,
    n_passes: int = 3,
    subset_size: int = 10, 
    **kwargs
) -> List[str]:
    """Generate candidate groups for items (facts or groups) based on core semantic meaning."""
    all_candidates = []

    for pass_num in range(n_passes):
        if len(items) > subset_size:
            item_subset = random.sample(items, min(subset_size, len(items)))
        else:
            item_subset = items


        prompt = f"""From the following {item_type}, identify specific and relevant conceptual groups.
        Think about the core subject or entity being discussed.

        GUIDELINES FOR GROUP NAMES:
        1.  **Prioritize Specificity:** Names should be precise and directly reflect the content.
        2.  **Favor Nouns and Noun Phrases:** Use descriptive nouns or noun phrases.
        3.  **AVOID:**
            *   Gerunds (words ending in -ing when used as nouns, like "Understanding", "Analyzing", "Processing"). If a gerund is unavoidable, try to make it a specific action (e.g., "User Authentication Module" is better than "Authenticating Users").
            *   Adverbs or descriptive adjectives that don't form a core part of the subject's identity (e.g., "Quickly calculating", "Effectively managing").
            *   Overly generic terms (e.g., "Concepts", "Processes", "Dynamics", "Mechanics", "Analysis", "Understanding", "Interactions", "Relationships", "Properties", "Structures", "Systems", "Frameworks", "Predictions", "Outcomes", "Effects", "Considerations", "Methods", "Techniques", "Data", "Theoretical", "Physical", "Spatial", "Temporal").
        4.  **Direct Naming:** If an item is a specific entity or action, it can be a group name itself (e.g., "Earth", "Lamb Shank Braising", "World War I").

        EXAMPLE:
        Input {item_type.capitalize()}: ["Self-intersection shocks drive accretion disk formation.", "Gravity stretches star into stream.", "Energy dissipation in shocks influences capture fraction."]
        Desired Output Groups: ["Accretion Disk Formation (Self-Intersection Shocks)", "Stellar Tidal Stretching", "Energy Dissipation from Shocks"]

        ---

        Now, analyze the following {item_type}:
        {item_type.capitalize()}: {json.dumps(item_subset)}

        Return a JSON object:
        """ + '{"groups": ["list of specific, precise, and relevant group names"]}'


        response = get_llm_response(
            prompt,
            model=model,
            provider=provider,
            format="json",
            npc=npc,
            context=context,
            **kwargs
        )

        candidates = response["response"].get("groups", [])
        all_candidates.extend(candidates)

    return list(set(all_candidates))

generate_groups(facts, model=None, provider=None, npc=None, context=None, **kwargs)

Generate conceptual groups for facts

Source code in npcpy/llm_funcs.py
def generate_groups(facts, 
                    model=None,
                    provider=None,
                    npc=None,
                    context: str =None, 
                    **kwargs):
    """Generate conceptual groups for facts"""

    facts_text = "\n".join([f"- {fact['statement']}" for fact in facts])

    prompt = f"""
    Generate conceptual groups for this group off facts:

    {facts_text}

    Create categories that encompass multiple related facts, but do not unnecessarily combine facts with conjunctions. 

    Your aim is to generalize commonly occurring ideas into groups, not to just arbitrarily generate associations. 
    Focus on the key commonly occurring items and expresions.     

    Group names should never be more than two words. They should not contain gerunds. They should never contain conjunctions like "AND" or "OR".
    Respond with JSON:
    """ + '{"groups": [{"name": "group name"}]}'

    response = get_llm_response(prompt,
                                model=model,
                                provider=provider,
                                format="json",
                                context=context,
                                npc=npc,
                                **kwargs)

    return response["response"].get("groups", [])

generate_image(prompt, model, provider, height=1024, width=1024, n_images=1, api_key=None, api_url=None, attachments=None, save_path=None, custom_model_path=None)

Unified function to generate or edit images using various providers.

Parameters:
  • prompt (str) –

    The prompt for generating/editing the image.

  • model (str) –

    The model to use.

  • provider (str) –

    The provider to use ('openai', 'diffusers', 'gemini', 'ollama').

  • height (int, default: 1024 ) –

    The height of the output image.

  • width (int, default: 1024 ) –

    The width of the output image.

  • n_images (int, default: 1 ) –

    Number of images to generate.

  • api_key (str, default: None ) –

    API key for the provider.

  • api_url (str, default: None ) –

    API URL for the provider.

  • attachments (list, default: None ) –

    List of images for editing. Can be file paths, bytes, or PIL Images.

  • save_path (str, default: None ) –

    Path to save the generated image.

  • custom_model_path (str, default: None ) –

    Path to a locally fine-tuned Diffusers model.

Returns:
  • List[PIL.Image.Image]: A list of generated PIL Image objects.

Source code in npcpy/gen/image_gen.py
def generate_image(
    prompt: str,
    model: str ,
    provider: str ,
    height: int = 1024,
    width: int = 1024,
    n_images: int = 1,
    api_key: Optional[str] = None,
    api_url: Optional[str] = None,
    attachments: Union[List[Union[str, bytes, Image.Image]], None] = None,
    save_path: Optional[str] = None,
    custom_model_path: Optional[str] = None,

):
    """
    Unified function to generate or edit images using various providers.

    Args:
        prompt (str): The prompt for generating/editing the image.
        model (str): The model to use.
        provider (str): The provider to use ('openai', 'diffusers', 'gemini', 'ollama').
        height (int): The height of the output image.
        width (int): The width of the output image.
        n_images (int): Number of images to generate.
        api_key (str): API key for the provider.
        api_url (str): API URL for the provider.
        attachments (list): List of images for editing. Can be file paths, bytes, or PIL Images.
        save_path (str): Path to save the generated image.
        custom_model_path (str): Path to a locally fine-tuned Diffusers model.

    Returns:
        List[PIL.Image.Image]: A list of generated PIL Image objects.
    """
    from urllib.request import urlopen
    import os

    if model is None and custom_model_path is None:
        if provider == "openai":
            model = "dall-e-2"
        elif provider == "diffusers":
            model = "runwayml/stable-diffusion-v1-5"
        elif provider == "gemini":
            model = "gemini-2.5-flash-image"
        elif provider == "ollama":
            model = "x/z-image-turbo"

    all_generated_pil_images = []

    if provider == "diffusers":
        if custom_model_path and os.path.isdir(custom_model_path):
            print(f"🌋 Using custom Diffusers model from path: {custom_model_path}")
            model_to_use = custom_model_path
        else:
            model_to_use = model
            print(f"🌋 Using standard Diffusers model: {model_to_use}")

        for _ in range(n_images):
            try:
                image = generate_image_diffusers(
                    prompt=prompt, 
                    model=model_to_use,
                    height=height, 
                    width=width
                )
                all_generated_pil_images.append(image)
            except MemoryError as e:
                raise e
            except Exception as e:
                raise e

    elif provider == "openai":
        images = openai_image_gen(
            prompt=prompt,
            model=model,
            attachments=attachments,
            height=height,
            width=width,
            n_images=n_images,
            api_key=api_key

        )
        all_generated_pil_images.extend(images)

    elif provider == "gemini":
        images = gemini_image_gen(
            prompt=prompt,
            model=model,
            attachments=attachments,
            n_images=n_images,
            api_key=api_key
        )
        all_generated_pil_images.extend(images)

    elif provider == "ollama":
        images = ollama_image_gen(
            prompt=prompt,
            model=model,
            height=height,
            width=width,
            n_images=n_images,
            api_url=api_url
        )
        all_generated_pil_images.extend(images)

    else:
        valid_sizes = ["256x256", "512x512", "1024x1024", "1024x1792", "1792x1024"]
        size = f"{width}x{height}"

        if attachments is not None:
            raise ValueError("Image editing not supported with litellm provider")

        litellm_model_string = f"{provider}/{model}" if provider and model else model

        image_response = image_generation(
            prompt=prompt,
            model=litellm_model_string,
            n=n_images,
            size=size,
            api_key=api_key,
            api_base=api_url,
        )
        for item_data in image_response.data:
            if hasattr(item_data, 'url') and item_data.url:
                with urlopen(item_data.url) as response:
                    all_generated_pil_images.append(Image.open(response))
            elif hasattr(item_data, 'b64_json') and item_data.b64_json:
                image_bytes = base64.b64decode(item_data.b64_json)
                all_generated_pil_images.append(Image.open(io.BytesIO(image_bytes)))
            else:
                print(f"Warning: litellm ImageResponse item has no URL or b64_json: {item_data}")

    if save_path:
        for i, img_item in enumerate(all_generated_pil_images):
            temp_save_path = f"{os.path.splitext(save_path)[0]}_{i}{os.path.splitext(save_path)[1]}"
            if isinstance(img_item, Image.Image):
                img_item.save(temp_save_path)
            else:
                print(f"Warning: Attempting to save non-PIL image item: {type(img_item)}. Skipping save for this item.")

    return all_generated_pil_images

generate_video_diffusers(prompt, model, npc=None, device='gpu', output_path='', num_inference_steps=5, num_frames=25, height=256, width=256)

Source code in npcpy/gen/video_gen.py
def generate_video_diffusers(
    prompt,
    model,
    npc=None,
    device="gpu",
    output_path="",
    num_inference_steps=5,
    num_frames=25,
    height=256,
    width=256,
):

    import torch
    from diffusers import DiffusionPipeline, DPMSolverMultistepScheduler
    import numpy as np
    import os 
    import cv2


    pipe = DiffusionPipeline.from_pretrained(
        "damo-vilab/text-to-video-ms-1.7b", torch_dtype=torch.float32
    ).to(device)
    pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)

    output = pipe(
        prompt,
        num_inference_steps=num_inference_steps,
        num_frames=num_frames,
        height=height,
        width=width,
    )

    def save_frames_to_video(frames, output_path, fps=8):
        """Handle the specific 5D array format (1, num_frames, H, W, 3) with proper type conversion"""

        if not (
            isinstance(frames, np.ndarray)
            and frames.ndim == 5
            and frames.shape[-1] == 3
        ):
            raise ValueError(
                f"Unexpected frame format. Expected 5D RGB array, got {frames.shape}"
            )


        frames = (frames[0] * 255).astype(np.uint8)  


        height, width = frames.shape[1:3]


        fourcc = cv2.VideoWriter_fourcc(*"mp4v")
        video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))

        if not video_writer.isOpened():
            raise IOError(f"Could not open video writer for {output_path}")


        for frame in frames:
            video_writer.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))

        video_writer.release()
        print(f"Successfully saved {frames.shape[0]} frames to {output_path}")

    from npcpy.npc_sysenv import get_videos_dir
    _vid_dir = get_videos_dir()
    os.makedirs(_vid_dir, exist_ok=True)
    if output_path == "":
        output_path = os.path.join(_vid_dir, prompt[0:8] + ".mp4")
    save_frames_to_video(output.frames, output_path)
    return output_path

generate_video_veo3(prompt, model, negative_prompt='', output_path='')

Generate video using Google's Veo 3 API with synchronized audio.

Source code in npcpy/gen/video_gen.py
def generate_video_veo3(
    prompt: str,
    model: str, 
    negative_prompt: str = "",
    output_path: str = "",
):
    """
    Generate video using Google's Veo 3 API with synchronized audio.
    """
    import time
    import os
    from google import genai
    from google.genai import types
    api_key =os.environ.get('GEMINI_API_KEY')

    client = genai.Client(        api_key=api_key)

    config = types.GenerateVideosConfig()
    if negative_prompt:
        config.negative_prompt = negative_prompt

    operation = client.models.generate_videos(
        model=model,
        prompt=prompt,
        config=config,
    )

    while not operation.done:
        time.sleep(20)
        operation = client.operations.get(operation)

    generated_video = operation.result.generated_videos[0]

    from npcpy.npc_sysenv import get_videos_dir
    videos_dir = get_videos_dir()
    os.makedirs(videos_dir, exist_ok=True)
    if output_path == "":
        safe_prompt = "".join(c for c in prompt[:20] if c.isalnum() or c in (' ', '-', '_')).rstrip()
        output_path = os.path.join(videos_dir, safe_prompt.replace(" ", "_") + "_veo3.mp4")

    client.files.download(file=generated_video.video)
    generated_video.video.save(output_path)

    return output_path

get_facts(content_text, model=None, provider=None, npc=None, context=None, attempt_number=1, n_attempts=3, **kwargs)

Extract facts from content text

Source code in npcpy/llm_funcs.py
def get_facts(content_text, 
              model= None,
              provider = None,
              npc=None,
              context : str=None, 
              attempt_number=1,
              n_attempts=3,

              **kwargs):
    """Extract facts from content text"""

    prompt = f"""
    Extract facts from this text. A fact is a specific statement that can be sourced from the text.

    Example: if text says "the moon is the earth's only currently known satellite", extract:
    - "The moon is a satellite of earth" 
    - "The moon is the only current satellite of earth"
    - "There may have been other satellites of earth" (inferred from "only currently known")

        A fact is a piece of information that makes a statement about the world.
        A fact is typically a sentence that is true or false.
        Facts may be simple or complex. They can also be conflicting with each other, usually
        because there is some hidden context that is not mentioned in the text.
        In any case, it is simply your job to extract a list of facts that could pertain to
        an individual's personality.

        For example, if a message says:
            "since I am a doctor I am often trying to think up new ways to help people.
            Can you help me set up a new kind of software to help with that?"
        You might extract the following facts:
            - The individual is a doctor
            - They are helpful

        Another example:
            "I am a software engineer who loves to play video games. I am also a huge fan of the
            Star Wars franchise and I am a member of the 501st Legion."
        You might extract the following facts:
            - The individual is a software engineer
            - The individual loves to play video games
            - The individual is a huge fan of the Star Wars franchise
            - The individual is a member of the 501st Legion

        Another example:
            "The quantum tunneling effect allows particles to pass through barriers
            that classical physics says they shouldn't be able to cross. This has
            huge implications for semiconductor design."
        You might extract these facts:
            - Quantum tunneling enables particles to pass through barriers that are
              impassable according to classical physics
            - The behavior of quantum tunneling has significant implications for
              how semiconductors must be designed

        Another example:
            "People used to think the Earth was flat. Now we know it's spherical,
            though technically it's an oblate spheroid due to its rotation."
        You might extract these facts:
            - People historically believed the Earth was flat
            - It is now known that the Earth is an oblate spheroid
            - The Earth's oblate spheroid shape is caused by its rotation

        Another example:
            "My research on black holes suggests they emit radiation, but my professor
            says this conflicts with Einstein's work. After reading more papers, I
            learned this is actually Hawking radiation and doesn't conflict at all."
        You might extract the following facts:
            - Black holes emit radiation
            - The professor believes this radiation conflicts with Einstein's work
            - The radiation from black holes is called Hawking radiation
            - Hawking radiation does not conflict with Einstein's work

        Another example:
            "During the pandemic, many developers switched to remote work. I found
            that I'm actually more productive at home, though my company initially
            thought productivity would drop. Now they're keeping remote work permanent."
        You might extract the following facts:
            - The pandemic caused many developers to switch to remote work
            - The individual discovered higher productivity when working from home
            - The company predicted productivity would decrease with remote work
            - The company decided to make remote work a permanent option

        Thus, it is your mission to reliably extract lists of facts.

    Here is the text:
    Text: "{content_text}"

    Facts should never be more than one or two sentences, and they should not be overly complex or literal. They must be explicitly
    derived or inferred from the source text. Do not simply repeat the source text verbatim when stating the fact. 

    No two facts should share substantially similar claims. They should be conceptually distinct and pertain to distinct ideas, avoiding lengthy convoluted or compound facts .
    Respond with JSON:
    """ + '{"facts": [{"statement": "fact statement that builds on input text to state a specific claim that can be falsified through reference to the source material", "source_text": "text snippets related to the source text", "type": "explicit or inferred"}]}'

    response = get_llm_response(prompt, 
                                model=model,
                                provider=provider, 
                                npc=npc,
                                format="json", 
                                context=context,
                                **kwargs)

    if len(response.get("response", {}).get("facts", [])) == 0 and attempt_number < n_attempts:
        print(f"  Attempt {attempt_number} to extract facts yielded no results. Retrying...")
        return get_facts(content_text, 
                         model=model, 
                         provider=provider, 
                         npc=npc,
                         context=context,
                         attempt_number=attempt_number+1,
                         n_attempts=n_attempts,
                         **kwargs)

    return response["response"].get("facts", [])

get_litellm_response(prompt=None, model=None, provider=None, images=None, tools=None, tool_choice=None, tool_map=None, think=None, format=None, messages=None, api_key=None, api_url=None, stream=False, attachments=None, auto_process_tool_calls=False, include_usage=False, **kwargs)

Source code in npcpy/gen/response.py
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
def get_litellm_response(
    prompt: str = None,
    model: str = None,
    provider: str = None,
    images: List[str] = None,
    tools: list = None,
    tool_choice: Dict = None,
    tool_map: Dict = None,
    think= None,
    format: Union[str, BaseModel] = None,
    messages: List[Dict[str, str]] = None,
    api_key: str = None,
    api_url: str = None,
    stream: bool = False,
    attachments: List[str] = None,
    auto_process_tool_calls: bool = False, 
    include_usage: bool = False,
    **kwargs,
) -> Dict[str, Any]:
    if not model:
        raise ValueError("No model specified. Please set a model in your NPC configuration or team settings.")
    result = {
        "response": None,
        "messages": messages.copy() if messages else [],
        "raw_response": None,
        "tool_calls": [],
        "tool_results":[],
    }
    if provider == "ollama":
        return get_ollama_response(
            prompt, 
            model, 
            images=images, 
            tools=tools, 
            tool_choice=tool_choice, 
            tool_map=tool_map,
            think=think,
            format=format, 
            messages=messages, 
            stream=stream, 
            attachments=attachments, 
            auto_process_tool_calls=auto_process_tool_calls, 
            **kwargs
        )
    elif provider == 'transformers':
        return get_transformers_response(
            prompt,
            model,
            images=images,
            tools=tools,
            tool_choice=tool_choice,
            tool_map=tool_map,
            think=think,
            format=format,
            messages=messages,
            stream=stream,
            attachments=attachments,
            auto_process_tool_calls=auto_process_tool_calls,
            **kwargs
        )
    elif provider == 'lora':
        print(f"🔧 LoRA provider detected, calling get_lora_response with model: {model}")
        result = get_lora_response(
            prompt=prompt,
            model=model,
            tools=tools,
            tool_map=tool_map,
            format=format,
            messages=messages,
            stream=stream,
            auto_process_tool_calls=auto_process_tool_calls,
            **kwargs
        )
        print(f"🔧 LoRA response: {result.get('response', 'NO RESPONSE')[:200] if result.get('response') else 'EMPTY'}")
        if result.get('error'):
            print(f"🔧 LoRA error: {result.get('error')}")
        return result
    elif provider == 'llamacpp':
        return get_llamacpp_response(
            prompt,
            model,
            images=images,
            tools=tools,
            tool_choice=tool_choice,
            tool_map=tool_map,
            think=think,
            format=format,
            messages=messages,
            stream=stream,
            attachments=attachments,
            auto_process_tool_calls=auto_process_tool_calls,
            **kwargs
        )
    elif provider == 'airllm':
        return get_airllm_response(
            prompt=prompt,
            model=model,
            tools=tools,
            tool_map=tool_map,
            format=format,
            messages=messages,
            auto_process_tool_calls=auto_process_tool_calls,
            **kwargs
        )
    elif provider == 'lmstudio' or (model and '.lmstudio' in str(model)):
        api_url = api_url or "http://127.0.0.1:1234/v1"
        provider = "openai"
        api_key = api_key or "lm-studio"
        if 'timeout' not in kwargs:
            kwargs['timeout'] = 300
    elif provider == 'llamacpp-server':
        api_url = api_url or "http://127.0.0.1:8080/v1"
        provider = "openai"
        api_key = api_key or "llamacpp"
        if 'timeout' not in kwargs:
            kwargs['timeout'] = 300
    elif provider == 'omlx':
        api_url = api_url or "http://127.0.0.1:8000/v1"
        provider = "openai"
        api_key = api_key or "omlx"
        if 'timeout' not in kwargs:
            kwargs['timeout'] = 300

    if attachments:
        for attachment in attachments:
            if os.path.exists(attachment):
                _, ext = os.path.splitext(attachment)
                ext = ext.lower()

                if ext in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
                    if not images:
                        images = []
                    images.append(attachment)
                elif ext == '.pdf':
                    try:
                        from npcpy.data.load import load_pdf
                        pdf_data = load_pdf(attachment)
                        if pdf_data is not None:
                            if prompt:
                                prompt += f"\n\nContent from PDF: {os.path.basename(attachment)}\n{pdf_data}..."
                            else:
                                prompt = f"Content from PDF: {os.path.basename(attachment)}\n{pdf_data}..."

                    except Exception:
                        pass
                elif ext == '.csv':
                    try:
                        from npcpy.data.load import load_csv
                        csv_data = load_csv(attachment)
                        if csv_data is not None:
                            csv_sample = csv_data.head(10).to_string()
                            if prompt:
                                prompt += f"\n\nContent from CSV: {os.path.basename(attachment)} (first 10 rows):\n{csv_sample}"
                            else:
                                prompt = f"Content from CSV: {os.path.basename(attachment)} (first 10 rows):\n{csv_sample}"
                    except Exception:
                        pass
                else:
                    text_extensions = {'.txt', '.text', '.log', '.md', '.markdown', '.rst', '.json', '.yaml', '.yml', '.toml', '.ini', '.conf', '.cfg', '.xml', '.html', '.htm', '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.h', '.cpp', '.hpp', '.go', '.rs', '.rb', '.php', '.sh', '.bash', '.sql', '.css', '.scss'}
                    filename = os.path.basename(attachment)
                    if ext in text_extensions or ext == '':
                        try:
                            with open(attachment, 'r', encoding='utf-8', errors='replace') as f:
                                text_content = f.read()
                            max_chars = 50000
                            if len(text_content) > max_chars:
                                text_content = text_content[:max_chars] + f"\n\n... [truncated]"
                            if text_content.strip():
                                if prompt:
                                    prompt += f"\n\nContent from {filename}:\n```\n{text_content}\n```"
                                else:
                                    prompt = f"Content from {filename}:\n```\n{text_content}\n```"
                        except Exception:
                            pass

    if prompt:
        if result['messages'] and result['messages'][-1]["role"] == "user":
            if isinstance(messages[-1]["content"], str):
                result['messages'][-1]["content"] = prompt
            elif isinstance(result['messages'][-1]["content"], list):
                for i, item in enumerate(result['messages'][-1]["content"]):
                    if item.get("type") == "text":
                        result['messages'][-1]["content"][i]["text"] = prompt
                        break
                else:
                    result['messages'][-1]["content"].append({"type": "text", "text": prompt})
        else:
            result['messages'].append({"role": "user", "content": prompt})

    if format == "json" and not stream:
        json_instruction = """If you are a returning a json object, begin directly with the opening {.
            If you are returning a json array, begin directly with the opening [.
            Do not include any additional markdown formatting or leading
            ```json tags in your response. The item keys should be based on the ones provided
            by the user. Do not invent new ones."""

        if result["messages"] and result["messages"][-1]["role"] == "user":
            if isinstance(result["messages"][-1]["content"], list):
                result["messages"][-1]["content"].append({"type": "text", "text": json_instruction})
            elif isinstance(result["messages"][-1]["content"], str):
                result["messages"][-1]["content"] += "\n" + json_instruction

    if format == "yaml" and not stream:
        yaml_instruction = """Return your response as valid YAML. Do not include ```yaml markdown tags.
            For multi-line strings like code, use the literal block scalar (|) syntax:
            code: |
              your code here
              more lines here
            The keys should be based on the ones requested by the user. Do not invent new ones."""

        if result["messages"] and result["messages"][-1]["role"] == "user":
            if isinstance(result["messages"][-1]["content"], list):
                result["messages"][-1]["content"].append({"type": "text", "text": yaml_instruction})
            elif isinstance(result["messages"][-1]["content"], str):
                result["messages"][-1]["content"] += "\n" + yaml_instruction

    if images:
        last_user_idx = -1
        for i, msg in enumerate(result["messages"]):
            if msg["role"] == "user":
                last_user_idx = i
        if last_user_idx == -1:
            result["messages"].append({"role": "user", "content": []})
            last_user_idx = len(result["messages"]) - 1
        if isinstance(result["messages"][last_user_idx]["content"], str):

            result["messages"][last_user_idx]["content"] = [{"type": "text", 
                                                             "text": result["messages"][last_user_idx]["content"]
                                                             }]

        elif not isinstance(result["messages"][last_user_idx]["content"], list):
            result["messages"][last_user_idx]["content"] = []
        for image_path in images:
            with open(image_path, "rb") as image_file:
                image_data = base64.b64encode(compress_image(image_file.read())).decode("utf-8")
                result["messages"][last_user_idx]["content"].append(
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}}
                )



    result["messages"] = sanitize_messages(result["messages"])
    api_params = {"messages": result["messages"]}

    if include_usage:
      litellm.include_cost_in_streaming_usage = True
      api_params['stream_options'] = {"include_usage": True}

    if api_url is not None and ('openai-like' in provider or provider == "openai-like" or provider == "openai"):
        api_params["api_base"] = api_url
        provider = "openai"


    if provider =='enpisi' and api_url is None:
        api_params['api_base'] = 'https://api.enpisi.com'
        if api_key is None:
            api_key = os.environ.get('NPC_STUDIO_LICENSE_KEY')
            api_params['api_key'] = api_key
        if '-npc' in model: 
            model = model.split('-npc')[0]
        provider = "openai"

    if isinstance(format, type) and issubclass(format, BaseModel):
        api_params["response_format"] = format
    if model is None:
        raise ValueError("No model specified. Please set a model in your NPC configuration or team settings.")
    if provider is None:
        raise ValueError("No provider specified. Please set a provider in your NPC configuration or team settings.")

    if "api_base" in api_params and provider == "openai":
        api_params["model"] = f"openai/{model}"
    elif "/" not in model or model.startswith("/"):
        api_params["model"] = f"{provider}/{model}"
    else:
        api_params["model"] = model
    if api_key is not None: 
        api_params["api_key"] = api_key
    if tools: 
        api_params["tools"] = tools
    if tool_choice: 
        api_params["tool_choice"] = tool_choice

    if kwargs:
        for key, value in kwargs.items():
            if key in [
                "stop", "temperature", "top_p", "max_tokens", "max_completion_tokens",
                 "extra_headers", "parallel_tool_calls",
                "response_format", "user", "timeout", "think", "thinking", "reasoning_effort",
            ]:
                # Handle temperature/top_p conflict for Claude models
                if key == "temperature" and "claude" in str(api_params.get("model", "")).lower():
                    api_params[key] = value
                elif key == "top_p" and "claude" in str(api_params.get("model", "")).lower():
                    # Only add top_p for Claude if temperature is not provided
                    if "temperature" not in kwargs:
                        api_params[key] = value
                else:
                    api_params[key] = value

    if not auto_process_tool_calls or not (tools and tool_map):
        api_params["stream"] = stream
        resp = completion(**api_params)
        result["raw_response"] = resp

        if hasattr(resp, 'usage') and resp.usage:
            result["usage"] = {
                "input_tokens": getattr(resp.usage, 'prompt_tokens', 0) or 0,
                "output_tokens": getattr(resp.usage, 'completion_tokens', 0) or 0,
            }
        elif hasattr(resp, 'prompt_eval_count'):
            result["usage"] = {
                "input_tokens": getattr(resp, 'prompt_eval_count', 0) or 0,
                "output_tokens": getattr(resp, 'eval_count', 0) or 0,
            }

        if stream:
            result["response"] = resp
            return result
        else:

            llm_response = resp.choices[0].message.content
            result["response"] = llm_response
            assistant_msg = {"role": "assistant", "content": llm_response}
            if hasattr(resp.choices[0].message, 'tool_calls') and resp.choices[0].message.tool_calls:
                raw_tcs = resp.choices[0].message.tool_calls
                result["tool_calls"] = raw_tcs
                tc_dicts = []
                for tc in raw_tcs:
                    if isinstance(tc, dict):
                        tc_dicts.append(tc)
                    else:
                        tc_dicts.append({
                            "id": getattr(tc, "id", str(uuid.uuid4())),
                            "type": "function",
                            "function": {
                                "name": getattr(tc.function, "name", "") if hasattr(tc, "function") else "",
                                "arguments": getattr(tc.function, "arguments", "{}") if hasattr(tc, "function") else "{}"
                            }
                        })
                assistant_msg["tool_calls"] = tc_dicts
            result["messages"].append(assistant_msg)
            if format == "json":
                try:
                    if isinstance(llm_response, str):
                        llm_response = llm_response.strip()

                        if '```json' in llm_response:
                            start = llm_response.find('```json') + 7
                            end = llm_response.rfind('```')
                            if end > start:
                                llm_response = llm_response[start:end].strip()

                        first_brace = llm_response.find('{')
                        first_bracket = llm_response.find('[')

                        if first_brace == -1 and first_bracket == -1:
                            result["response"] = {}
                            result["error"] = "No JSON found in response"
                            return result

                        if first_brace != -1 and (first_bracket == -1 or first_brace < first_bracket):
                            llm_response = llm_response[first_brace:]
                            last_brace = llm_response.rfind('}')
                            if last_brace != -1:
                                llm_response = llm_response[:last_brace+1]
                        else:
                            llm_response = llm_response[first_bracket:]
                            last_bracket = llm_response.rfind(']')
                            if last_bracket != -1:
                                llm_response = llm_response[:last_bracket+1]

                        parsed_json = json.loads(llm_response, strict=False)

                        if "json" in parsed_json:
                            result["response"] = parsed_json["json"]
                        else:
                            result["response"] = parsed_json

                except (json.JSONDecodeError, TypeError) as e:
                    logger.debug(f"JSON parsing error: {str(e)}, raw response: {llm_response[:500]}")
                    result["response"] = {}
                    result["error"] = "Invalid JSON response"

            if format == "yaml":
                try:
                    if isinstance(llm_response, str):
                        llm_response = llm_response.strip()

                        if '```yaml' in llm_response:
                            start = llm_response.find('```yaml') + 7
                            end = llm_response.rfind('```')
                            if end > start:
                                llm_response = llm_response[start:end].strip()
                        elif '```' in llm_response:
                            start = llm_response.find('```') + 3
                            newline = llm_response.find('\n', start)
                            if newline != -1:
                                start = newline + 1
                            end = llm_response.rfind('```')
                            if end > start:
                                llm_response = llm_response[start:end].strip()

                        parsed_yaml = yaml.safe_load(llm_response)
                        result["response"] = parsed_yaml

                except (yaml.YAMLError, TypeError) as e:
                    logger.debug(f"YAML parsing error: {str(e)}, raw response: {llm_response[:500]}")
                    result["response"] = {}
                    result["error"] = "Invalid YAML response"

            return result



    initial_api_params = api_params.copy()
    initial_api_params["stream"] = False

    try:
        resp = completion(**initial_api_params)
    except Exception as e:
        logger.error(f"litellm completion() failed: {type(e).__name__}: {e}")
        result["error"] = str(e)
        result["response"] = f"LLM call failed: {e}"
        return result

    result["raw_response"] = resp

    if hasattr(resp, 'usage') and resp.usage:
        result["usage"] = {
            "input_tokens": getattr(resp.usage, 'prompt_tokens', 0) or 0,
            "output_tokens": getattr(resp.usage, 'completion_tokens', 0) or 0,
        }

    if not resp.choices:
        result["response"] = "No response from model"
        return result

    has_tool_calls = hasattr(resp.choices[0].message, 'tool_calls') and resp.choices[0].message.tool_calls

    if has_tool_calls:
        result["tool_calls"] = resp.choices[0].message.tool_calls

        processed_result = process_tool_calls(result,
                                              tool_map,
                                              model,
                                              provider,
                                              result["messages"],
                                              stream=False,
                                              tools=tools)

        clean_messages = []
        tool_results_summary = []

        for msg in processed_result["messages"]:
            role = msg.get('role', '')
            if role == 'assistant' and 'tool_calls' in msg:
                continue
            elif role == 'tool':
                content = msg.get('content', '')
                if len(content) > 2000:
                    content = content[:2000] + "... (truncated)"
                tool_results_summary.append(content)
            else:
                clean_messages.append(msg)

        if tool_results_summary:
            clean_messages.append({
                "role": "assistant",
                "content": "I executed the requested tools. Here are the results:\n\n" + "\n\n".join(tool_results_summary)
            })

        clean_messages.append({
            "role": "user",
            "content": "Based on the tool results above, provide a brief summary of what happened. Do NOT output any code - the tool has already executed. Just describe the results concisely."
        })

        final_api_params = api_params.copy()
        final_api_params["messages"] = clean_messages
        final_api_params["stream"] = stream
        if "tools" in final_api_params:
            del final_api_params["tools"]
        if "tool_choice" in final_api_params:
            del final_api_params["tool_choice"]

        final_resp = completion(**final_api_params)

        if stream:
            processed_result["response"] = final_resp
        else:
            if final_resp.choices:
                final_content = final_resp.choices[0].message.content
                processed_result["response"] = final_content
                processed_result["messages"].append({"role": "assistant", "content": final_content})
            else:
                if tool_results_summary:
                    fallback_content = "\n\n".join(tool_results_summary)
                else:
                    fallback_content = "Tool executed successfully."
                processed_result["response"] = fallback_content
                processed_result["messages"].append({"role": "assistant", "content": fallback_content})

        return processed_result


    else:
        llm_response = resp.choices[0].message.content
        result["messages"].append({"role": "assistant", "content": llm_response})

        if stream:
            def string_chunk_generator():
                chunk_size = 1
                for i, char in enumerate(llm_response):
                    yield type('MockChunk', (), {
                        'id': f'mock-chunk-{i}',
                        'object': 'chat.completion.chunk',
                        'created': int(time.time()),
                        'model': model or 'unknown',
                        'choices': [type('Choice', (), {
                            'index': 0,
                            'delta': type('Delta', (), {
                                'content': char,
                                'role': 'assistant' if i == 0 else None
                            })(),
                            'finish_reason': 'stop' if i == len(llm_response) - 1 else None
                        })()]
                    })()

            result["response"] = string_chunk_generator()
        else:
            result["response"] = llm_response
    return result            

get_llm_response(prompt, model=None, provider=None, images=None, npc=None, team=None, messages=None, api_url=None, api_key=None, context=None, stream=False, attachments=None, include_usage=False, n_samples=1, matrix=None, **kwargs)

Generate a response using the specified provider and model.

Source code in npcpy/llm_funcs.py
def get_llm_response(
    prompt: str,
    model: str = None,
    provider: str = None,
    images: List[str] = None,
    npc: Any = None,
    team: Any = None,
    messages: List[Dict[str, str]] = None,
    api_url: str = None,
    api_key: str = None,
    context=None,
    stream: bool = False,
    attachments: List[str] = None,
    include_usage: bool = False,
    n_samples: int = 1,
    matrix: Optional[Dict[str, List[Any]]] = None,
    **kwargs,
):
    """Generate a response using the specified provider and model."""
    logger.debug(
        f"[get_llm_response] {len(messages) if messages else 0} messages, "
        f"prompt_len={len(prompt) if prompt else 0}, "
        f"context={'yes' if context else 'no'}"
    )

    base_model, base_provider, base_api_url, base_api_key = resolve_model_provider(
        npc=npc, team=team, model=model, provider=provider,
        api_url=api_url, api_key=api_key, images=images, attachments=attachments,
    )
    if api_key is None:
        api_key = base_api_key

    use_matrix = matrix is not None and len(matrix) > 0
    multi_sample = n_samples and n_samples > 1

    if not use_matrix and not multi_sample:
        # Simple single-call path
        run_model, run_provider, run_api_url, run_api_key = resolve_model_provider(
            npc=npc, team=team, model=base_model, provider=base_provider,
            api_url=api_url, api_key=api_key, images=images, attachments=attachments,
        )
        tool_capable = bool(kwargs.get("tools"))
        system_message = get_system_message(npc, team, tool_capable=tool_capable) if npc is not None else "You are a helpful assistant."

        # Build messages
        run_messages = copy.deepcopy(messages) if messages else []
        if not run_messages:
            run_messages = [{"role": "system", "content": system_message}]

        # Build full text from prompt and/or context
        full_text = ""
        if prompt and context:
            full_text = f"{prompt}\n\n\nUser Provided Context: {context}"
        elif prompt:
            full_text = prompt
        elif context:
            full_text = f"User Provided Context: {context}"

        if full_text:
            if run_messages[-1]["role"] == "user" and isinstance(run_messages[-1]["content"], str):
                run_messages[-1]["content"] += "\n" + full_text
            else:
                run_messages.append({"role": "user", "content": full_text})

        if not run_model:
            raise ValueError("No model specified. Please set a model in your NPC configuration or team settings.")
        return get_litellm_response(
            full_text or None,
            messages=run_messages,
            model=run_model,
            provider=run_provider,
            api_url=run_api_url,
            api_key=api_key,
            images=images,
            attachments=attachments,
            stream=stream,
            include_usage=include_usage,
            **kwargs,
        )

    # Matrix / multi-sample path
    combos = []
    if use_matrix:
        keys = list(matrix.keys())
        values = []
        for key in keys:
            val = matrix[key]
            if isinstance(val, (list, tuple, set)):
                values.append(list(val))
            else:
                values.append([val])
        for combo_values in itertools.product(*values):
            combos.append(dict(zip(keys, combo_values)))
    else:
        combos.append({})

    runs = []
    for combo in combos:
        run_npc = combo.get("npc", npc)
        run_team = combo.get("team", team)
        run_context = combo.get("context", context)
        extra_kwargs = dict(kwargs)
        for k, v in combo.items():
            if k not in {"model", "provider", "npc", "context", "team"}:
                extra_kwargs[k] = v

        run_model, run_provider, run_api_url, run_api_key = resolve_model_provider(
            npc=run_npc, team=run_team,
            model=combo.get("model", base_model),
            provider=combo.get("provider", base_provider),
            api_url=api_url, api_key=api_key, images=images, attachments=attachments,
        )

        tool_capable = bool(extra_kwargs.get("tools"))
        system_message = get_system_message(run_npc, run_team, tool_capable=tool_capable) if run_npc is not None else "You are a helpful assistant."

        run_messages = copy.deepcopy(messages) if messages else []
        if not run_messages:
            run_messages = [{"role": "system", "content": system_message}]

        full_text = ""
        if prompt and run_context:
            full_text = f"{prompt}\n\n\nUser Provided Context: {run_context}"
        elif prompt:
            full_text = prompt
        elif run_context:
            full_text = f"User Provided Context: {run_context}"

        if full_text:
            if run_messages[-1]["role"] == "user" and isinstance(run_messages[-1]["content"], str):
                run_messages[-1]["content"] += "\n" + full_text
            else:
                run_messages.append({"role": "user", "content": full_text})

        for sample_idx in range(max(1, n_samples or 1)):
            resp = get_litellm_response(
                full_text or None,
                messages=run_messages,
                model=run_model,
                provider=run_provider,
                api_url=run_api_url,
                api_key=api_key,
                images=images,
                attachments=attachments,
                stream=stream,
                include_usage=include_usage,
                **extra_kwargs,
            )
            runs.append({
                "response": resp.get("response") if isinstance(resp, dict) else resp,
                "raw": resp,
                "combo": combo,
                "sample_index": sample_idx,
            })

    aggregated = {
        "response": runs[0]["response"] if runs else None,
        "runs": runs,
    }
    if runs and isinstance(runs[0]["raw"], dict) and "messages" in runs[0]["raw"]:
        aggregated["messages"] = runs[0]["raw"].get("messages")
    return aggregated

Links any node (fact or concept) to ALL relevant concepts in the entire ontology.

Source code in npcpy/llm_funcs.py
def get_related_concepts_multi(node_name: str, 
                               node_type: str, 
                               all_concept_names, 
                               model: str = None,
                               provider: str = None,
                               npc=None,
                               context : str = None, 
                               **kwargs):
    """Links any node (fact or concept) to ALL relevant concepts in the entire ontology."""
    json_format = 'Respond with JSON: {"related_concepts": ["Concept A", "Concept B", ...]}'
    prompt = f"""
    Which of the following concepts from the entire ontology relate to the given {node_type}?
    Select all that apply, from the most specific to the most abstract.

    {node_type.capitalize()}: "{node_name}"

    Available Concepts:
    {json.dumps(all_concept_names, indent=2)}

    {json_format}
    """
    response = get_llm_response(prompt, 
                                model=model, 
                                provider=provider, 
                                format="json", 
                                npc=npc,
                                context=context, 
                                **kwargs)
    return response["response"].get("related_concepts", [])

Identifies which existing facts are causally or thematically related to a new fact.

Source code in npcpy/llm_funcs.py
def get_related_facts_llm(new_fact_statement, 
                          existing_fact_statements, 
                          model = None, 
                          provider = None,
                          npc = None, 
                          attempt_number = 1,
                          n_attempts = 3,
                          context='', 
                          **kwargs):
    """Identifies which existing facts are causally or thematically related to a new fact."""
    prompt = f"""
    A new fact has been learned: "{new_fact_statement}"

    Which of the following existing facts are directly related to it (causally, sequentially, or thematically)?
    Select only the most direct and meaningful connections.

    Existing Facts:
    {json.dumps(existing_fact_statements, indent=2)}

    Respond with JSON:
    """ + '{"related_facts": ["statement of a related fact", ...]}'
    response = get_llm_response(prompt,
                                model=model, 
                                provider=provider, 
                                format="json", 
                                npc=npc,
                                context=context,
                                **kwargs)   
    if attempt_number <= n_attempts:
        if not response["response"].get("related_facts", []):
            print(f"  Attempt {attempt_number} to find related facts yielded no results. Retrying...")
            return get_related_facts_llm(new_fact_statement, 
                                           existing_fact_statements, 
                                           model=model, 
                                           provider=provider, 
                                           npc=npc,
                                           attempt_number=attempt_number+1,
                                           n_attempts=n_attempts,
                                           context=context,
                                           **kwargs)    

    return response["response"].get("related_facts", [])

get_system_message(npc, team=None, tool_capable=False)

Source code in npcpy/npc_sysenv.py
def get_system_message(npc, team=None, tool_capable=False) -> str:

    if npc is None:
        return "You are a helpful assistant"
    if npc.plain_system_message:
        return npc.primary_directive

    system_message = f"""
.
..
...
....
.....
......
.......
........
.........
..........
Hello!
Welcome to the team.
You are the {npc.name} NPC with the following primary directive: {npc.primary_directive}.
Users may refer to you by your assistant name, {npc.name} and you should
consider this to be your core identity.
The current working directory is {os.getcwd()}.
The current date and time are : {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
"""

    if hasattr(npc, 'kg_data') and npc.kg_data:
        memory_context = npc.get_memory_context()
        if memory_context:
            system_message += f"\n\nMemory Context:\n{memory_context}\n"

    if npc.db_conn is not None:
        db_path = None
        if hasattr(npc.db_conn, "url") and npc.db_conn.url:
            db_path = npc.db_conn.url.database
        elif hasattr(npc.db_conn, "database"):
            db_path = npc.db_conn.database
        system_message += """What follows is information about the database connection. If you are asked to execute queries with tools, use this information. 
        If you are asked for help with debugging queries, use this information. 
        Do not unnecessarily reference that you possess this information unless it is
        specifically relevant to the request.

        DB Connection Information:        
        """
        if db_path:
            system_message += f"\nDatabase path: {db_path}\n"
        if npc.tables is not None:
            system_message += f"\nDatabase tables: {npc.tables}\n"

    if team is not None and npc.name == getattr(team, 'forenpc_name', 'sibiji'):
        team_context = team.context if hasattr(team, "context") and team.context else ""
        team_preferences = team.shared_context.get('preferences', '') if hasattr(team, "shared_context") else ""
        system_message += f"\nTeam context: {team_context}\n"
        if team_preferences:
            system_message += f"Team preferences: {team_preferences}\n"

        if hasattr(team, 'npcs') and team.npcs:
            members = []
            for name, member in team.npcs.items():
                if name != npc.name:
                    directive = getattr(member, 'primary_directive', '')
                    desc = directive[:50].strip() if directive else ''
                    members.append(f"  - @{name}: {desc}")
            if members:
                system_message += "\nTeam members available for delegation:\n" + "\n".join(members) + "\n"

    if hasattr(npc, 'jinxes_dict') and npc.jinxes_dict:
        tool_lines = []
        for jname, jinx in npc.jinxes_dict.items():
            desc = getattr(jinx, 'description', '') or ''
            tool_lines.append(f"  - {jname}: {desc.strip()}")
        if tool_lines:
            system_message += "\nYou have access to the following jinxes:\n"
            system_message += "\n".join(tool_lines) + "\n"
            if tool_capable:
                system_message += "\nUse the provided function calling interface to invoke tools when they are relevant to the request. For multi-step tasks, call one tool at a time and use its result to inform the next step.\n"
            else:
                jinx_names_str = ", ".join(npc.jinxes_dict.keys())
                jinx_instructions = f"""
                if you are in the [ReAct loop] and you are asked to use jinxes, refer to these guidelines:
              [BEGIN GUIDELINES FOR JINX EXECUTION]
                  Use jinxes when appropriate. For example:

                    - If you are asked about something up-to-date or dynamic (e.g., latest exchange rates)
                    - If the user asks you to read or edit a file
                    - If the user asks for code that should be executed
                    - If the user requests to open, search, download or scrape, which involve actual system or web actions
                    - If they request a screenshot, audio, or image manipulation
                    - Situations requiring file parsing (e.g., CSV or JSON loading)
                    - Scripted workflows or pipelines, e.g., generate a chart, fetch data, summarize from source, etc.

                    You MUST use a jinx if the request directly refers to a tool the AI cannot handle directly (e.g., 'run', 'open', 'search', etc).

                    You do not need to use a jinx if:

                    - the user asks a simple question like 'what is 2+2' or 'who invented linux', essentially any question which only requires general knowledge.
                    - The user asks you to write them a story (unless they separately specify saving it to a file, then you should directly write the story to be output through a jinx to said file.)
                    To invoke a jinx, return the action 'invoke_jinx' along with the jinx specific name.
                    An example for a jinx-specific return would be:
                    """ +"""
                    {
                        "action": "invoke_jinx",
                        "jinx_name": "file_reader",
                        "explanation": "Read the contents of <full_filename_path_from_user_request> and <detailed explanation of how to accomplish the problem outlined in the request>."
                    }


                    Do not use the jinx names as the action keys. You must use the action 'invoke_jinx' to invoke a jinx!
                    Do not invent jinx names. Use only those provided.


                Respond with a single JSON object only.
                To use a jinx, set action to jinx, jinx_name to one of [{jinx_names_str}], and inputs with the required parameters.

              [END GUIDELINES FOR JINX EXECUTION]


"""
                system_message += jinx_instructions




    return system_message

handle_action_choice(command, action_data, jinxes, model=None, provider=None, api_url=None, api_key=None, npc=None, team=None, messages=None, images=None, stream=False, extra_globals=None, last_jinx_output=None, step_outputs=None, context='', **kwargs)

Source code in npcpy/llm_funcs.py
def handle_action_choice(command: str,
                         action_data: dict, 
                         jinxes: list, 
                         model : str = None,
                         provider: str = None, 
                         api_url:str = None, 
                         api_key: str = None , 
                         npc: Any = None,
                         team: Any = None,
                         messages: list = None,
                         images: list = None,
                         stream: bool = False, 
                         extra_globals: dict = None,
                         last_jinx_output = None,
                         step_outputs: list = None,
                         context: str = '',
                         **kwargs,
                        ):
    action_name = action_data.get("action", "answer")
    if action_name == "invoke_jinx" or 'jinx_name' in action_data:
        jname = action_data.get("jinx_name", "")
        step_context = context or ""
        if step_outputs:
            step_context += f"\nContext from previous steps: {json.dumps(step_outputs)}"

        result = handle_jinx_call(
            command, 
            jname, 
            jinxes,
            model=model, 
            provider=provider, 
            api_url=api_url, 
            api_key=api_key,
            npc=npc, 
            team=team, 
            messages=messages, 
            stream=stream,
            context=step_context, 
            extra_globals=extra_globals,
            previous_output=last_jinx_output,
        )
        output = result.get("output", "")
        current_messages = result.get("messages", messages)
        jinx_calls = result.get("jinx_calls", [])
        stream_events = result.get("stream_events", [])

        # Display
        if output and str(output).strip():
            content = str(output).replace('\\n', '\n').replace('\\t', '\t')
            render_markdown(f"\n{jname}:")
            lines = content.split('\n')
            if len(lines) > 50:
                render_markdown('\n'.join(lines[:25]))
                print(f"\n... ({len(lines) - 50} lines hidden) ...\n")
                render_markdown('\n'.join(lines[-25:]))
            else:
                render_markdown(content)
    elif action_name != 'answer':
        output = 'INVALID_ACTION'
        jinx_calls = []
        return {'output':output, 'messages':messages, 'jinx_calls': jinx_calls}
    else:
        response = get_llm_response(
            f"""The user asked: {command}

              Provide a direct answer. Do not reference tools or jinxes.""",
            model=model,
            provider=provider,
            api_url=api_url,
            api_key=api_key,
            messages=[],
            npc=npc,
            team=team,
            images=images,
            stream=stream,
            context=context,
            **kwargs,
        )
        output = response.get("response", "")
        current_messages = response.get("messages", messages)
        jinx_calls = []

    return {'output':output, 'messages':current_messages, 'jinx_calls': jinx_calls, 'stream_events': stream_events if 'stream_events' in locals() else []}

handle_jinx_call(command, jinx_name, jinxes, model=None, provider=None, api_url=None, api_key=None, npc=None, team=None, messages=None, stream=False, context=None, extra_globals=None, previous_output=None, n_attempts=3, attempt=0)

Resolve inputs for a single jinx and execute it. Retries on failure.

Source code in npcpy/llm_funcs.py
def handle_jinx_call(
    command,
    jinx_name,
    jinxes,
    model=None,
    provider=None,
    api_url=None,
    api_key=None,
    npc=None,
    team=None,
    messages=None,
    stream=False,
    context=None,
    extra_globals=None,
    previous_output=None,
    n_attempts=3,
    attempt=0,
):
    """Resolve inputs for a single jinx and execute it. Retries on failure."""
    if messages is None:
        messages = []

    jinx = jinxes.get(jinx_name)
    if not jinx:
        if attempt < n_attempts:
            available = ", ".join(jinxes.keys())
            return check_llm_command(
                f"""In the previous attempt, the jinx name was: {jinx_name}.
That jinx was not available. Only select from: {available}.
Original request: {command}""",
                model=model, 
                provider=provider, 
                api_url=api_url, 
                api_key=api_key,
                npc=npc, 
                team=team,
                messages=messages, 
                stream=stream,
                context=context, 
                extra_globals=extra_globals,
            )
        return {"output": f"Jinx '{jinx_name}' not found after {n_attempts} attempts.", "messages": messages}

    print(f"[JINX] {jinx.jinx_name}", flush=True)

    # Build example format from jinx inputs
    example_format = {}
    for inp in jinx.inputs:
        if isinstance(inp, str):
            example_format[inp] = "..."
        elif isinstance(inp, dict):
            key = list(inp.keys())[0]
            example_format[key] = "..."
    json_format_str = json.dumps(example_format, indent=4)

    # Show full jinx definition so model understands what it's filling
    prompt = f"""The user wants to use the jinx '{jinx_name}' with the following request:
'{command}'

Here were the previous 5 messages in the conversation: {messages[-5:]}

Here is the jinx file:
```
{jinx.to_dict()}
```

Please determine the required inputs for the jinx as a JSON object.
They must be exactly as they are named in the jinx.
If the jinx requires a file path, you must include an absolute path with extension.
If the jinx requires code, generate it exactly according to the instructions.

Return only the JSON object without any markdown formatting.
The format of the JSON object is:
{json_format_str}"""

    if npc and hasattr(npc, "shared_context"):
        if npc.shared_context.get("dataframes"):
            context_info = "\nAvailable dataframes:\n"
            for df_name in npc.shared_context["dataframes"].keys():
                context_info += f"- {df_name}\n"
            prompt += f"\nContextual info: {context_info}"

    response = get_llm_response(
        prompt,
        format="json",
        model=model,
        provider=provider,
        api_url=api_url,
        api_key=api_key,
        messages=messages[-10:],
        npc=npc,
        team=team,
        context=context,
    )

    response_text = response.get("response", "{}")
    if isinstance(response_text, str):
        response_text = response_text.replace("```json", "").replace("```", "").strip()
        try:
            input_values = json.loads(response_text)
        except json.JSONDecodeError as e:
            if attempt < n_attempts:
                return handle_jinx_call(
                    command, 
                    jinx_name, 
                    jinxes,
                    model=model, 
                    provider=provider, 
                    api_url=api_url, 
                    api_key=api_key,
                    npc=npc, 
                    team=team, 
                    messages=messages, 
                    stream=stream,
                    context=f"Previous attempt failed to parse JSON: {e}. Raw: {response_text}",
                    extra_globals=extra_globals, previous_output=previous_output,
                    n_attempts=n_attempts, 
                    attempt=attempt + 1,
                )
            return {"output": f"Error extracting inputs for jinx '{jinx_name}'", "messages": messages}
    elif isinstance(response_text, dict):
        input_values = response_text
    else:
        input_values = {}

    # Validate required inputs
    missing = []
    for inp in jinx.inputs:
        if not isinstance(inp, dict):
            if inp not in input_values or input_values[inp] == "":
                missing.append(inp)
    if missing and attempt < n_attempts:
        return handle_jinx_call(
            command + f". Previous attempt missing inputs: {missing}. Values were: {input_values}.",
            jinx_name, 
            jinxes,
            model=model, 
            provider=provider, 
            api_url=api_url, 
            api_key=api_key,
            npc=npc, team=team, 
            messages=messages, 
            stream=stream,
            context=context, 
            extra_globals=extra_globals, 
            previous_output=previous_output,
            n_attempts=n_attempts, 
            attempt=attempt + 1,
        )
    elif missing:
        return {"output": f"Missing inputs for jinx '{jinx_name}': {missing}", "messages": messages}

    print(f"[INPUTS] {json.dumps({k: str(v)[:200] for k, v in input_values.items()})}", flush=True)

    # Inject previous step output for inter-step data flow
    if previous_output is not None:
        input_values['previous_output'] = previous_output

    # Execute
    output = _execute_jinx(jinx, input_values, npc, team, messages, extra_globals)

    print(f"[RESULT] {str(output)[:300]}", flush=True)

    # Retry on error
    if isinstance(output, str) and output.startswith("Error:") and attempt < n_attempts:
        return handle_jinx_call(
            command, 
            jinx_name, 
            jinxes,
            model=model, 
            provider=provider, 
            api_url=api_url, 
            api_key=api_key,
            npc=npc, team=team, 
            messages=messages, 
            stream=stream,
            context=f"Jinx failed: {output}. Previous inputs: {input_values}",
            extra_globals=extra_globals, 
            previous_output=previous_output,
            n_attempts=n_attempts, 
            attempt=attempt + 1,
        )

    # Collect any stream events produced during jinx execution (e.g. delegation sub-events)
    stream_events = []
    if npc and hasattr(npc, 'shared_context') and npc.shared_context.get('sub_events'):
        stream_events = npc.shared_context.pop('sub_events')

    return {
        "output": output,
        "messages": messages,
        "jinx_calls": [{
            "name": jinx_name,
            "arguments": input_values,
            "result": str(output) if output else "",
        }],
        "stream_events": stream_events,
    }

handle_request_input(context, model, provider)

Analyze text and decide what to request from the user

Source code in npcpy/llm_funcs.py
def handle_request_input(
    context: str,
    model: str ,
    provider: str 
):
    """
    Analyze text and decide what to request from the user
    """
    json_format = """Return a JSON object with:
    {
        "input_needed": boolean,
        "request_reason": string explaining why input is needed,
        "request_prompt": string to show user if input needed
    }

    Do not include any additional markdown formatting or leading ```json tags. Your response
    must be a valid JSON object."""

    prompt = f"""
    Analyze the text:
    {context}
    and determine what additional input is needed.
    {json_format}
    """

    response = get_llm_response(
        prompt,
        model=model,
        provider=provider,
        messages=[],
        format="json",
    )

    result = response.get("response", {})
    if isinstance(result, str):
        result = json.loads(result)

    user_input = request_user_input(
        {"reason": result["request_reason"], "prompt": result["request_prompt"]},
    )
    return user_input

harmonize(prompt, items, model=None, provider=None, npc=None, team=None, harmony_rules=None, context=None, agent_roles=None, **kwargs)

Harmonize using multiple specialized agents

Source code in npcpy/llm_funcs.py
def harmonize(
    prompt: str,
    items: List[str],
    model: str = None,
    provider: str = None,
    npc: Any = None,
    team: Any = None,
    harmony_rules: List[str] = None,
    context: str = None,
    agent_roles: List[str] = None,
    **kwargs
) -> Dict[str, Any]:
    """Harmonize using multiple specialized agents"""

    if team and hasattr(team, 'npcs'):

        available_agents = list(team.npcs.values())

        if agent_roles:

            selected_agents = []
            for role in agent_roles:
                matching_agent = next((a for a in available_agents if role.lower() in a.name.lower() or role.lower() in a.primary_directive.lower()), None)
                if matching_agent:
                    selected_agents.append(matching_agent)
            agents_to_use = selected_agents or available_agents[:len(items)]
        else:

            agents_to_use = available_agents[:min(len(items), len(available_agents))]

        harmonized_results = []
        for i, (item, agent) in enumerate(zip(items, agents_to_use)):
            harmony_prompt = f"""Harmonize this element: {item}
Task: {prompt}
Rules: {', '.join(harmony_rules or ['maintain_consistency'])}
Context: {context}
Your role in harmony: {agent.primary_directive}"""

            response = get_llm_response(
                harmony_prompt,
                npc=agent,
                context=context,
                **kwargs
            )
            harmonized_results.append({
                'agent': agent.name,
                'item': item,
                'harmonized': response.get("response", "")
            })


        coordinator = team.get_forenpc() if team else npc
        synthesis_prompt = f"""Synthesize these harmonized elements:
{chr(10).join([f"{r['agent']}: {r['harmonized']}" for r in harmonized_results])}
Create unified harmonious result."""

        return get_llm_response(synthesis_prompt, npc=coordinator, context=context, **kwargs)

    else:

        items_text = chr(10).join([f"{i+1}. {item}" for i, item in enumerate(items)])
        harmony_prompt = f"""Harmonize these items: {items_text}
Task: {prompt}
Rules: {', '.join(harmony_rules or ['maintain_consistency'])}
Context: {context}"""

        return get_llm_response(harmony_prompt, model=model, provider=provider, npc=npc, context=context, **kwargs)

identify_groups(facts, model, provider, npc=None, context=None, **kwargs)

Identify natural groups from a list of facts

Source code in npcpy/llm_funcs.py
def identify_groups(
    facts: List[str],
    model,
    provider,
    npc =  None,
    context: str = None,
    **kwargs
) -> List[str]:
    """Identify natural groups from a list of facts"""


    prompt = """What are the main groups these facts could be organized into?
    Express these groups in plain, natural language.

    For example, given:
        - User enjoys programming in Python
        - User works on machine learning projects
        - User likes to play piano
        - User practices meditation daily

    You might identify groups like:
        - Programming
        - Machine Learning
        - Musical Interests
        - Daily Practices

    Return a JSON object with the following structure:
        `{
            "groups": ["list of group names"]
        }`

    Return only the JSON object. Do not include any additional markdown formatting or
    leading json characters.
    """

    response = get_llm_response(
        prompt + f"\n\nFacts: {json.dumps(facts)}",
        model=model,
        provider=provider,
        format="json",
        npc=npc,
        context=context,

        **kwargs
    )
    return response["response"]["groups"]

lookup_provider(model)

Determine the provider based on the model name. Checks custom providers first, then falls back to known providers.

Parameters:
  • model (str) –

    The model name

Returns:
  • str

    The provider name or None if not found

Source code in npcpy/npc_sysenv.py
def lookup_provider(model: str) -> str:
    """
    Determine the provider based on the model name.
    Checks custom providers first, then falls back to known providers.

    Args:
        model: The model name

    Returns:
        The provider name or None if not found
    """
    if model and os.path.isdir(os.path.expanduser(model)):
        adapter_config = os.path.join(os.path.expanduser(model), 'adapter_config.json')
        if os.path.exists(adapter_config):
            return "lora"

    custom_providers = load_custom_providers()

    for provider_name, config in custom_providers.items():
        if model.startswith(f"{provider_name}-"):
            return provider_name

        try:
            import requests
            api_key_var = config.get('api_key_var') or \
                         f"{provider_name.upper()}_API_KEY"
            api_key = os.environ.get(api_key_var)

            if api_key:
                base_url = config.get('base_url', '')
                headers = config.get('headers', {})
                headers['Authorization'] = f'Bearer {api_key}'

                models_endpoint = f"{base_url.rstrip('/')}/models"
                response = requests.get(
                    models_endpoint, 
                    headers=headers, 
                    timeout=1.0
                )

                if response.status_code == 200:
                    data = response.json()
                    models = []

                    if isinstance(data, dict) and 'data' in data:
                        models = [m['id'] for m in data['data']]
                    elif isinstance(data, list):
                        models = [m['id'] for m in data]

                    if model in models:
                        return provider_name
        except Exception:
            pass

    if model == "deepseek-chat" or model == "deepseek-reasoner":
        return "deepseek"

    if model.startswith("airllm-"):
        return "airllm"

    ollama_prefixes = [
        "llama", "deepseek", "qwen", "llava", 
        "phi", "mistral", "mixtral", "dolphin", 
        "codellama", "gemma",]
    if any(model.startswith(prefix) for prefix in ollama_prefixes):
        return "ollama"

    openai_prefixes = ["gpt-", "dall-e-", "whisper-", "o1"]
    if any(model.startswith(prefix) for prefix in openai_prefixes):
        return "openai"

    if model.startswith("claude"):
        return "anthropic"
    if model.startswith("gemini"):
        return "gemini"
    if "diffusion" in model:
        return "diffusers"

    return None

orchestrate(prompt, items, model=None, provider=None, npc=None, team=None, workflow='sequential_coordination', context=None, **kwargs)

Orchestrate using team.orchestrate method

Source code in npcpy/llm_funcs.py
def orchestrate(
    prompt: str,
    items: List[str],
    model: str = None,
    provider: str = None,
    npc: Any = None,
    team: Any = None,
    workflow: str = "sequential_coordination",
    context: str = None,
    **kwargs
) -> Dict[str, Any]:
    """Orchestrate using team.orchestrate method"""

    if team and hasattr(team, 'orchestrate'):

        orchestration_request = f"""Orchestrate workflow: {workflow}
Task: {prompt}
Items: {chr(10).join([f'- {item}' for item in items])}
Context: {context}"""

        return team.orchestrate(orchestration_request)

    else:

        items_text = chr(10).join([f"{i+1}. {item}" for i, item in enumerate(items)])
        orchestrate_prompt = f"""Orchestrate using {workflow}:
Task: {prompt}
Items: {items_text}
Context: {context}"""

        return get_llm_response(orchestrate_prompt, model=model, provider=provider, npc=npc, context=context, **kwargs)

print_and_process_stream_with_markdown(response, model, provider, show=False, rerender=True)

Source code in npcpy/npc_sysenv.py
def print_and_process_stream_with_markdown(response, model, provider, show=False, rerender=True):
    import sys

    str_output = ""
    dot_count = 0
    tool_call_data = {"id": None, "function_name": None, "arguments": ""}
    interrupted = False

    if isinstance(response, str):
        render_markdown(response)
        print('\n') 
        return response 


    if rerender:
        sys.stdout.write('\033[s')
        sys.stdout.flush()

    try:
        for chunk in response:

            if provider == "ollama":

                if "message" in chunk and "tool_calls" in chunk["message"]:
                    for tool_call in chunk["message"]["tool_calls"]:
                        if "id" in tool_call:
                            tool_call_data["id"] = tool_call["id"]
                        if "function" in tool_call:
                            if "name" in tool_call["function"]:
                                tool_call_data["function_name"] = tool_call["function"]["name"]
                            if "arguments" in tool_call["function"]:
                                if isinstance(tool_call["function"]["arguments"], dict):
                                    tool_call_data["arguments"] += json.dumps(tool_call["function"]["arguments"])
                                else:
                                    tool_call_data["arguments"] += tool_call["function"]["arguments"]
                chunk_content = chunk["message"]["content"] if "message" in chunk and "content" in chunk["message"] else ""
                reasoning_content = chunk['message'].get('thinking', '') if "message" in chunk and "thinking" in chunk['message'] else ""
                if show:
                    if len(reasoning_content) > 0:
                        print(reasoning_content, end="", flush=True)
                    if chunk_content != "":
                        print(chunk_content, end="", flush=True)
                else:
                    print('.', end="", flush=True)
                    dot_count += 1

            else:
                for c in chunk.choices:
                    if hasattr(c.delta, "tool_calls") and c.delta.tool_calls:
                        for tool_call in c.delta.tool_calls:
                            if tool_call.id:
                                tool_call_data["id"] = tool_call.id
                            if tool_call.function:
                                if hasattr(tool_call.function, "name") and tool_call.function.name:
                                    tool_call_data["function_name"] = tool_call.function.name
                                if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
                                    tool_call_data["arguments"] += tool_call.function.arguments

                chunk_content = ''
                reasoning_content = ''
                for c in chunk.choices:
                    if hasattr(c.delta, "reasoning_content"):
                        reasoning_content += c.delta.reasoning_content

                chunk_content += "".join(
                    c.delta.content for c in chunk.choices if c.delta.content
                )
                if show:
                    if reasoning_content is not None:
                        print(reasoning_content, end="", flush=True)
                    if chunk_content != "":
                        print(chunk_content, end="", flush=True)
                else:
                    print('.', end="", flush=True)
                    dot_count += 1

            if not chunk_content:
                continue
            str_output += chunk_content

    except KeyboardInterrupt:
        interrupted = True
        print('\n⚠️ Stream interrupted by user')

    if tool_call_data["id"] or tool_call_data["function_name"] or tool_call_data["arguments"]:
        str_output += "\n\n"
        if tool_call_data["id"]:
            str_output += f"**ID:** {tool_call_data['id']}\n\n"
        if tool_call_data["function_name"]:
            str_output += f"**Function:** {tool_call_data['function_name']}\n\n"
        if tool_call_data["arguments"]:
            try:
                args_parsed = json.loads(tool_call_data["arguments"])
                str_output += f"**Arguments:**\n```json\n{json.dumps(args_parsed, indent=2)}\n```"
            except Exception:
                str_output += f"**Arguments:** `{tool_call_data['arguments']}`"

    if interrupted:
        str_output += "\n\n[⚠️ Response interrupted by user]"


    if rerender:
        sys.stdout.write('\033[u')
        sys.stdout.write('\033[J')
        sys.stdout.flush()
        render_markdown(str_output)
    print('\n')

    return str_output

prune_fact_subset_llm(fact_subset, concept_name, model=None, provider=None, npc=None, context=None, **kwargs)

Identifies redundancies WITHIN a small, topically related subset of facts.

Source code in npcpy/llm_funcs.py
def prune_fact_subset_llm(fact_subset, 
                          concept_name, 
                          model=None,
                          provider=None,
                          npc=None,
                          context : str = None,
                          **kwargs):
    """Identifies redundancies WITHIN a small, topically related subset of facts."""
    print(f"  Step Sleep-A: Pruning fact subset for concept '{concept_name}'...")


    prompt = f"""
    The following facts are all related to the concept "{concept_name}".
    Review ONLY this subset and identify groups of facts that are semantically identical.
    Return only the set of facts that are semantically distinct, and archive the rest.

    Fact Subset: {json.dumps(fact_subset, indent=2)}

    Return a json list of groups
    """ + '{"refined_facts": [fact1, fact2, fact3, ...]}'
    response = get_llm_response(prompt, 
                                model=model, 
                                provider=provider, 
                                npc=None,
                                format="json", 
                                context=context)
    return response['response'].get('refined_facts', [])

remove_idempotent_groups(group_candidates, model=None, provider=None, npc=None, context=None, **kwargs)

Remove groups that are essentially identical in meaning, favoring specificity and direct naming, and avoiding generic structures.

Source code in npcpy/llm_funcs.py
def remove_idempotent_groups(
    group_candidates: List[str],
    model: str = None,
    provider: str =None,
    npc = None, 
    context : str = None,
    **kwargs: Any
) -> List[str]:
    """Remove groups that are essentially identical in meaning, favoring specificity and direct naming, and avoiding generic structures."""

    prompt = f"""Compare these group names. Identify and list ONLY the groups that are conceptually distinct and specific.

    GUIDELINES FOR SELECTING DISTINCT GROUPS:
    1.  **Prioritize Specificity and Direct Naming:** Favor precise nouns or noun phrases that directly name the subject.
    2.  **Prefer Concrete Entities/Actions:** If a name refers to a specific entity or action (e.g., "Earth", "Sun", "Water", "France", "User Authentication Module", "Lamb Shank Braising", "World War I"), keep it if it's distinct.
    3.  **Rephrase Gerunds:** If a name uses a gerund (e.g., "Understanding TDEs"), rephrase it to a noun or noun phrase (e.g., "Tidal Disruption Events").
    4.  **AVOID OVERLY GENERIC TERMS:** Do NOT use very broad or abstract terms that don't add specific meaning. Examples to avoid: "Concepts", "Processes", "Dynamics", "Mechanics", "Analysis", "Understanding", "Interactions", "Relationships", "Properties", "Structures", "Systems", "Frameworks", "Predictions", "Outcomes", "Effects", "Considerations", "Methods", "Techniques", "Data", "Theoretical", "Physical", "Spatial", "Temporal". If a group name seems overly generic or abstract, it should likely be removed or refined.
    5.  **Similarity Check:** If two groups are very similar, keep the one that is more descriptive or specific to the domain.

    EXAMPLE 1:
    Groups: ["Accretion Disk Formation", "Accretion Disk Dynamics", "Formation of Accretion Disks"]
    Distinct Groups: ["Accretion Disk Formation", "Accretion Disk Dynamics"] 

    EXAMPLE 2:
    Groups: ["Causes of Events", "Event Mechanisms", "Event Drivers"]
    Distinct Groups: ["Event Causation", "Event Mechanisms"] 

    EXAMPLE 3:
    Groups: ["Astrophysics Basics", "Fundamental Physics", "General Science Concepts"]
    Distinct Groups: ["Fundamental Physics"] 

    EXAMPLE 4:
    Groups: ["Earth", "The Planet Earth", "Sun", "Our Star"]
    Distinct Groups: ["Earth", "Sun"]

    EXAMPLE 5:
    Groups: ["User Authentication Module", "Authentication System", "Login Process"]
    Distinct Groups: ["User Authentication Module", "Login Process"]

    ---

    Now, analyze the following groups:
    Groups: {json.dumps(group_candidates)}

    Return JSON:
    """ + '{"distinct_groups": ["list of specific, precise, and distinct group names to keep"]}'

    response = get_llm_response(
        prompt,
        model=model,
        provider=provider,
        format="json",
        npc=npc,
        context=context,
        **kwargs
    )

    return response["response"]["distinct_groups"]

remove_redundant_groups(groups, model=None, provider=None, npc=None, context=None, **kwargs)

Remove redundant groups

Source code in npcpy/llm_funcs.py
def remove_redundant_groups(groups, 
                            model=None,
                            provider=None,
                            npc=None,
                            context: str = None,
                            **kwargs):
    """Remove redundant groups"""

    groups_text = "\n".join([f"- {g['name']}" for g in groups])

    prompt = f"""
    Remove redundant groups from this list:

    {groups_text}

    Merge similar groups and keep only distinct concepts.
    Create abstract categories that encompass multiple related facts, but do not unnecessarily combine facts with conjunctions. For example, do not try to combine "characters", "settings", and "physical reactions" into a
    compound group like "Characters, Setting, and Physical Reactions". This kind of grouping is not productive and only obfuscates true abstractions. 
    For example, a group that might encompass the three aforermentioned names might be "Literary Themes" or "Video Editing Functionis", depending on the context.
    Your aim is to abstract, not to just arbitrarily generate associations. 

    Group names should never be more than two words. They should not contain gerunds. They should never contain conjunctions like "AND" or "OR".

    Respond with JSON:
    """ + '{"groups": [{"name": "final group name"}]}'

    response = get_llm_response(prompt,
                                model=model,
                                provider=provider,
                                npc=npc,
                                context=context,
                                **kwargs)

    return response["response"].get("groups", [])

render_markdown(text)

Renders markdown text, but handles code blocks as plain syntax-highlighted text.

Source code in npcpy/npc_sysenv.py
def render_markdown(text: str) -> None:
    """
    Renders markdown text, but handles code blocks as plain syntax-highlighted text.
    """
    lines = text.split("\n")
    console = Console()

    inside_code_block = False
    code_lines = []
    prose_lines = []
    lang = None

    def _flush_prose():
        if not prose_lines:
            return
        import re
        # Normalize CR/CRLF and collapse 3+ consecutive blank lines to 2.
        block = "\n".join(ln.rstrip('\r') for ln in prose_lines)
        block = re.sub(r'\n{3,}', '\n\n', block)

        _box_re = re.compile(r'[─-╿]')  # U+2500–U+257F box drawing block

        def _is_structured(ln: str) -> bool:
            """Box-drawing art or 4-space-indented code: must be rendered verbatim."""
            s = ln.strip()
            return bool(s) and (bool(_box_re.search(ln)) or ln.startswith('    '))

        lines = block.split('\n')
        segments: list = []   # (is_structured: bool, lines: list[str])
        struct_acc: list = []
        prose_acc: list = []

        def _commit_struct():
            if struct_acc:
                segments.append((True, list(struct_acc)))
                struct_acc.clear()

        def _commit_prose():
            if prose_acc:
                acc = list(prose_acc)
                while acc and not acc[0].strip():
                    acc.pop(0)
                while acc and not acc[-1].strip():
                    acc.pop()
                if acc:
                    segments.append((False, acc))
                prose_acc.clear()

        in_struct = False
        for i, ln in enumerate(lines):
            if _is_structured(ln):
                if not in_struct:
                    _commit_prose()
                    in_struct = True
                struct_acc.append(ln)
            elif not ln.strip():
                if in_struct:
                    # Suppress blank if the next non-blank line is also structured.
                    j = i + 1
                    while j < len(lines) and not lines[j].strip():
                        j += 1
                    if j < len(lines) and _is_structured(lines[j]):
                        pass  # drop blank between consecutive structured lines
                    else:
                        _commit_struct()
                        in_struct = False
                        prose_acc.append(ln)
                else:
                    prose_acc.append(ln)
            else:
                if in_struct:
                    _commit_struct()
                    in_struct = False
                prose_acc.append(ln)

        if in_struct:
            _commit_struct()
        else:
            _commit_prose()

        for is_str, seg_lines in segments:
            seg = '\n'.join(seg_lines)
            if not seg.strip():
                continue
            if is_str:
                # Verbatim output preserves box-drawing structure.
                # Rich Markdown would join consecutive lines as soft-breaks.
                sys.stdout.write(seg + '\n')
                sys.stdout.flush()
            else:
                console.print(Markdown(seg))

        prose_lines.clear()

    for line in lines:
        if line.startswith("```"):
            if inside_code_block:
                code = "\n".join(code_lines)
                if code.strip():
                    syntax = Syntax(
                        code, lang or "python", theme="monokai", line_numbers=False
                    )
                    console.print(syntax)
                code_lines = []
            else:
                _flush_prose()
                lang = line[3:].strip() or None
            inside_code_block = not inside_code_block
        elif inside_code_block:
            code_lines.append(line)
        else:
            prose_lines.append(line)

    _flush_prose()

request_user_input(input_request)

Request and get input from user.

Parameters:
  • input_request (Dict[str, str]) –

    Dict with reason and prompt for input

Returns:
  • str

    User's input text

Source code in npcpy/npc_sysenv.py
def request_user_input(input_request: Dict[str, str]) -> str:
    """
    Request and get input from user.

    Args:
        input_request: Dict with reason and prompt for input

    Returns:
        User's input text
    """
    print(f"\nAdditional input needed: {input_request['reason']}")
    return input(f"{input_request['prompt']}: ")

resolve_model_provider(npc=None, team=None, model=None, provider=None, api_url=None, api_key=None, images=None, attachments=None)

Resolve model, provider, api_url, and api_key from npc/team/explicit overrides.

Source code in npcpy/llm_funcs.py
def resolve_model_provider(
    npc=None, team=None, model=None, provider=None,
    api_url=None, api_key=None, images=None, attachments=None,
):
    """Resolve model, provider, api_url, and api_key from npc/team/explicit overrides."""
    m, p, a_url, a_key = model, provider, api_url, api_key
    if m is not None and p is not None:
        pass
    elif p is None and m is not None:
        p = lookup_provider(m)
    elif npc is not None:
        if npc.provider is not None:
            p = npc.provider
        if npc.model is not None:
            m = npc.model
    elif team is not None:
        if team.model is not None:
            m = team.model
        if team.provider is not None:
            p = team.provider
    else:
        p = "ollama"
        m = None
    # Always resolve api_url and api_key from npc/team if not explicitly provided
    if a_url is None and npc is not None and getattr(npc, 'api_url', None) is not None:
        a_url = npc.api_url
    if a_url is None and team is not None and getattr(team, 'api_url', None) is not None:
        a_url = team.api_url
    if a_key is None and npc is not None and getattr(npc, 'api_key', None) is not None:
        a_key = npc.api_key
    if a_key is None and team is not None and getattr(team, 'api_key', None) is not None:
        a_key = team.api_key
    return m, p, a_url, a_key

spread_and_sync(prompt, variations, model=None, provider=None, npc=None, team=None, sync_strategy='consensus', context=None, **kwargs)

Spread across agents/variations then sync with distribution analysis

Source code in npcpy/llm_funcs.py
def spread_and_sync(
    prompt: str,
    variations: List[str],
    model: str = None,
    provider: str = None,
    npc: Any = None,
    team: Any = None,
    sync_strategy: str = "consensus",
    context: str = None,
    **kwargs
) -> Dict[str, Any]:
    """Spread across agents/variations then sync with distribution analysis"""

    if team and hasattr(team, 'npcs') and len(team.npcs) >= len(variations):

        agents = list(team.npcs.values())[:len(variations)]
        results = []

        for variation, agent in zip(variations, agents):
            variation_prompt = f"""Analyze from {variation} perspective:
Task: {prompt}
Context: {context}
Apply your expertise with {variation} approach."""

            response = get_llm_response(variation_prompt, npc=agent, context=context, **kwargs)
            results.append({
                'agent': agent.name,
                'variation': variation,
                'response': response.get("response", "")
            })
    else:

        results = []
        agent = npc or (team.get_forenpc() if team else None)

        for variation in variations:
            variation_prompt = f"""Analyze from {variation} perspective:
Task: {prompt}
Context: {context}"""

            response = get_llm_response(variation_prompt, model=model, provider=provider, npc=agent, context=context, **kwargs)
            results.append({
                'variation': variation,
                'response': response.get("response", "")
            })


    response_texts = [r['response'] for r in results]
    return synthesize(response_texts, sync_strategy, model, provider, npc or (team.get_forenpc() if team else None), context)

synthesize(prompt, model=None, provider=None, npc=None, team=None, context=None, **kwargs)

Synthesize information from multiple sources or perspectives

Source code in npcpy/llm_funcs.py
def synthesize(
    prompt: str,
    model: str = None,
    provider: str = None,
    npc: Any = None,
    team: Any = None,
    context: str = None,
    **kwargs
) -> Dict[str, Any]:
    """Synthesize information from multiple sources or perspectives"""

    responses = kwargs.get('responses', [prompt])
    sync_strategy = kwargs.get('sync_strategy', 'consensus')

    if len(responses) > 1:
        synthesis_prompt = f"""Synthesize these multiple perspectives:

        {chr(10).join([f'Response {i+1}: {r}' for i, r in enumerate(responses)])}

        Synthesis strategy: {sync_strategy}
        Context: {context}

        Create a coherent synthesis that incorporates key insights from all perspectives."""
    else:
        synthesis_prompt = f"""Refine and synthesize this content:

        {responses[0]}

        Context: {context}

        Create a clear, concise synthesis that captures the essence of the content."""

    return get_llm_response(
        synthesis_prompt,
        model=model,
        provider=provider,
        npc=npc,
        team=team,
        context=context,
        **kwargs
    )

zoom_in(facts, model=None, provider=None, npc=None, context=None, attempt_number=1, n_attempts=3, **kwargs)

Infer new implied facts from existing facts

Source code in npcpy/llm_funcs.py
def zoom_in(facts, 
            model= None,
            provider=None, 
            npc=None,
            context: str = None, 
            attempt_number: int = 1,
            n_attempts=3,            
            **kwargs):
    """Infer new implied facts from existing facts"""
    valid_facts = []
    for fact in facts:
        if isinstance(fact, dict) and 'statement' in fact:
            valid_facts.append(fact)
    if not valid_facts:
        return []     

    fact_lines = []
    for fact in valid_facts:
        fact_lines.append(f"- {fact['statement']}")
    facts_text = "\n".join(fact_lines)

    prompt = f"""
    Look at these facts and infer new implied facts:

    {facts_text}

    What other facts can be reasonably inferred from these?
    """ +"""
    Respond with JSON:
    {
        "implied_facts": [
            {
                "statement": "new implied fact",
                "inferred_from": ["which facts this comes from"]
            }
        ]
    }
    """

    response = get_llm_response(prompt, 
                                model=model, 
                                provider=provider, 
                                format="json", 
                                context=context,
                                npc=npc,
                                **kwargs)

    facts =  response.get("response", {}).get("implied_facts", [])
    if len(facts) == 0:
        return zoom_in(valid_facts, 
                       model=model, 
                       provider=provider, 
                       npc=npc,
                       context=context,
                       attempt_number=attempt_number+1,
                       n_tries=n_attempts,
                       **kwargs)
    return facts