diff --git a/lisa/_cli_tools/lisa_ai.py b/lisa/_cli_tools/lisa_ai.py
new file mode 100755
index 0000000000000000000000000000000000000000..ecb951c90a74491f6621cf8fa12b1e01f37d938b
--- /dev/null
+++ b/lisa/_cli_tools/lisa_ai.py
@@ -0,0 +1,364 @@
+#! /usr/bin/env python3
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# Copyright (C) 2024, Arm Limited and contributors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import argparse
+import os
+from enum import Enum, auto
+import textwrap
+import itertools
+import json
+from operator import itemgetter
+import inspect
+import xml.etree.ElementTree as ET
+
+from openai import AsyncOpenAI
+from agents import Agent, Runner, ModelSettings, set_default_openai_client, set_tracing_disabled
+from pydantic import BaseModel, Field
+
+from lisa.utils import get_short_doc, order_as, get_doc_url
+
+
+# def make_client():
+ # client = OpenAI(
+ # api_key=os.getenv("OPENAI_API_KEY"),
+ # base_url=os.getenv('OPENAI_API_BASE'),
+ # )
+ # return client
+
+def get_analysis_items():
+ from lisa.analysis.base import TraceAnalysisBase
+ available = [
+ {
+ 'name': f'{meth.__module__}.{meth.__qualname__}',
+ 'typ': 'func',
+ 'kind': kind,
+ 'doc': meth.__module__.split('.')[-1] + ' ' + ' '.join(meth.__name__.split('_')) + '. ' + get_short_doc(meth),
+ 'code': textwrap.dedent(inspect.getsource(meth)),
+ 'url': get_doc_url(meth),
+
+ }
+ for name, cls in TraceAnalysisBase.get_analysis_classes().items()
+ if not inspect.isabstract(cls)
+ for (kind, meth) in itertools.chain(
+ (
+ ('df', meth)
+ for meth in cls.get_df_methods()
+ ),
+ (
+ ('plot', meth)
+ for meth in cls.get_plot_methods()
+ ),
+ )
+ ]
+ return sorted(
+ available,
+ key=itemgetter('name')
+ )
+
+def recommend(args):
+ user_prompt = args.prompt
+
+ # Tracing does not use the client we provide, at best it uses the key from
+ # it but not the base_url so it's unusable for us.
+ set_tracing_disabled(True)
+ set_default_openai_client(
+ AsyncOpenAI(
+ api_key=os.getenv("OPENAI_API_KEY"),
+ base_url=os.getenv('OPENAI_API_BASE'),
+ )
+ )
+
+ class Item(BaseModel):
+ name: str = Field(description='name of the item')
+
+ def indent(s):
+ idt = ' ' * 16
+ return s.replace('\n', f'\n{idt}')
+
+ def make_xml_item(item):
+ item = item.copy()
+ del item['code']
+ doc = item.pop('doc')
+
+ elem = ET.Element('item')
+ for k, v in item.items():
+ v = str(v)
+ if False:
+ # if k in ('doc', 'code'):
+ child = ET.SubElement(elem, k)
+ child.text = v
+ else:
+ elem.set(k, v)
+
+ elem.text = doc
+
+ tree = ET.ElementTree(elem)
+ ET.indent(tree, space=' ' * 4, level=0)
+ return ET.tostring(tree.getroot(), encoding='unicode')
+
+ def make_lee_item(item):
+ item = item.copy()
+ del item['code']
+ return ' | '.join(
+ f'{k.upper()}: {v}'
+ for k, v in order_as(
+ item.items(),
+ ('id', 'name', 'kind', 'doc'),
+ key=itemgetter(0),
+ )
+ ) + f' | END ID: {item["id"]}'
+
+
+ def make_json_item(item):
+ # item = item.copy()
+ # del item['code']
+ import json
+ return json.dumps(item)
+
+ available = get_analysis_items()
+
+ # Put far away from each other items that look similar, to avoid confusing
+ # the LLM
+ def unsort(it, key):
+ data = sorted(it, key=key)
+ mid = len(data) // 2
+ half1 = data[:mid]
+ half2 = data[mid:]
+ for a, b in zip(half1, half2):
+ yield a
+ yield b
+
+ if len(half1) != len(half2):
+ yield half2[-1]
+
+ available = sorted(
+ available,
+ key=itemgetter('name'),
+ )
+ # for i, item in enumerate(available):
+ # item['id'] = i
+
+
+ with open('api.json', 'w') as f:
+ json.dump(
+ available,
+ f,
+ )
+ breakpoint()
+
+ map(
+ make_xml_item,
+ # make_lee_item,
+ # make_json_item,
+ available,
+ )
+
+ # XML is supposed to perform better than JSON
+ formatted_available = '\n'.join(
+ map(
+ make_xml_item,
+ # make_lee_item,
+ # make_json_item,
+ available,
+ )
+ )
+ formatted_available = f'\n{formatted_available}\n
'
+ print(formatted_available)
+
+ reword_agent = Agent(
+ name='Prompt reword',
+ model='gpt-4.1',
+ model_settings=ModelSettings(
+ temperature=0,
+ ),
+ instructions=textwrap.dedent('''
+ # Identity
+
+ You are a technical assistant rewording a user input in a domain-specific context.
+
+ # Instructions
+
+ Reword the given input using vocabulary from the following document:
+ {formatted_available}
+
+ * Specify in the kind of item that is needed as well as the reworded description.
+ * Use the domain-specific vocabulary.
+ * Always include the object of the prompt.
+ * Skip any preliminary text.
+
+
+ Plot temperature of CPUs
+
+
+
+ Plot the temperature of each CPU.
+
+
+
+ Get the temperature of CPUs.
+
+
+
+ Get temperature dataframe of all CPU.
+
+
+ # Vocabulary
+
+ * All vocabulary is to be interpreted in the context of the Linux kernel.
+ * "df": kind of functions that provide raw data as dataframe.
+ * "plot": kind of functions that provide a graph.
+ * "rtapp", "rt-app": rt-app command line tool, not RT (realtime) tasks.
+
+ '''),
+ tools=[],
+ output_type=str,
+ )
+
+ finder_agent = Agent(
+ name='Item finder',
+ model='gpt-4.1',
+ model_settings=ModelSettings(
+ temperature=0,
+ ),
+ instructions=textwrap.dedent('''
+ # Identity
+
+ You a helpful salesman recommending items from your listing matching what the user asked.
+
+ # Instructions
+
+ Find the items in the list with the docthat is the best answer to the user query. Then provide the verbatim name of that item exactly as provided in the "name" XML attribute.
+
+ 1. You are a precise data-driven assistant, never return a name that is not in the list. Never invent a name.
+ 2. Find the items in the list that are most related to the user prompt.
+ 3. Use the entire list, do not stop at the first match.
+ 4. Return the name of the 3 best matching items.
+
+ # Examples
+
+
+ DataFrame of a task's active time on a given CPU
+
+
+
+ lisa.analysis.tasks.TasksAnalysis.df_task_activation
+
+
+
+
+ Plot temperature of CPUs
+
+
+
+ lisa.analysis.thermal.ThermalAnalysis.plot_thermal_zone_temperature
+
+
+
+
+ Get the temperature of CPUs
+
+
+
+ lisa.analysis.thermal.ThermalAnalysis.df_thermal_zones_temperature
+
+
+
+
+ Tasks which wakeup more frequently than a specified threshold.
+
+
+
+ lisa.analysis.tasks.TasksAnalysis.df_top_wakeup
+
+
+
+ # Allowed items
+
+ {formatted_available}
+
+ '''),
+ tools=[],
+ output_type=list[Item],
+ )
+
+ # reworded_prompt = user_prompt
+ result = Runner.run_sync(reword_agent, user_prompt)
+ reworded_prompt = result.final_output
+ print('Reworded prompt:', reworded_prompt)
+
+ result = Runner.run_sync(finder_agent, reworded_prompt)
+ selected = result.final_output
+ allowed = {item['name'] for item in available}
+ print(selected)
+ selected = [
+ item
+ for item in selected
+ if item.name in allowed
+ ]
+ print(selected)
+
+ # client = make_client()
+ # completions api:
+ # completion = client.chat.completions.create(messages=[{"role": "user","content":"hi"}], model="gpt-4o-mini")
+ # completion = client.chat.completions.create(
+ # model="gpt-4o",
+ # messages=[
+ # {"role": "developer", "content": "Be excruciatingly polite"},
+ # {"role": "user", "content": "Hello there, let's waste a lot of tokens (and money and water) together!"},
+ # ],
+ # )
+ # print(completion.choices[0].message.content)
+
+ # # responses api
+ # response = client.responses.create(
+ # model="gpt-4o",
+ # instructions="You are a coding assistant that talks like a pirate.",
+ # input="How do I check if a Python object is an instance of a class?",
+ # )
+ # print(response.output_text)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="""
+ LISA AI assistant.
+
+ Set OPENAI_API_KEY and OPENAI_API_BASE environment variables before running.
+ """,
+ )
+
+ subparsers = parser.add_subparsers(title='subcommands', dest='subcommand')
+
+ recommend_parser = subparsers.add_parser(
+ 'recommend',
+ description='Recommend functions from the LISA Python API',
+ )
+ recommend_parser.add_argument(
+ 'prompt',
+ help='Prompt describing what you are after.'
+ )
+
+ args = parser.parse_args()
+
+ if args.subcommand == 'recommend':
+ recommend(args)
+
+if __name__ == '__main__':
+ main()
+
diff --git a/setup.py b/setup.py
index 9ae1b5322b27a0e8e6e52caccaf720a42924bd9b..b81fd5a92bbc1f7e323c64acd3533e97411b68b1 100755
--- a/setup.py
+++ b/setup.py
@@ -172,6 +172,11 @@ if __name__ == "__main__":
"cffi", # unshare syscall
"typeguard",
+
+ "openai",
+ "openai-agents",
+ "pydantic",
+
],
extras_require=extras_require,