avfranco commited on
Commit
ba8e285
·
1 Parent(s): af36e79

ea4all-gradio-agents-mcp-hackathon-tools-refactoring-apm

Browse files
ea4all/ea4all_mcp.py CHANGED
@@ -25,7 +25,8 @@ from PIL import Image
25
 
26
  from ea4all.utils.utils import (
27
  UIUtils,
28
- ea4all_agent_init, get_image, filter_page, init_dbr
 
29
  )
30
 
31
  TITLE = """
@@ -40,7 +41,7 @@ TITLE = """
40
  tracer = LangChainTracer(project_name=os.getenv('LANGCHAIN_PROJECT'))
41
 
42
  config = RunnableConfig(
43
- run_name = os.getenv('LANGCHAIN_RUNNAME', "ea4all-mcp"),
44
  tags = [os.getenv('EA4ALL_ENV', "MCP")],
45
  callbacks = [tracer],
46
  recursion_limit = 25,
@@ -63,7 +64,7 @@ async def run_qna_agentic_system(question: str) -> AsyncGenerator[list, None]:
63
  format_response = ""
64
  chat_memory = []
65
  if not question:
66
- format_response = "Hi, how are you today? To start our conversation, please chat your message!"
67
  chat_memory.append(ChatMessage(role="assistant", content=format_response))
68
  yield chat_memory
69
 
@@ -505,7 +506,7 @@ with gr.Blocks(title="Your ArchitectGPT",fill_height=True, fill_width=True) as e
505
  qna_prompt.submit(run_qna_agentic_system,[qna_prompt],ea4all_chatbot, api_name="landscape_answering_agent")
506
  #qna_prompt.submit(lambda: "", None, [qna_prompt])
507
  #ea4all_chatbot.like(fn=get_user_feedback)
508
- #qna_examples.input(lambda value: value, qna_examples, qna_prompt)
509
 
510
  #Execute Reference Architecture
511
  dbr_run.click(run_reference_architecture_agentic_system,show_progress='full',inputs=[dbr_text],outputs=[togaf_vision,tabs_togaf,tabs_reference_architecture, architecture_runway, diagram_header, tab_diagram], api_name="togaf_blueprint_generation")
@@ -515,7 +516,7 @@ with gr.Blocks(title="Your ArchitectGPT",fill_height=True, fill_width=True) as e
515
  vqa_prompt.submit(run_vqa_agentic_system,[vqa_prompt, vqa_image], ea4all_vqa, api_name="diagram_answering_agent")
516
 
517
  #ea4all_vqa.like(fn=get_user_feedback)
518
- vqa_examples.input(lambda value: [value['text'], value['files'][-1]], vqa_examples, outputs=[vqa_prompt, vqa_image])
519
 
520
  #Invoke CrewAI PMO Agentic System
521
  pmo_prompt.submit(run_pmo_agentic_system,[pmo_prompt],pmo_chatbot, api_name="architect_demand_agent")
 
25
 
26
  from ea4all.utils.utils import (
27
  UIUtils,
28
+ ea4all_agent_init, get_image, filter_page,
29
+ get_question_diagram_from_example,
30
  )
31
 
32
  TITLE = """
 
41
  tracer = LangChainTracer(project_name=os.getenv('LANGCHAIN_PROJECT'))
42
 
43
  config = RunnableConfig(
44
+ run_name = os.getenv('LANGCHAIN_RUNNAME', "ea4all-gradio-agent-mcp-hackathon-run"),
45
  tags = [os.getenv('EA4ALL_ENV', "MCP")],
46
  callbacks = [tracer],
47
  recursion_limit = 25,
 
64
  format_response = ""
65
  chat_memory = []
66
  if not question:
67
+ format_response = "Hi, how are you today? To start using the EA4ALL MCP Tool, provide the required Inputs!"
68
  chat_memory.append(ChatMessage(role="assistant", content=format_response))
69
  yield chat_memory
70
 
 
506
  qna_prompt.submit(run_qna_agentic_system,[qna_prompt],ea4all_chatbot, api_name="landscape_answering_agent")
507
  #qna_prompt.submit(lambda: "", None, [qna_prompt])
508
  #ea4all_chatbot.like(fn=get_user_feedback)
509
+ qna_examples.input(lambda value: value, qna_examples, qna_prompt, show_api=False)
510
 
511
  #Execute Reference Architecture
512
  dbr_run.click(run_reference_architecture_agentic_system,show_progress='full',inputs=[dbr_text],outputs=[togaf_vision,tabs_togaf,tabs_reference_architecture, architecture_runway, diagram_header, tab_diagram], api_name="togaf_blueprint_generation")
 
516
  vqa_prompt.submit(run_vqa_agentic_system,[vqa_prompt, vqa_image], ea4all_vqa, api_name="diagram_answering_agent")
517
 
518
  #ea4all_vqa.like(fn=get_user_feedback)
519
+ vqa_examples.input(get_question_diagram_from_example, vqa_examples, outputs=[vqa_prompt, vqa_image], show_api=False)
520
 
521
  #Invoke CrewAI PMO Agentic System
522
  pmo_prompt.submit(run_pmo_agentic_system,[pmo_prompt],pmo_chatbot, api_name="architect_demand_agent")
ea4all/src/ea4all_apm/graph.py CHANGED
@@ -6,8 +6,6 @@ and key functions for processing & routing user queries, generating answer to
6
  Enterprise Architecture related user questions
7
  about an IT Landscape or Websearch.
8
  """
9
- import json
10
- import tempfile
11
  import os
12
 
13
  from langgraph.graph import END, StateGraph
@@ -19,7 +17,7 @@ from langchain_core.prompts import PromptTemplate, FewShotChatMessagePromptTempl
19
  from langchain_core.prompts import ChatPromptTemplate
20
  from langchain_core.output_parsers.json import JsonOutputParser
21
  from langchain_core.output_parsers import StrOutputParser
22
- from langchain_core.runnables.history import RunnableLambda
23
  from langchain_core.runnables import RunnablePassthrough, RunnableConfig
24
  from langchain_core.runnables import RunnableGenerator
25
  from langchain_core.documents import Document
@@ -27,19 +25,15 @@ from langchain_core.documents import Document
27
  from langchain.load import dumps, loads
28
  from langchain.hub import pull
29
 
30
- ##Utils and tools
31
- from langchain_community.document_loaders import JSONLoader
32
- from langchain_community.utilities import BingSearchAPIWrapper
33
- from langchain_community.tools.bing_search.tool import BingSearchResults
34
-
35
  from operator import itemgetter
 
36
 
37
  #compute amount of tokens used
38
  import tiktoken
39
 
40
  #import APMGraph packages
41
  from ea4all.src.ea4all_apm.configuration import AgentConfiguration
42
- from ea4all.src.ea4all_apm.state import APMState, InputState
43
  import ea4all.src.ea4all_apm.prompts as e4p
44
  from ea4all.src.shared.utils import (
45
  load_mock_content,
@@ -50,12 +44,15 @@ from ea4all.src.shared.utils import (
50
  _join_paths,
51
  )
52
  from ea4all.src.shared import vectorstore
 
 
 
53
 
54
  # This file contains sample APM QUESTIONS
55
  APM_MOCK_QNA = "apm_qna_mock.txt"
56
 
57
  async def retrieve_documents(
58
- state: APMState, *, config: RunnableConfig
59
  ) -> dict[str, list[Document]]:
60
  """Retrieve documents based on a given query.
61
 
@@ -223,6 +220,7 @@ async def get_retrieval_chain(rag_input, ea4all_user, question, retriever, confi
223
  decomposition_prompt = ChatPromptTemplate.from_template(e4p.decomposition_answer_recursevely_template)
224
 
225
  # Answer each question and return final answer
 
226
  q_a_pairs = ""
227
  for q in questions:
228
  rag_chain = (
@@ -257,7 +255,7 @@ async def get_retrieval_chain(rag_input, ea4all_user, question, retriever, confi
257
  retrieval_chain = (
258
  {
259
  # Retrieve context using the normal question
260
- "normal_context": RunnableLambda(lambda x: x["standalone_question"]) | retriever,
261
  # Retrieve context using the step-back question
262
  "step_back_context": generate_queries_step_back | retriever,
263
  # Pass on the question
@@ -287,14 +285,14 @@ async def get_retrieval_chain(rag_input, ea4all_user, question, retriever, confi
287
 
288
  #Get relevant asnwers to user query
289
  ##get_relevant_documents "deprecated" - replaced by invoke : 2024-06-07
290
- def get_relevant_answers(query, config: RunnableConfig):
291
 
292
  if query != "":
293
  #retriever.vectorstore.index.ntotal
294
  #retriever = retriever_faiss(user_ip)
295
  #response = retriever.invoke({"standalone_question": query})
296
 
297
- response = retrieve_documents(query, config)
298
  return response
299
  else:
300
  return []
@@ -346,7 +344,7 @@ def get_relevant_questions():
346
 
347
  #Rephrase the original user question based on system prompt to lead a better LLM answer
348
  def user_query_rephrasing(
349
- state: APMState, _prompt=None, *, config: RunnableConfig
350
  ) -> dict[str,str]:
351
 
352
  question = getattr(state,'question')
@@ -475,7 +473,7 @@ async def grade_documents(state, config: RunnableConfig):
475
  score = retrieval_grader(llm).ainvoke(
476
  {"user_question": question, "document": d.page_content}
477
  )
478
- grade = score["score"]
479
  # Document relevant
480
  if grade.lower() == "yes":
481
  print("---GRADE: DOCUMENT RELEVANT---")
@@ -518,7 +516,7 @@ def decide_to_generate(state):
518
  return "generate"
519
 
520
  def grade_generation_v_documents_and_question(
521
- state:APMState, config: RunnableConfig):
522
  """
523
  Determines whether the generation is grounded in the document and answers question.
524
 
@@ -571,7 +569,7 @@ def grade_generation_v_documents_and_question(
571
  yield "not supported"
572
 
573
  async def apm_query_router(
574
- state: APMState, config: RunnableConfig
575
  ) -> str:
576
 
577
  configuration = AgentConfiguration.from_runnable_config(config)
@@ -595,12 +593,16 @@ async def apm_query_router(
595
 
596
  response = await route.ainvoke({"user_question": user_query})
597
 
598
- datasource = extract_structured_output(response.content)['datasource']
599
-
 
 
 
 
600
  return datasource
601
 
602
  async def retrieve(
603
- state: APMState, config: RunnableConfig
604
  ):
605
  """
606
  Retrieve documents
@@ -654,49 +656,8 @@ async def retrieve(
654
 
655
  return {"documents": format_docs(documents['cdocs']), "question": question, "rag":getattr(state,'rag')}
656
 
657
- async def websearch(
658
- state: APMState, config: RunnableConfig
659
- ) -> dict[str,any]:
660
- """
661
- Web search based on the re-phrased question.
662
-
663
- Args:
664
- state (dict): The current graph state
665
- config (RunnableConfig): Configuration with the model used for query analysis.
666
-
667
- Returns:
668
- state (dict): Updates documents key with appended web results
669
- """
670
-
671
- # print("---WEB SEARCH---")
672
- ##Rephrase user question to lead bettern LLM response
673
- question = user_query_rephrasing(state=state, config=config)['question']
674
-
675
- ##API Wrapper
676
- search = BingSearchAPIWrapper()
677
-
678
- ##Bing Search Results
679
- web_results = BingSearchResults(k=3, api_wrapper=search)
680
- result = await web_results.ainvoke(
681
- {"query": question},
682
- )
683
- fixed_string = result.replace("'", "\"")
684
- result_json = json.loads(fixed_string)
685
-
686
- # Create a temporary file
687
- with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file:
688
- # Write the JSON data to the temporary file
689
- json.dump(result_json, temp_file)
690
- temp_file.flush()
691
-
692
- # Load the JSON data from the temporary file
693
- loader = JSONLoader(file_path=temp_file.name, jq_schema=".[]", text_content=False)
694
- docs = loader.load()
695
-
696
- return {"documents": format_docs(docs), "question": question, "web_search": "Yes", "generation": None}
697
-
698
  ### Edges ###
699
- def route_to_node(state:APMState):
700
 
701
  if state.source == "websearch":
702
  #print("---ROUTE QUESTION TO WEB SEARCH---")
@@ -706,8 +667,8 @@ def route_to_node(state:APMState):
706
  return "vectorstore"
707
 
708
  async def route_question(
709
- state: APMState, config: RunnableConfig
710
- ) -> dict[str, any]:
711
  """
712
  Route question to web search or RAG.
713
 
@@ -724,12 +685,16 @@ async def route_question(
724
  return {"source":source}
725
 
726
  async def stream_generation(
727
- state: APMState, config: RunnableConfig
728
- ):
729
  configuration = AgentConfiguration.from_runnable_config(config)
730
 
731
  llm = get_llm_client(model=configuration.query_model, api_base_url=configuration.api_base_url,streaming=configuration.streaming)
732
 
 
 
 
 
733
  async for s in state:
734
  documents = getattr(s,"documents")
735
  web_search = getattr(s,"web_search")
@@ -759,8 +724,8 @@ async def stream_generation(
759
  yield(output)
760
 
761
  async def generate(
762
- state: APMState, config: RunnableConfig
763
- ) -> dict[str, any]:
764
  """
765
  Generate answer
766
 
@@ -797,7 +762,7 @@ async def generate(
797
 
798
  #ea4all-qna-agent-conversational-with-memory
799
  async def apm_agentic_qna(
800
- state:APMState, config: RunnableConfig):
801
 
802
  configuration = AgentConfiguration.from_runnable_config(config)
803
 
@@ -845,14 +810,17 @@ async def apm_agentic_qna(
845
 
846
  return {"documents": format_docs(documents['cdocs']), "question": question, "rag":5, "web_search": "No", "generation": None}
847
 
848
- async def final(state: APMState):
849
  return {"safety_status": state}
850
 
851
- async def choose_next(state: APMState):
852
- return "exit" if state.safety_status[0] == 'no' else "route"
 
 
 
853
 
854
  class SafetyCheck:
855
- def apm_safety_check(self,state: APMState, config: RunnableConfig):
856
 
857
  configuration = AgentConfiguration.from_runnable_config(config)
858
  question = state.question
@@ -877,7 +845,7 @@ class SafetyCheck:
877
  def __init__(self):
878
  self._safety_run = self.apm_safety_check
879
 
880
- def __call__(self, state: APMState, config: RunnableConfig) -> dict[str, list]:
881
  try:
882
  response = getattr(self, '_safety_run')(state, config)
883
  return {"safety_status": [response['safety_status'][0], "", state.question]}
@@ -886,7 +854,7 @@ class SafetyCheck:
886
 
887
  ##BUILD APM Graph
888
  # Build graph
889
- workflow = StateGraph(APMState, input=InputState, config_schema=AgentConfiguration)
890
 
891
  # Define the nodes
892
  workflow.add_node("safety_check",SafetyCheck())
 
6
  Enterprise Architecture related user questions
7
  about an IT Landscape or Websearch.
8
  """
 
 
9
  import os
10
 
11
  from langgraph.graph import END, StateGraph
 
17
  from langchain_core.prompts import ChatPromptTemplate
18
  from langchain_core.output_parsers.json import JsonOutputParser
19
  from langchain_core.output_parsers import StrOutputParser
20
+ from langchain_core.runnables import RunnableLambda
21
  from langchain_core.runnables import RunnablePassthrough, RunnableConfig
22
  from langchain_core.runnables import RunnableGenerator
23
  from langchain_core.documents import Document
 
25
  from langchain.load import dumps, loads
26
  from langchain.hub import pull
27
 
 
 
 
 
 
28
  from operator import itemgetter
29
+ from typing import AsyncGenerator, AsyncIterator
30
 
31
  #compute amount of tokens used
32
  import tiktoken
33
 
34
  #import APMGraph packages
35
  from ea4all.src.ea4all_apm.configuration import AgentConfiguration
36
+ from ea4all.src.ea4all_apm.state import InputState, OutputState, OverallState
37
  import ea4all.src.ea4all_apm.prompts as e4p
38
  from ea4all.src.shared.utils import (
39
  load_mock_content,
 
44
  _join_paths,
45
  )
46
  from ea4all.src.shared import vectorstore
47
+ from ea4all.src.tools.tools import (
48
+ websearch,
49
+ )
50
 
51
  # This file contains sample APM QUESTIONS
52
  APM_MOCK_QNA = "apm_qna_mock.txt"
53
 
54
  async def retrieve_documents(
55
+ state: OverallState, *, config: RunnableConfig
56
  ) -> dict[str, list[Document]]:
57
  """Retrieve documents based on a given query.
58
 
 
220
  decomposition_prompt = ChatPromptTemplate.from_template(e4p.decomposition_answer_recursevely_template)
221
 
222
  # Answer each question and return final answer
223
+ answer = ""
224
  q_a_pairs = ""
225
  for q in questions:
226
  rag_chain = (
 
255
  retrieval_chain = (
256
  {
257
  # Retrieve context using the normal question
258
+ "normal_context": RunnableLambda(lambda x: getattr(x, "standalone_question")) | retriever,
259
  # Retrieve context using the step-back question
260
  "step_back_context": generate_queries_step_back | retriever,
261
  # Pass on the question
 
285
 
286
  #Get relevant asnwers to user query
287
  ##get_relevant_documents "deprecated" - replaced by invoke : 2024-06-07
288
+ async def get_relevant_answers(state: OverallState, query, config: RunnableConfig):
289
 
290
  if query != "":
291
  #retriever.vectorstore.index.ntotal
292
  #retriever = retriever_faiss(user_ip)
293
  #response = retriever.invoke({"standalone_question": query})
294
 
295
+ response = await retrieve_documents(state, config=config)
296
  return response
297
  else:
298
  return []
 
344
 
345
  #Rephrase the original user question based on system prompt to lead a better LLM answer
346
  def user_query_rephrasing(
347
+ state: OverallState, _prompt=None, *, config: RunnableConfig
348
  ) -> dict[str,str]:
349
 
350
  question = getattr(state,'question')
 
473
  score = retrieval_grader(llm).ainvoke(
474
  {"user_question": question, "document": d.page_content}
475
  )
476
+ grade = getattr(score,"score", "no")
477
  # Document relevant
478
  if grade.lower() == "yes":
479
  print("---GRADE: DOCUMENT RELEVANT---")
 
516
  return "generate"
517
 
518
  def grade_generation_v_documents_and_question(
519
+ state:OverallState, config: RunnableConfig):
520
  """
521
  Determines whether the generation is grounded in the document and answers question.
522
 
 
569
  yield "not supported"
570
 
571
  async def apm_query_router(
572
+ state: OverallState, config: RunnableConfig
573
  ) -> str:
574
 
575
  configuration = AgentConfiguration.from_runnable_config(config)
 
593
 
594
  response = await route.ainvoke({"user_question": user_query})
595
 
596
+ extracted = extract_structured_output(response.content)
597
+ if extracted is not None:
598
+ datasource = extracted.get('datasource', 'vectorstore')
599
+ else:
600
+ datasource = 'vectorstore'
601
+
602
  return datasource
603
 
604
  async def retrieve(
605
+ state: OverallState, config: RunnableConfig
606
  ):
607
  """
608
  Retrieve documents
 
656
 
657
  return {"documents": format_docs(documents['cdocs']), "question": question, "rag":getattr(state,'rag')}
658
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  ### Edges ###
660
+ def route_to_node(state:OverallState):
661
 
662
  if state.source == "websearch":
663
  #print("---ROUTE QUESTION TO WEB SEARCH---")
 
667
  return "vectorstore"
668
 
669
  async def route_question(
670
+ state: OverallState, config: RunnableConfig
671
+ ) -> dict[str, str]:
672
  """
673
  Route question to web search or RAG.
674
 
 
685
  return {"source":source}
686
 
687
  async def stream_generation(
688
+ state: OverallState, config: RunnableConfig
689
+ ) -> AsyncGenerator[str, None]:
690
  configuration = AgentConfiguration.from_runnable_config(config)
691
 
692
  llm = get_llm_client(model=configuration.query_model, api_base_url=configuration.api_base_url,streaming=configuration.streaming)
693
 
694
+ documents = None
695
+ web_search = None
696
+ question = None
697
+ chat_memory = None
698
  async for s in state:
699
  documents = getattr(s,"documents")
700
  web_search = getattr(s,"web_search")
 
724
  yield(output)
725
 
726
  async def generate(
727
+ state: OverallState, config: RunnableConfig
728
+ ) -> dict[str, str]:
729
  """
730
  Generate answer
731
 
 
762
 
763
  #ea4all-qna-agent-conversational-with-memory
764
  async def apm_agentic_qna(
765
+ state:OverallState, config: RunnableConfig):
766
 
767
  configuration = AgentConfiguration.from_runnable_config(config)
768
 
 
810
 
811
  return {"documents": format_docs(documents['cdocs']), "question": question, "rag":5, "web_search": "No", "generation": None}
812
 
813
+ async def final(state: OverallState):
814
  return {"safety_status": state}
815
 
816
+ async def choose_next(state: OverallState):
817
+ if state.safety_status is not None and len(state.safety_status) > 0 and state.safety_status[0] == 'no':
818
+ return "exit"
819
+ else:
820
+ return "route"
821
 
822
  class SafetyCheck:
823
+ def apm_safety_check(self,state: OverallState, config: RunnableConfig):
824
 
825
  configuration = AgentConfiguration.from_runnable_config(config)
826
  question = state.question
 
845
  def __init__(self):
846
  self._safety_run = self.apm_safety_check
847
 
848
+ def __call__(self, state: OverallState, config: RunnableConfig) -> dict[str, list]:
849
  try:
850
  response = getattr(self, '_safety_run')(state, config)
851
  return {"safety_status": [response['safety_status'][0], "", state.question]}
 
854
 
855
  ##BUILD APM Graph
856
  # Build graph
857
+ workflow = StateGraph(OverallState, input=InputState, output=OutputState, config_schema=AgentConfiguration)
858
 
859
  # Define the nodes
860
  workflow.add_node("safety_check",SafetyCheck())
ea4all/src/ea4all_apm/state.py CHANGED
@@ -8,6 +8,11 @@ from dataclasses import dataclass, field
8
  from typing import Optional, Literal, List, Tuple
9
  from typing_extensions import TypedDict
10
 
 
 
 
 
 
11
  # Optional, the InputState is a restricted version of the State that is used to
12
  # define a narrower interface to the outside world vs. what is maintained
13
  # internally.
@@ -25,58 +30,31 @@ class InputState:
25
  question: user question
26
  """
27
  question: str
28
- safety_status: Optional[Tuple[str, str, str]] = None
29
-
30
- """Messages track the primary execution state of the agent.
31
-
32
- Typically accumulates a pattern of Human/AI/Human/AI messages; if
33
- you were to combine this template with a tool-calling ReAct agent pattern,
34
- it may look like this:
35
-
36
- 1. HumanMessage - user input
37
- 2. AIMessage with .tool_calls - agent picking tool(s) to use to collect
38
- information
39
- 3. ToolMessage(s) - the responses (or errors) from the executed tools
40
-
41
- (... repeat steps 2 and 3 as needed ...)
42
- 4. AIMessage without .tool_calls - agent responding in unstructured
43
- format to the user.
44
-
45
- 5. HumanMessage - user responds with the next conversational turn.
46
-
47
- (... repeat steps 2-5 as needed ... )
48
-
49
- Merges two lists of messages, updating existing messages by ID.
50
-
51
- By default, this ensures the state is "append-only", unless the
52
- new message has the same ID as an existing message.
53
-
54
- Returns:
55
- A new list of messages with the messages from `right` merged into `left`.
56
- If a message in `right` has the same ID as a message in `left`, the
57
- message from `right` will replace the message from `left`."""
58
 
 
 
 
 
59
 
60
- class Router(TypedDict):
61
- """Classify a user query."""
62
- logic: str
63
- datasource: Optional[Literal["vectorstore", "websearch"]] = None
64
 
65
  @dataclass(kw_only=True)
66
- class APMState(InputState):
67
  """State of the APM graph / agent."""
68
 
69
  """
70
- safety_status: user question's safeguarding status, justification, rephrased question
71
- router: classification of the user's query
72
- source: RAG or websearch
73
- web_search: whether to add search
74
- retrieved: list of documents retrieved by the retriever
75
- rag: last RAG approach used
76
- chat_memory: user chat memory
77
- generation: should the agent generate a response
78
- documents: list of documents retrieved by the retriever
79
  """
 
80
  router: Optional[Router] = None
81
  source: Optional[str] = None
82
  rag: Optional[str] = None
 
8
  from typing import Optional, Literal, List, Tuple
9
  from typing_extensions import TypedDict
10
 
11
+ class Router(TypedDict):
12
+ """Classify a user query."""
13
+ logic: str
14
+ datasource: Optional[Literal["vectorstore", "websearch"]]
15
+
16
  # Optional, the InputState is a restricted version of the State that is used to
17
  # define a narrower interface to the outside world vs. what is maintained
18
  # internally.
 
30
  question: user question
31
  """
32
  question: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ @dataclass(kw_only=True)
35
+ class OutputState:
36
+ """Represents the output schema for the APM agent.
37
+ """
38
 
39
+ answer: str
40
+ """Answer to user's Architecture IT Landscape question about ."""
 
 
41
 
42
  @dataclass(kw_only=True)
43
+ class OverallState(InputState):
44
  """State of the APM graph / agent."""
45
 
46
  """
47
+ safety_status: user question's safeguarding status, justification, rephrased question
48
+ router: classification of the user's query
49
+ source: RAG or websearch
50
+ web_search: whether to add search
51
+ retrieved: list of documents retrieved by the retriever
52
+ rag: last RAG approach used
53
+ chat_memory: user chat memory
54
+ generation: should the agent generate a response
55
+ documents: list of documents retrieved by the retriever
56
  """
57
+ safety_status: Optional[Tuple[str, str, str]] = None
58
  router: Optional[Router] = None
59
  source: Optional[str] = None
60
  rag: Optional[str] = None
ea4all/src/graph.py CHANGED
@@ -83,7 +83,7 @@ def make_supervisor_node(model: BaseChatModel, members: list[str]):
83
  if _goto not in ["portfolio_team", "diagram_team", "blueprint_team", "websearch_team"]:
84
  _goto = "__end__"
85
 
86
- print(f"---Supervisor got a request--- Routing to {_goto} \n User Question: {state['messages'][-1].content}")
87
 
88
  return Command(
89
  #update={"next": _goto},
@@ -98,7 +98,7 @@ async def call_landscape_agentic(state: State, config: RunnableConfig) -> Comman
98
  update={
99
  "messages": [
100
  AIMessage(
101
- content=response.get('generation', response['safety_status']), name="landscape_agentic"
102
  )
103
  ]
104
  },
@@ -151,7 +151,7 @@ async def call_togaf_agentic(state: State, config: RunnableConfig) -> Command[Li
151
 
152
  # Wrap-up websearch answer to user's question
153
  async def call_generate_websearch(state:State, config: RunnableConfig) -> Command[Literal["__end__"]]:
154
- from ea4all.src.ea4all_apm.state import APMState
155
 
156
  if config is not None:
157
  source = config.get('metadata', {}).get('langgraph_node', 'unknown')
@@ -164,7 +164,7 @@ async def call_generate_websearch(state:State, config: RunnableConfig) -> Comman
164
  "source": source
165
  }
166
 
167
- apm_state = APMState(**state_dict)
168
  generation = await apm_graph.nodes["generate"].ainvoke(apm_state, config)
169
 
170
  return Command(
 
83
  if _goto not in ["portfolio_team", "diagram_team", "blueprint_team", "websearch_team"]:
84
  _goto = "__end__"
85
 
86
+ print(f"---Supervisor got a request--- Question: {state['messages'][-1].content} ==> Routing to {_goto}\n")
87
 
88
  return Command(
89
  #update={"next": _goto},
 
98
  update={
99
  "messages": [
100
  AIMessage(
101
+ content=str(response), name="landscape_agentic"
102
  )
103
  ]
104
  },
 
151
 
152
  # Wrap-up websearch answer to user's question
153
  async def call_generate_websearch(state:State, config: RunnableConfig) -> Command[Literal["__end__"]]:
154
+ from ea4all.src.ea4all_apm.state import OverallState
155
 
156
  if config is not None:
157
  source = config.get('metadata', {}).get('langgraph_node', 'unknown')
 
164
  "source": source
165
  }
166
 
167
+ apm_state = OverallState(**state_dict)
168
  generation = await apm_graph.nodes["generate"].ainvoke(apm_state, config)
169
 
170
  return Command(
ea4all/src/tools/tools.py CHANGED
@@ -2,8 +2,9 @@ from typing import Literal, Annotated
2
  from typing_extensions import TypedDict
3
  import json
4
  import tempfile
 
5
 
6
- from langchain_core.runnables import RunnableConfig
7
 
8
  from langgraph.graph import END
9
  from langgraph.types import Command
@@ -21,14 +22,14 @@ from ea4all.src.shared.configuration import (
21
 
22
  from ea4all.src.shared.state import (
23
  State
24
- )
25
 
26
  from ea4all.src.shared.utils import (
27
  get_llm_client,
28
  format_docs,
29
  )
30
 
31
- def make_supervisor_node(config: RunnableConfig, members: list[str]) -> str:
32
  options = ["FINISH"] + members
33
  system_prompt = (
34
  "You are a supervisor tasked with managing a conversation between the"
@@ -61,9 +62,9 @@ def make_supervisor_node(config: RunnableConfig, members: list[str]) -> str:
61
 
62
  return Command(goto=goto, update={"next": goto})
63
 
64
- return supervisor_node
65
 
66
- async def websearch(state: State):
67
  """
68
  Web search based on the re-phrased question.
69
 
@@ -76,15 +77,20 @@ async def websearch(state: State):
76
  """
77
 
78
  ##API Wrapper
79
- search = BingSearchAPIWrapper()
 
 
 
 
 
80
 
81
  question = state.get('messages')[-1].content
82
 
83
  ##Bing Search Results
84
  web_results = BingSearchResults(
85
- k=5,
86
  api_wrapper=search,
87
  handle_tool_error=True,
 
88
  )
89
 
90
  result = await web_results.ainvoke({"query": question})
 
2
  from typing_extensions import TypedDict
3
  import json
4
  import tempfile
5
+ import os
6
 
7
+ from langchain_core.runnables import RunnableLambda, RunnableConfig
8
 
9
  from langgraph.graph import END
10
  from langgraph.types import Command
 
22
 
23
  from ea4all.src.shared.state import (
24
  State
25
+ )
26
 
27
  from ea4all.src.shared.utils import (
28
  get_llm_client,
29
  format_docs,
30
  )
31
 
32
+ def make_supervisor_node(config: RunnableConfig, members: list[str]) -> RunnableLambda:
33
  options = ["FINISH"] + members
34
  system_prompt = (
35
  "You are a supervisor tasked with managing a conversation between the"
 
62
 
63
  return Command(goto=goto, update={"next": goto})
64
 
65
+ return RunnableLambda(supervisor_node)
66
 
67
+ async def websearch(state: State) -> dict[str,dict[str,str]]:
68
  """
69
  Web search based on the re-phrased question.
70
 
 
77
  """
78
 
79
  ##API Wrapper
80
+ bing_subscription_key = os.environ.get("BING_SUBSCRIPTION_KEY", "")
81
+ bing_search_url = os.environ.get("BING_SEARCH_URL", "https://api.bing.microsoft.com/v7.0/search")
82
+ search = BingSearchAPIWrapper(
83
+ bing_subscription_key=bing_subscription_key,
84
+ bing_search_url=bing_search_url
85
+ )
86
 
87
  question = state.get('messages')[-1].content
88
 
89
  ##Bing Search Results
90
  web_results = BingSearchResults(
 
91
  api_wrapper=search,
92
  handle_tool_error=True,
93
+ args_schema={"k":"5"},
94
  )
95
 
96
  result = await web_results.ainvoke({"query": question})
ea4all/utils/utils.py CHANGED
@@ -8,7 +8,6 @@ from ea4all.src.shared.configuration import BaseConfiguration
8
  from ea4all.src.ea4all_indexer.configuration import IndexConfiguration
9
  from ea4all.src.ea4all_indexer.graph import indexer_graph
10
 
11
-
12
  from langchain_community.document_loaders import ConfluenceLoader
13
  from langchain_core.messages import ChatMessage
14
  from langsmith import Client
@@ -164,3 +163,11 @@ def on_dbrtext(file):
164
 
165
  def unload_dbr():
166
  return gr.TextArea(visible=False)
 
 
 
 
 
 
 
 
 
8
  from ea4all.src.ea4all_indexer.configuration import IndexConfiguration
9
  from ea4all.src.ea4all_indexer.graph import indexer_graph
10
 
 
11
  from langchain_community.document_loaders import ConfluenceLoader
12
  from langchain_core.messages import ChatMessage
13
  from langsmith import Client
 
163
 
164
  def unload_dbr():
165
  return gr.TextArea(visible=False)
166
+
167
+ def get_question_diagram_from_example(value) -> list:
168
+ """
169
+ Extracts the question and diagram from the selected example.
170
+ """
171
+ if value:
172
+ return [value['text'], value['files'][-1]] if 'files' in value else [value['text'], None]
173
+ return ["", None]
pyproject.toml CHANGED
@@ -4,4 +4,6 @@ version = "0.1.0"
4
  description = "EA4ALL Agentic System MCP Server"
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
- dependencies = []
 
 
 
4
  description = "EA4ALL Agentic System MCP Server"
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
+ dependencies = [
8
+ "jq>=1.8.0",
9
+ ]
requirements.txt CHANGED
@@ -4,6 +4,7 @@ gradio==5.32.1
4
  gradio_client==1.10.2
5
  graphviz
6
  huggingface-hub
 
7
  langchain==0.3.25
8
  langchain-community==0.3.24
9
  langchain-core==0.3.61
 
4
  gradio_client==1.10.2
5
  graphviz
6
  huggingface-hub
7
+ jq>=1.8.0
8
  langchain==0.3.25
9
  langchain-community==0.3.24
10
  langchain-core==0.3.61
uv.lock ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "ea4all-gradio-agent-mcp-hackathon"
7
+ version = "0.1.0"
8
+ source = { virtual = "." }
9
+ dependencies = [
10
+ { name = "jq" },
11
+ ]
12
+
13
+ [package.metadata]
14
+ requires-dist = [{ name = "jq", specifier = ">=1.8.0" }]
15
+
16
+ [[package]]
17
+ name = "jq"
18
+ version = "1.8.0"
19
+ source = { registry = "https://pypi.org/simple" }
20
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/32/3eaca3ac81c804d6849da2e9f536ac200f4ad46a696890854c1f73b2f749/jq-1.8.0.tar.gz", hash = "sha256:53141eebca4bf8b4f2da5e44271a8a3694220dfd22d2b4b2cfb4816b2b6c9057", size = 2058265, upload-time = "2024-08-17T08:13:36.301Z" }
21
+ wheels = [
22
+ { url = "https://files.pythonhosted.org/packages/45/b3/dd0d41cecb0d8712bc792b3c40b42a36c355d814d61f6bda4d61cbb188e5/jq-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14f5988ae3604ebfdba2da398f9bd941bb3a72144a2831cfec2bc22bd23d5563", size = 415943, upload-time = "2024-08-17T08:14:38.437Z" },
23
+ { url = "https://files.pythonhosted.org/packages/9b/2c/39df803632c7222e9cd6922101966ddbec05d1c4213e7923c95e4e442666/jq-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f8903b66fac9f46de72b3a2f69bfa3c638a7a8d52610d1894df87ef0a9e4d2d3", size = 422267, upload-time = "2024-08-17T08:14:40.746Z" },
24
+ { url = "https://files.pythonhosted.org/packages/3a/b3/ddc1e691b832c6aa0f5142935099c1f05a89ff2f337201e2dcfafc726ec9/jq-1.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccda466f5722fa9be789099ce253bfc177e49f9a981cb7f5b6369ea37041104", size = 729142, upload-time = "2024-08-17T08:14:44.144Z" },
25
+ { url = "https://files.pythonhosted.org/packages/c5/b9/42a55d08397d25b4b1f6580f58c59ba3e3e120270db2e75923644ccc0d29/jq-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f57649e84a09b334eeb80d22ecc96ff7b31701f3f818ef14cb8bb162c84863", size = 748871, upload-time = "2024-08-17T08:14:46.816Z" },
26
+ { url = "https://files.pythonhosted.org/packages/90/4f/83639fdae641b7e8095b4a51d87a3da46737e70570d9df14d99ea15a0b16/jq-1.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7453731008eb7671725222781eb7bc5ed96e80fc9a652d177cb982276d3e08b4", size = 735908, upload-time = "2024-08-17T08:14:48.865Z" },
27
+ { url = "https://files.pythonhosted.org/packages/f7/9f/f54c2050b21490201613a7328534d2cb0c34e5a547167849a1464d89ae3e/jq-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:917812663613fc0542117bbe7ec43c8733b0c6bb174db6be06a15fc612de3b70", size = 721970, upload-time = "2024-08-17T08:14:51.442Z" },
28
+ { url = "https://files.pythonhosted.org/packages/24/b0/6c9a14ef103df4208e032bce25e66293201dacac18689d2ec4c0e68c8b77/jq-1.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ec9e4db978237470e9d65f747eb459f4ffee576c9c9f8ca92ab32d5687a46e4a", size = 746825, upload-time = "2024-08-17T08:14:53.536Z" },
29
+ { url = "https://files.pythonhosted.org/packages/f4/67/4eb836a9eac5f02983ed7caf76c4d0cad32fdd6ae08176be892b3a6b3d17/jq-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9f2548c83473bbe88a32a0735cb949a5d01804f8d411efae5342b5d23be8a2f", size = 751186, upload-time = "2024-08-17T08:14:57.32Z" },
30
+ { url = "https://files.pythonhosted.org/packages/2c/8f/66739f56ee1e3d144e7eef6453c5967275f75bf216e1915cdd9652a779aa/jq-1.8.0-cp312-cp312-win32.whl", hash = "sha256:e3da3538549d5bdc84e6282555be4ba5a50c3792db7d8d72d064cc6f48a2f722", size = 405483, upload-time = "2024-08-17T08:15:00.532Z" },
31
+ { url = "https://files.pythonhosted.org/packages/f6/9f/e886c23b466fc41f105b715724c19dd6089585f2e34375f07c38c69ceaf1/jq-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:049ba2978e61e593299edc6dd57b9cefd680272740ad1d4703f8784f5fab644d", size = 417281, upload-time = "2024-08-17T08:15:03.048Z" },
32
+ { url = "https://files.pythonhosted.org/packages/9c/25/c73afa16aedee3ae87b2e8ffb2d12bdb9c7a34a8c9ab5038318cb0b431fe/jq-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aea6161c4d975230e85735c0214c386e66035e96cfc4fd69159e87f46c09d4", size = 415000, upload-time = "2024-08-17T08:15:05.25Z" },
33
+ { url = "https://files.pythonhosted.org/packages/06/97/d09338697ea0eb7386a3df0c6ca2a77ab090c19420a85acdc6f36971c6b8/jq-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c24a5f9e3807e277e19f305c8bcd0665b8b89251b053903f611969657680722", size = 421253, upload-time = "2024-08-17T08:15:07.633Z" },
34
+ { url = "https://files.pythonhosted.org/packages/b8/c3/d020c19eca167b5085e74d2277bc3d9e35d1b4ee5bcb9076f1e26882514d/jq-1.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb484525dd801583ebd695d02f9165445a4d1b2fb560b187e6fc654911f0600e", size = 725885, upload-time = "2024-08-17T08:15:10.647Z" },
35
+ { url = "https://files.pythonhosted.org/packages/78/b8/8f6b886856f52f3277663d2d7a199663c6ede589dd0714aac9491b82ba6e/jq-1.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddd9abdf0c1b30be1bf853d8c52187c96a51b2cbc05f40c43a37bf6a9b956807", size = 746334, upload-time = "2024-08-17T08:15:13.183Z" },
36
+ { url = "https://files.pythonhosted.org/packages/76/c2/2fa34e480068863ab372ec91c59b10214e9f8f3ae8b6e2de61456e93bae1/jq-1.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c7464d9b88c74a7119b53f4bbf88028d07a9de9a1a279e45209b763b89d6582", size = 733716, upload-time = "2024-08-17T08:15:15.836Z" },
37
+ { url = "https://files.pythonhosted.org/packages/2e/db/59cb84ec59247af7f7bedd2b5c88b3a4ca17253fd2cc0d40f08573f7ff72/jq-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b99761e8ec2cedb9906df4ceae33f467a377621019ef40a9a275689ac3577456", size = 720978, upload-time = "2024-08-17T08:15:17.759Z" },
38
+ { url = "https://files.pythonhosted.org/packages/e0/6f/d04bdcc037ced716e2522ebf7a677541b8654d7855cd1404d894f1ecd144/jq-1.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1be1638f9d5f38c83440fb9626d8f78905ed5d70e926e3a664d3de1198e1ef79", size = 746431, upload-time = "2024-08-17T08:15:19.948Z" },
39
+ { url = "https://files.pythonhosted.org/packages/84/52/f100fb2ccd467c17a2ecc186334aa7b512e49ca1a678ecc53dd4defd6e22/jq-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d7e82d58bf3afe373afb3a01f866e473bbd34f38377a2f216c6222ec028eeea", size = 750404, upload-time = "2024-08-17T08:15:22.198Z" },
40
+ { url = "https://files.pythonhosted.org/packages/86/b4/e2459542207238d86727cf81af321ee4920497757092facf347726d64965/jq-1.8.0-cp313-cp313-win32.whl", hash = "sha256:96cb0bb35d55b19b910b12aba3d72e333ad6348a703494c7738cc4664e4410f0", size = 405691, upload-time = "2024-08-17T08:15:25.346Z" },
41
+ { url = "https://files.pythonhosted.org/packages/ce/4d/6e1230f96052d578439eee4ea28069728f3ad4027de127a93b8c6da142f0/jq-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:53e60a87657efc365a5d9ccfea2b536cddc1ffab190e823f8645ad933b272d51", size = 417930, upload-time = "2024-08-17T08:15:28.487Z" },
42
+ ]