How Amnesty International speaks to manage the Agile program with meeting and simplifying
With the combination of COPILOT into the institution’s applications, finding data is rarely used from files, SharePoint and other sources that can be accessed incredibly easy. I used to rely heavily on this Gen AI’s ability. One day, you needed a brief view of all the features (delivery of the team for a quarter in the AGILE) and put them on the team. Unfortunately, Copilot denied reading data from the meeting page, which is ideal and expected. Many organizations, projects and program updates are stored on meeting pages. Overview of the team’s goals, deliveries, project risk, and the situation is consumed for a leader or person who deals with multiple programs. I thought, why do you not have a smart assistant to bring and summarize that information? It will be an effective empowerment factor for the director of the program and the higher leadership.
The insertion of Agency Ai was savior for me, and I decided to choose this frame as a solution. However, there were challenges: What is the framework to be used, and is there an open source available? What is the cost of the managed platform? Finally, with all research, I decided to go with the open source and use the technical stack below to create a lightweight AI assistant using:
Step 1. Search meeting page.
Instead of handing URLs manually, each team name is set to its URL using a dictionary. When the user chooses “Team A” from the right part, the background automatically brings the associated URL and leads to a bulldozer.
Step 2. The web was bulldozing through the theatrical writer
This step took some time. Finally, I ended up using the theatrical writer for the browser -based freezing, which helps us to download dynamic content and deal with login:
Step 3. Determine the customer, agent and rapid engineering with a semantic nucleus.
The tool aims to be a empowerment factor for the management of the program. The agent’s instructions are formulated to use broken content and produce a suitable result for PM. The instructions will help obtain out as a text summary or in a planning coordination. This is also an example of a low symbol.
In addition, I defined the agent as an Amnesty International as an additional component.
Step 4. Determine the user’s insertion to process the question with or without the tool.
I decided to add an additional LLM customer to check whether the user’s entry is suitable for managing the program or not.
Step 5. The last step is to produce the result. Here is the entire symbol.
Here is the entire symbol. I deleted my project details. We need to store a state.json first to use it in the code
Json Import Import Import Asyncio Import Pandas As PD ISPORTS AS St writing from Dotenv Import Load_dotenv from OpenAi Import Asyncazureopenai from Playwright.async semantic_KERNEL.CONNECTORS.AI.APEN_AI Import OpenAICHATCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCOCCOCOCOCOCOCCOCCOLTION from semantic_Kerrene.Contents job import,
TEAM_URL_MAPPING = {“Team 1”: “Url Confluence for Team 1”, “Team 2”: “Confluence URL for Team 2”, “Team 3”: “Confluence Url for Team 3”, “Team 4”: “Confluence Url For For Team 4, “Team 5”
# —- Definition of the auxiliary program —-
#Bar Chart with a fixed size Def Plot_bar_chart (df): star_count[“status”]Value_counts () Fig, ax = pl ‘y’, colors = ‘Green’) # y-ticks in the green st.plot (shape)
Def Extract_json_from_response (Text): # Use Regex to find the first JSON Safin in Text Match = Re.search (R “(\ \[\s*{.*}\s*\]), Text, Return
Plugin: Def
@kernel_function(description="Scrape and return text content from a Confluence page.")
async def get_confluence_page_content(
self, team_name: Annotated\[str, "Name of the Agile team"\]
) -> Annotated\[str, "Returns extracted text content from the page"\]:
print(f"Attempting to scrape Confluence page for team: '{team_name}'") # Added for debugging
target_url = TEAM_URL_MAPPING.get(team_name)
if not target_url:
print(f"Failed to find URL for team: '{team_name}' in TEAM_URL_MAPPING.") # Added for debugging
return f"
No Confluence URL mapped for team '{team_name}'"
async with async_playwright() as p:
browser = await p.chromium.launch()
context = await browser.new_context(storage_state="state.json")
page = await context.new_page()
pages_to_scrape = \[target_url\]
# Loop through each page URL and scrape the content
for page_url in pages_to_scrape:
await page.goto(page_url)
await asyncio.sleep(30) # Wait for the page to load
await page.wait_for_selector('div.refresh-module-id, table.some-jira-table')
html = await page.content()
soup = BeautifulSoup(html, "html.parser")
body_div = soup.find("div", class_="wiki-content") or soup.body
if not body_div:
return "
Could not find content on the Confluence page."
# Process the scraped content (example: extract headings)
headings = soup.find_all('h2')
text = body_div.get_text(separator="\\n", strip=True)
return text\[:4000\] # Truncate if needed to stay within token limits
await browser.close()
@kernel_function(description="Summarize and structure scraped Confluence content into JSON.")
async def summarize_confluence_data(
self, raw_text: Annotated\[str, "Raw text scraped from the Confluence page"\],
output_style: Annotated\[str, "Output style, either 'bullet' or 'json'"\] = "json" # Default to 'json'
) -> Annotated\[str, "Returns structured summary in JSON format"\]:
prompt = f"""
You are a Program Management Data Extractor.
Your job is to analyze the following Confluence content and produce structured machine-readable output.
Confluence Content:
{raw_text}
Instructions:
- If output_style is 'bullet', return bullet points summary.
- If output_style is 'json', return only valid JSON array by removing un printable characters and spaces from beginning and end.
- DO NOT write explanations.
- DO NOT suggest code snippets.
- DO NOT wrap JSON inside triple backticks \`\`\`json
- Output ONLY the pure JSON array or bullet points list.
Output_style: {output_style}
"""
# Call OpenAI again
completion = await client.chat.completions.create(
model="gpt-4o",
messages=\[
{"role": "system", "content": "You are a helpful Program Management Data Extractor."},
{"role": "user", "content": prompt}
\],
temperature=0.1
)
structured_json = completion.choices\[0\].message.content.strip()
return structured_json
# —- Download applications for applications interface — Load_dotenv (client = asyncazureopenai (azure_endpoint = “<>” “” API_KEY = OS. ” Chat_completion_Service = OpenAICHATCOLTIONTIONC_CLIENT = Customer)
Agent_instructions = “” “You are a useful agent for program management that can help extract major information such as team member, features and maleum from the meeting page.
Important: When users determine a team page, only the features and files of this team are extracted.
When you start the conversation, give yourself this message: “Hello! I’m the Prime Minister. I can help get a state of features and epic.
Steps to be followed: 1. Always, call first `Get_confluence_page_Conten ‘to scrape the meeting page.
- If the user message starts with “Team: {team_name}.” Use this {team_name} to get a tick_name’s intermediary. For example, if the inputs “team: raptor. What are the latest features?” , “Team_name` is“ RAPTOR ”2. If the user requests a summary, then save the lead points menu. If the user requests the JSON group, a scheme or a plot. Then immediately call` Suffarize_confluence_data ‘using broken content. The user did not specify the output pattern, so do the default storage to the POINT summary.
Instructions: – If Output_style is a “bullet”, then the summary of the lead points. – If Output_style is “JSON”, then return Json Safin Al -Saleh by removing non -printable letters and spaces from the beginning and the end. Do not write explanations. – Do not suggest code scraps. Json does not overcome in Triple BackTicks `Json – Just comes out of the pure JSON points or lead points.
What is the team that is interested in helping you in planning today? “
Always give the priority of user preferences. If they mention a specific team, focus your data on this team instead of suggesting alternatives. “Agent = chatCCCOCCOCOLTIONGENTANT (SERVICE = Chat_completion_service, plugins =[ ConfluencePlugin() ]Name = “Confluenceage”, Instructions = Agent_instructions)
# —- The main Async logic —- Async Def Defreprons [] FULL_RESPONSE = [] function_calls = [] Parsed_Json_result = There is no completion = Await Client.chat.completions.create (Model = “GPT-4O”, Messenger =[ {“role”: “system”, “content”: “You are a Judge of the content of user input. Anlyze the user’s input. If it asking to scrap internal COnfluence Page for a team then it is related to Program Management. If it is not related to Program Management, provide the reply but add ‘False|’ to the response. If it is related to Program Management, add ‘True|’ to the response.”}, {“role”: “user”, “content”: user_input} ]Term = 0.5) _Text = Finish.[0].Mssage.CONTENT.STRIP () Print (“text of response:”, repunse_text) if the response is _Text.startswith (“” FALSE | “[6:] Return to response _Text, interconnected indicator, [] # Return the response without any further process[5:]Function_calls = Re.findall (R “(\ W+\ (.*\)),” Reponse_text) # Remove the job calls from the text of the response to the contact in the function _calls: desponse_text = response_text.replace (Call, “”
async for response in agent.invoke_stream(messages=user_input, thread=thread):
print("Response:", response)
thread = response.thread
agent_name = response.name
for item in list(response.items):
if isinstance(item, FunctionCallContent):
pass # You can ignore this now
elif isinstance(item, FunctionResultContent):
if item.name == "summarize_confluence_data":
raw_content = item.result
extracted_json = extract_json_from_response(raw_content)
if extracted_json:
try:
parsed_json = json.loads(extracted_json)
yield parsed_json, thread, function_calls
except Exception as e:
st.error(f"Failed to parse extracted JSON: {e}")
else:
full_response.append(raw_content)
else:
full_response.append(item.result)
elif isinstance(item, StreamingTextContent) and item.text:
full_response.append(item.text)
#print("Full Response:", full_response)
# After loop ends, yield final result
if parsed_json_result:
yield parsed_json_result, thread, function_calls
else:
yield ''.join(full_response), thread, function_calls
# —- Stipit UI SETUP —- St.Set_page_config (leatout = “wide”) left_col, right_col = st.columns ([1, 1]ST.MARKDOWN (“” “” “” Unfafe_Allow_html = True ” # —- App Main Sperteit App —- With Left_col: St.title ( Enabler AI”) St.write (“Ask me about a different Wiley program.
if "history" not in st.session_state:
st.session_state.history = \[\]
if "thread" not in st.session_state:
st.session_state.thread = None
if "charts" not in st.session_state:
st.session_state.charts = \[\] # Each entry: {"df": ..., "title": ..., "question": ...}
if "chart_dataframes" not in st.session_state:
st.session_state.chart_dataframes = \[\]
if st.button("
Clear Chat"):
st.session_state.history = \[\]
st.session_state.thread = None
st.rerun()
# Input box at the top
user_input = st.chat_input("Ask me about your team's features...")
# Example:
team_selected = st.session_state.get("selected_team")
if st.session_state.get("selected_team") and user_input:
user_input = f"Team: {st.session_state.get('selected_team')}. {user_input}"
# Preserve chat history when program or team is selected
if user_input and not st.session_state.get("selected_team_changed", False):
st.session_state.selected_team_changed = False
if user_input:
df = pd.DataFrame()
full_response_holder = {"text": "","df": None}
with st.chat_message("assistant"):
response_container = st.empty()
assistant_text = ""
try:
chat_index = len(st.session_state.history)
response_gen = stream_response(user_input, st.session_state.thread)
print("Response generator started",response_gen)
async def process_stream():
async for update in response_gen:
nonlocal_thread = st.session_state.thread
if len(update) == 3:
content, nonlocal_thread, function_calls = update
full_response_holder\["text"\] = content
if isinstance(content, list):
data = json.loads(re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","")))
df = pd.DataFrame(data)
df.columns = df.columns.str.lower()
print("\\n
Features Status Chart")
st.subheader("
Features Status Chart")
plot_bar_chart(df)
st.subheader("
Detailed Features Table")
st.dataframe(df)
chart_df.columns = chart_df.columns.str.lower()
full_response_holder\["df"\] = chart_df
elif (re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","").replace(" ",""))\[0\] =="\[" and re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","").replace(" ",""))\[-1\] == "\]"):
data = json.loads(re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","")))
df = pd.DataFrame(data)
df.columns = df.columns.str.lower()
chart_df = pd.DataFrame(data)
chart_df.columns = chart_df.columns.str.lower()
full_response_holder\["df"\] = chart_df
else:
if function_calls:
st.markdown("\\n".join(function_calls))
flagtext = 'text'
st.session_state.thread = nonlocal_thread
try:
with st.spinner("
AI is thinking..."):
flagtext = None
# Run the async function to process the stream
asyncio.run(process_stream())
# Update history with the assistant's response
if full_response_holder\["df"\] is not None and flagtext is None:
st.session_state.chart_dataframes.append({
"question": user_input,
"data": full_response_holder\["df"\],
"type": "chart"
})
elif full_response_holder\["text"\].strip():
# Text-type response
st.session_state.history.append({
"user": user_input,
"assistant": full_response_holder\["text"\],
"type": "text"
})
flagtext = None
except Exception as e:
error_msg = f"
Error: {e}"
response_container.markdown(error_msg)
if chat_index > 0 and "Error" in full_response_holder\["text"\]:
# Remove the last message only if it was an error
st.session_state.history.pop(chat_index)
# Handle any exceptions that occur during the async call
except Exception as e:
full_response_holder\["text"\] = f"
Error: {e}"
response_container.markdown(full_response_holder\["text"\])
chat_index = len(st.session_state.history)
#for item in st.session_state.history\[:-1\]:
for item in reversed(st.session_state.history):
if item\["type"\] == "text":
with st.chat_message("user"):
st.markdown(item\["user"\])
with st.chat_message("assistant"):
st.markdown(item\["assistant"\])
With Right_Col: St.title (“Select Wiley PROGRAM”)
team_list = {
"Program 1": \["Team 1", "Team 2", "Team 3"\],
"Program 2": \["Team 4", "Team 5", "Team 6"\]
}
selected_program = st.selectbox("Select the Program:", \["No selection"\] + list(team_list.keys()), key="program_selectbox")
selected_team = st.selectbox("Select the Agile Team:", \["No selection"\] + team_list.get(selected_program, \[\]), key="team_selectbox")
st.session_state\["selected_team"\] = selected_team if selected_team != "No selection" else None
if st.button("
Clear All Charts"):
st.session_state.chart_dataframes = \[\]
chart_idx = 1
#if len(st.session_state.chart_dataframes) == 1:
for idx, item in enumerate(st.session_state.chart_dataframes):
#for idx, item in enumerate(st.session_state.chart_dataframes):
st.markdown(f"\*\*Chart {idx + 1}: {item\['question'\]}\*\*")
st.subheader("
Features Status Chart")
plot_bar_chart(item\["data"\])
st.subheader("
Detailed Features Table")
st.dataframe(item\["data"\])
chart_idx += 1
the Managing programs based on patch from artificial intelligence Chatbot The teams help to track the features of the project and the epic of meeting pages. Use the app Consumer agents of the nucleus With Openai GPT-4O to scrape the team’s meeting content using use playwright. The state is used for approval. The tool allows the selection of the program and the relevant team, and based on the user’s inputs. With the AIGNIC AI feature, we can enable LLM to be a real personal assistant. It can be strong in reducing LLM access to data, but still benefits from the LLM feature of restricted data. It is an example to understand the AIC AI feature and how it can be its power.