Как ИИ революционизирует управление программами Agile с Confluence & Streatlit

Как ИИ революционизирует управление программами Agile с Confluence & Streatlit

15 июня 2025 г.

Problem Statement:

С помощью Copilot, интегрированного в приложения организации, поиск редко используемых данных из файлов, SharePoint и других доступных источников стало невероятно простым. Я в значительной степени полагался на эту возможность искусственного интеллекта. Однажды мне понадобился краткий обзор всех функций (результаты команды за четверть в Agile Framework) и их статусы, над которыми работает команда. К сожалению, Copilot отрицал данные чтения со страницы слияния, что является идеальным и ожидаемым. Многие организации, проекты и обновления программ хранятся на страницах слияния. Получение обзора командных целей, результатов, рисков проекта и статуса может быть трудоемким для лидера или человека, занимающегося несколькими программами. Я подумал, почему бы не иметь интеллектуального помощника, чтобы получить и суммировать эту информацию? Это будет эффективным фактором для менеджера программы и старшего руководства.

Solution: AI Agentic Assistant Powered by Streamlit + Semantic Kernel

Внедрение агентского ИИ было для меня спасителем, и я решил выбрать эту структуру в качестве решения. Тем не менее, были проблемы: какую структуру следует использовать, и есть ли доступный открытый исходный код? Насколько дорогой будет управляемая платформа? Наконец, со всеми исследованиями я решил пойти с открытым исходным кодом и использовать приведенный ниже технический стек, чтобы создать легкого помощника ИИ, использующего:

  • Потокдля передней части,
  • Семантическое ядродля быстрого управления и цепочки,
  • Azure openaiДля обработки естественного языка,
  • ДраматургДля безопасного и динамического сетевого соскоба страниц слияния.

Что он делает

Этот инструмент позволит менеджерам и лидерам программ:

  • ВыберитеНазвание команды или программыИз выпадения,

  • Автоматически извлекать связанный URL -адрес страницы слияния,

  • Секция контента Crape Key с этой страницы (например, функции, эпохи, зависимости, риски, члены команды),

  • Задайте такие вопросы, как «Каковы результаты команды Q4?» или «суммировать функции, основанные на статусе» и т. Д.,

  • Отображать ответы в виде суммированного текста.

How it works - Pseudo Code

Шаг 1.Поиск страницы слияния.

Вместо вручную вручную URL -адреса, каждая команда названо на карту с URL -адресом слияния с использованием словаря. Когда пользователь выбирает «команду А» из правой панели, бэкэнд автоматически приносит связанный URL -адрес и триггеры.

team_to_url_map = { "Team A": "https://confluence.company.com/display/TEAM_A",

"Team B": "https://confluence.company.com/display/TEAM_B", ... }

Шаг 2. Скрепинг веб -сайта через драматург

Этот шаг занял некоторое время. Наконец, я закончил тем, что использовал Playwright для безголовного скребки на основе браузера, который помогает нам загрузить динамическое содержание и обрабатывать вход:

Неудачный подход:-

  • []Используя библиотеку запросов Python, получите данные о слиянии с помощью API. Механизм аутентификации не был успешным. В противном случае это был бы отличный способ получить данные страницы слияния.
  • []Использование библиотеки Python Beautifysoup. Это было исключено из -за динамического содержания.
  • []Я закончил с Python Playwright. У слоя SSO были проблемы, но, наконец, он сработал после загрузки State State JSON HTML и повторного использования.

@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"]:

Шаг 3. Определите клиента, агента и быстрого разработки с помощью семантического ядра.

Инструмент предназначен для того, чтобы стать фактором управления программой. Инструкции агента составлены для использования скрещенного контента и получения результата, подходящего для вопросов PM. Инструкции помогут получить вывод в качестве текстовой резюме или в формате диаграммы. Это также пример низкого кода.

Кроме того, я определил клиента как агента искусственного интеллекта как плагин.

AGENT_INSTRUCTIONS = “““

“““

client = OpenAI(<Local open source LLM>)

chat_completion_service = OpenAIChatCompletion(ai_model_id="<>",

async_client=client )

agent = ChatCompletionAgent( service=chat_completion_service, plugins=[ ConfluencePlugin() ], name="ConfluenceAgent", instructions=AGENT_INSTRUCTIONS )

Шаг 4. Определите ввод пользователя, чтобы обработать вопрос с или без инструмента.

Я решил добавить дополнительный клиент LLM, чтобы проверить, имеет ли значение пользовательский ввод для управления программой или нет.

completion = await client.chat.completions.create( model="gpt-4o", messages=[ {"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} ], temperature=0.5 )

Step 5. The final step is to produce the result. Here is the entire code.

Вот весь код. Я удалил свои детали, специфичные для проекта. Нам нужно хранить State.json в первую очередь, чтобы использовать его в коде



import json import os import asyncio import pandas as pd import streamlit as st from typing import Annotated from dotenv import load_dotenv from openai import AsyncAzureOpenAI from playwright.async_api import async_playwright from bs4 import BeautifulSoup from semantic_kernel.functions import kernel_function from typing import Annotated import re import matplotlib.pyplot as plt от semantic_kernel.agents import chatcompletionagent from semantic_kernel.connectors.ai.open_ai import openaichatcompletion из semantic_kernel.contents importcontent, функции, функциональный

Team_url_mapping = {«Team 1»: «URL Confluence для команды 1», «Команда 2»: «URL Confluence для команды 2», «Команда 3»: «URL Confluence для команды 3», «Команда 4»: «Confluence URL для команды 4», «Команда 5»: «Contruence Url для команды 5», «Команда 6»: «Confluence URL для команды 6« Команда 6 «Команда 6« Команда 6 «Команда 6« Команда 6 «Команда 6« Команда 6 «Команда 6« Команда 6 «6».

# ---- Определение плагина ----

#BAR Диаграмма с фиксированным размером def plot_bar_chart (df): status_counts = df ["status"]. Value_counts () Fig, ax = plt.subplots (figsize = (1,5, 1)) #Ширина, высота в дюймах Ax.bar (status_counts.index, status_counts.values, #4caf50 "ax.setex. ax.set_ylabel ("count") # Изменить цветовой ax.tick_params (axis = 'x', colors = 'blue', labelrotation = 90) # x-тик в синем, вращающийся Ax.tick_params (Axis = 'y', Colors = 'green') # Y-Ticks в зеленой St.pryplot (Fig)

DEF EXTRACT_JSON_FROM_RESPONSE (TEXT): # Используйте REGEX, чтобы найти первый массив JSON в текстовом совпадении = re.search (r "(\ [\ s*{.*} \ s*\])", text, re.dotall), если совпадает: return match.group (1) note none

Class ConfluencePlugin: defинициатор(self): self.default_confluence_url = "<>" load_dotenv ()

@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

# ---- Загрузить учетные данные API ---- load_dotenv () client = asyncazureopenai (azure_endpoint = "<>", api_key = os.getenv ("azure_api_key"), api_version = '<>') cath_completion_service = openaichatcom async_client = client)

Agent_instructions = "" "Вы - полезный агент по управлению программами, который может помочь извлечь ключевую информацию, такую ​​как член команды, функции, Epics со страницы слияния.

ВАЖНО: Когда пользователи указывают страницу команды, только извлекают функции и эпосы этой команды.

Когда начнется разговор, представитесь этим сообщением: «Здравствуйте! Я ваш помощник по премьере. Я могу помочь вам получить статус функций и эпосов.

Шаги, которые вы должны следовать: 1. Всегда сначала вызовите `get_confluence_page_content`, чтобы скрепить страницу слияния.

  • Если сообщение пользователя начинается с "team: {team_name}.", Используйте это {team_name} для аргумента `team_name`. Например, если ввод - «Команда: Raptor. Каковы последние функции?», «Team_Name» - это «Raptor». 2. Если пользователь просит краткую информацию, предоставьте список пулевых баллов. 3. Если пользователь запрашивает массив JSON, диаграмму или сюжет. Затем немедленно вызовите `Summarize_confluence_data` с помощью сохраненного контента. 4. На основании стиля вывода, запрашиваемого пользователем, верните либо массив JSON, либо точки пуль. 5. Если пользователь не указывает стиль вывода, по умолчанию сводку с пулевой точкой. 6. Если пользователь запрашивает массив JSON, верните только действительный JSON и сюжетный график/график.

Инструкции: - Если output_style - это «пуля», вернуть пуль -баллы. - Если output_style - это «json», верните только действительный массив JSON, удалив неватные символы и пространства с начала и конца. - Не пишите объяснения. - Не предлагайте фрагменты кода. - Не оберните JSON в Triple Backticks `` `json - выводит только список чистого массива JSON или пуль.

Какую команду вы заинтересованы, чтобы помочь вам планировать сегодня? "

Всегда расставляйте приоритеты пользовательских предпочтений. Если они упоминают конкретную команду, сосредоточьте свои данные на этой команде, а не на предложении альтернатив. "" "Agent = ChatCompletionAGENT (service = CHAT_COMPLETION_SERVICE, PLUGINS = [confluencePlugin ()], name =" confluenceagent ", инструкции = Agent_instructions)

# ---- Main Async Logic ---- Async def Stream_response (user_input, thread = none): html_blocks = [] full_response = [] function_calls = [] parsed_json_result = none opplion = await client.chat.completions.create (model = "gpt-4o", a await.chat.completions.create (model = "gpt-4o", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ",", ". «Вы являетесь судьей содержания пользовательского ввода. opplion.choices [0] .message.content.strip () print («Текст ответа:», response_text) if response_text.startswith ("false |"): response_text = response_text [6:] witch response_text, thread, [] # return response без какого -либо дальнейшего обработки response_text. re.findall (r "(\ w+\ (.*? \))", response_text) # Удалить вызовы функции из текста ответа для вызова в функции_каллов: response_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

# ---- STREADLIT UI SETUP ---- st.set_page_config (layout = "wide") Leat_col, right_col = st.columns ([1, 1]) St.markdown ("" <style> html, тело, [класс*= "css"] {font-size: 12px! ---- Основное приложение для потокового часа ---- с левой_кол: St.title («💬 💬 управление программами Ai Ai») St.Write («Спросите меня о различных коммерческих предметах Wiley.!») St.Write («Я могу помочь вам получить статус функций и эпосов».

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"\])

с right_col: St.title («Выберите программу Wiley»)

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

Заключение

ААйбот управления программами на основе потоковПомогает командам отслеживать функции проекта и эпосии со страниц слияния. Приложение используетСемантические агенты ядрас OpenAI GPT-4O, чтобы соскребить контент страницы слияния команды, используя страницу с использованиемДраматургПолем Государство используется для аутентификации. Инструмент позволяет выбирать программу и соответствующую команду, и, основываясь на выборе, вход пользователя будет отвечать. С помощью агентской функции ИИ мы можем позволить LLM быть настоящим личным помощником. Это может быть мощным в ограничении доступа LLM к данным, но все же использовать функцию LLM ограниченных данных. Это пример, чтобы понять агентскую функцию ИИ и насколько мощной она может быть.

Ссылка:https://github.com/microsoft/ai-agents-for-beginners?tab=readme-ov-file

              Playwright documentation. 


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE