From bedfda04a9c59ac925ed6b5687e29fb45c3c3e79 Mon Sep 17 00:00:00 2001 From: Lance Herman Date: Tue, 21 Oct 2025 05:49:00 +0000 Subject: [PATCH 1/3] Implement concurrent agent execution --- backend/__init__.py | 0 backend/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 169 bytes .../app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 173 bytes backend/app/api/v1/routes.py | 27 +++++++- .../__pycache__/orchestrator.cpython-313.pyc | Bin 0 -> 2903 bytes backend/app/core/orchestrator.py | 40 +++++++++++ .../__pycache__/report_models.cpython-313.pyc | Bin 0 -> 866 bytes .../report_service.cpython-313.pyc | Bin 0 -> 1314 bytes backend/app/services/report_service.py | 11 ++- .../__pycache__/id_generator.cpython-313.pyc | Bin 0 -> 551 bytes backend/main.py | 8 +-- ..._orchestrator.cpython-313-pytest-8.4.2.pyc | Bin 0 -> 7204 bytes backend/tests/test_orchestrator.py | 63 ++++++++++++++++++ main.py | 15 ----- 14 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 backend/__init__.py create mode 100644 backend/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/app/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/app/core/__pycache__/orchestrator.cpython-313.pyc create mode 100644 backend/app/core/orchestrator.py create mode 100644 backend/app/models/__pycache__/report_models.cpython-313.pyc create mode 100644 backend/app/services/__pycache__/report_service.cpython-313.pyc create mode 100644 backend/app/utils/__pycache__/id_generator.cpython-313.pyc create mode 100644 backend/tests/__pycache__/test_orchestrator.cpython-313-pytest-8.4.2.pyc create mode 100644 backend/tests/test_orchestrator.py diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/__pycache__/__init__.cpython-313.pyc b/backend/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..936f948a2b9a9d6f3abb9d783c8b7cff7e5c2689 GIT binary patch literal 169 zcmey&%ge<81ka1UXMpI(AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl;?{fzwFRQ;mV zg8bsllKi5~)M9;~(%j6vlGGf>yu_T!lFZ~{edmnC%)B6=nxYb2#{f_Lq{QUx)Vvh^ r`1s7c%#!$cy@JYH95%W6DWy57c15f}gFzM-gBTx~85tRin1L(+wwo*A literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/__init__.cpython-313.pyc b/backend/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1063ede4b801af74c975043cb5d7e485e89945e9 GIT binary patch literal 173 zcmey&%ge<81Q{njWPs?$AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl<7{fzwFRQ;mV zg8bsllKi5~)M9;~(%j6vlGGf>yu_T!lFZ~{edmnC%)B6=nxYb2#{f_Lq{QUx)Vvh^ v#DW6-`1s7c%#!$cy@JYH95%W6DWy57c15f}!$Fo8gBTx~85tRin1L(+V=*ly literal 0 HcmV?d00001 diff --git a/backend/app/api/v1/routes.py b/backend/app/api/v1/routes.py index 9c70525..d720f39 100644 --- a/backend/app/api/v1/routes.py +++ b/backend/app/api/v1/routes.py @@ -1,13 +1,34 @@ from fastapi import APIRouter -from app.models.report_models import ReportRequest, ReportResponse -from app.services.report_service import generate_report +from backend.app.models.report_models import ReportRequest, ReportResponse +from backend.app.services.report_service import generate_report +from backend.app.core.orchestrator import orchestrator +import asyncio router = APIRouter() +# Dummy Agent for demonstration +async def dummy_agent_one(report_id: str, token_id: str) -> dict: + print(f"Dummy Agent One running for report {report_id} and token {token_id}") + await asyncio.sleep(1) # Simulate async work + return {"agent_one_data": "data_from_agent_one"} + +async def dummy_agent_two(report_id: str, token_id: str) -> dict: + print(f"Dummy Agent Two running for report {report_id} and token {token_id}") + await asyncio.sleep(0.5) # Simulate async work + return {"agent_two_data": "data_from_agent_two"} + +# Register agents +orchestrator.register_agent("AgentOne", dummy_agent_one) +orchestrator.register_agent("AgentTwo", dummy_agent_two) + @router.get("/") async def read_root(): return {"message": "Welcome to API v1"} @router.post("/report/generate", response_model=ReportResponse) async def generate_report_endpoint(request: ReportRequest): - return await generate_report(request) + report_response = await generate_report(request) + report_id = report_response.report_id + # Execute agents concurrently + await orchestrator.execute_agents_concurrently(report_id, request.token_id) + return report_response diff --git a/backend/app/core/__pycache__/orchestrator.cpython-313.pyc b/backend/app/core/__pycache__/orchestrator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3984733481a531fbe7a6b62b79e95fb6ebc78ba1 GIT binary patch literal 2903 zcmaJ@O>7&-6`t82a!Ia!Oj)v|B+^<^s?b(RJNk1C*^MG6v1D6qwWY&^EVk?AlG=n? zsxwQ+a*O_?L7^fATA)>-=29JckZ(pW^-)1C4%!1*yOugtQ3E~sCRGO9qCND@l50z9 zngRCBd-L<=y*KZBvwKZVQ3UN6eKP-P2%*1oA!t&A*_#Ds1?fn~mk{B-a0$-{M3@nY zI3p2B<746_c_u_c8tO!SNSC^iE{_Uz4J4ck_o2~-I-(SDBJ_w$vk8$&7aYrD%9L3u znb%4-i%h;}Iv!^en6*;#x@{19u}nN8Z+fQHP~vyK>A>xM0AvN_5WzYkf{ux(3*CrF zx|ox7DHqDgIXNZd!Z}5Tl_P>rC0*`D??@z~!@M5O$s>}k_#@HJ+ZU|XBRORxsz?1D zF+E1qqMB%8>Q{(0Z&Qz$UYR`N2U!?M(0D{d`1ayJR?sPwMx|g5711p`dkeE8#4bA2 zvx%KI%%bgj6ha|UyHucDm&Vem`SOCD;&Gu4oa5Lub-A+OxSm~_bj{L|=U6l~HE%lZ zoFBU{PJVSdbAV@_-V$cy4NJ1pd zoNwdgC=kk(XqrY^pl8iNkm?~~&mA!N;ADjFZUq8{51!ssdTL6~s`p1_fbeTzkw$_! zd8T+}h>_vs4|u@${}8^thYwIKi7(8Y0tDv$|_SJToQ zPmx0{Esr*GMnh@wGgqLw@S#m0X0B%9qYczP({SZW2*tqWtP<>)3xE?0K;H)^=~x#| z;W0c7c%BCjuHzMPR#uT4K80K*+Y~HMhXd{4%f*H-A7!SwD{d^yug7ocXqE@vZ$T}1 zsUe&F2xa-z`b`(pv1X_m>1Z<=mICciSLAGauqGW1#`REA$b882<;o;NdN>``m8T;! z4R4PH`!bz+SdS!yz=FfxX^?dan)TodkBbGIh|Y2ZdKKat_6^Hk^qjIwzkV}3$)TtX z-fSP#3Z_%C^V)lkH?P@*l*w6*h3bO|frY4NdKJoKe~Lw{^1@=t_UwE@ATNMfOyZ>s z7BFHtCXUIb{|qn%=xk zSj2WK3pNxUfTBYk7p!nCD5$RmPB3|qK#gD`lqbT$42Ss;>ja*3@nd6ki#(IQO9O#} zn?8df3pea#akE$ipuvH8sL{R;3_C1T=Yx9SZ|8sp=ss#Xrm0hbs z2!t8@d+7k6KW8P^O?QDj;`r|9T#otVp*FCojn%ZVO>Ls4O+3)f*VN~iFKnsDHr1Y* z3I*W4I>aK3j^57Sjoj_~qsB z!xwt1O80hG-`eo=W16NAc7N>Ep4p^Lu4`oLFA6N!BRHWl53p?s9j7)%fk!uL7z^cm zrNnvFFuqkWOO2Tp!zeg}dL_rT-Lhd2URKFTPI$Q|{89;zKk49Pm=oXocEjMc$gvF5 z^N4c|utp8z7V_V{dXyBtm=msq{shR6&_4t$U&T8iEWf-qyn~>-eesU9F?{FkyIocN z%};x)=Q6dCw;uDEolu)RkMF=T-rwzi?Dsn-WI4V*wS%C$li8qmr#9ZH&Sk5)Z`S6_ z>g#6h+% Dict: + try: + return await agent_func(report_id, token_id) + except Exception as e: + print(f"Error running agent '{name}': {e}") + raise # Re-raise to be caught by asyncio.gather + +orchestrator = Orchestrator() diff --git a/backend/app/models/__pycache__/report_models.cpython-313.pyc b/backend/app/models/__pycache__/report_models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce396ee95276102022465cf8298873c4f12a91f5 GIT binary patch literal 866 zcmZ`%&5P4O6rV}bG_Cz|-4#)hRjE=zbJ3d!s~30It+Kf&6ozaww!t*XzL_3+6BhJZ z4-5Va-u!E19oUnny$N0KzL&J1pab*E`s z5A)o|hGn9P44|of)E?wBaH$mnYDaA#d}JrBi${AWH@ok5=HBP^PpxlG7cQ;h{=J5K z=>Gp2iiOw(`3|_F3X<5wr<9 z1ZxCDSJ~$}NtahZsTDoN$=Yj2{gX!f!)_T!F@5aD%V$xTB8fmhmT~ngkJC}HqLQH^ z+1~|>_qhBEp1^f{#PO=z2tRGk`6;_`%C4O|7QZv!K4SRn@qC0J==GnGho zmNMX+g7yW{LjfOkY;S%P2~J>Rk)Vg3a#LXkMQ@#@C`IT1oWC8;&dfJ^v9z>6kc#}D zgHx4|zoaoGEm1C>qVgNrAU3h-E~!zVH3bw?+RCn4(?E-LbvIShK{v@~1kx?NWwi7y zC58XCGPZUTrJZU~TW={_nw`Ezo@%gQ8!f}mV4P+z#Gb|dMJxMOUTi61sm{UxqB{Gj z&v+zqQOS)sxDj(8j&!^ZdbJ&|9f=jscY3TBz)^C@MH-S!IFrz#+*R!bXs#>n>MJel(nWlbvCG+Lo@T&MWXfw_|f8WxC5d}XoAKOB|3vQ1^47kvFS2l4w;n# z*$ZSPTaqj-nXJ!Jz>vC(<+2Ua6wS&(&|?*uihEH29^;iq{hsGXtozV+yGN1N=9TRO z*YhXy|McOv)yfZU8w=}H+%T+g28SNj9Or<@N$hfXB$odN5Uk*(vIM@3>@9gtUgtkP z&6l26$2wX6=xnVtS}UCwuAdb)M}^Jv)#CYj@$XEkq`%bGUw9+Uyv&m9wL$fyJ6hZx zYTN&E+1Af@@6os?p>^EIZO;QVgEhp!39Jp$M14Z{QMgwTlBt6_|1tp-(0xUQ-9aX2e4fs~T-!KJ zZ@f0H9$O=$bY|Qd8Ml7ld1ZWlsgq1@n7=Wo4(<=j)nTso%GgcDx^;){Sy^EwV3!f> zaxdtxE-xpWH?cqtD_$^BoR95C9)@}yC)t7k;>{F#bQEH{AfLsgSCvN~mL13SaV~D; z;rn)+cv<3Z{va0^N+t~(U*WC&F8dma=#z->IWH3b4XTR wY#pn|-yLUu{pOV19BUc6LkADWM9P!zFJn0_r063$Xpf1MCu{Fw3D+_0e`q^9kN^Mx literal 0 HcmV?d00001 diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index 00d6cf3..a33a841 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -1,5 +1,5 @@ -from app.models.report_models import ReportRequest, ReportResponse -from app.utils.id_generator import generate_report_id +from backend.app.models.report_models import ReportRequest, ReportResponse +from backend.app.utils.id_generator import generate_report_id from typing import Dict # In-memory storage for reports (to be replaced with persistent storage) @@ -15,3 +15,10 @@ async def generate_report(request: ReportRequest) -> ReportResponse: "report_id": report_id } return ReportResponse(report_id=report_id, status="processing") + +async def save_report_data(report_id: str, data: Dict): + if report_id in in_memory_reports: + in_memory_reports[report_id].update(data) + else: + # Handle case where report_id does not exist, or log a warning + print(f"Warning: Report ID {report_id} not found for saving data.") diff --git a/backend/app/utils/__pycache__/id_generator.cpython-313.pyc b/backend/app/utils/__pycache__/id_generator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0efe7d5fccc6900f2ffd22800afc1ae86f380db GIT binary patch literal 551 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 literal 0 HcmV?d00001 diff --git a/backend/tests/test_orchestrator.py b/backend/tests/test_orchestrator.py new file mode 100644 index 0000000..396293d --- /dev/null +++ b/backend/tests/test_orchestrator.py @@ -0,0 +1,63 @@ +import pytest +import asyncio +from unittest.mock import AsyncMock, patch +from backend.app.core.orchestrator import Orchestrator +from backend.app.services.report_service import in_memory_reports + +@pytest.fixture(autouse=True) +def clear_in_memory_reports(): + in_memory_reports.clear() + yield + in_memory_reports.clear() + +@pytest.mark.asyncio +async def test_execute_agents_concurrently_success(): + orchestrator = Orchestrator() + mock_agent_one = AsyncMock(return_value={"agent_one_result": "data1"}) + mock_agent_two = AsyncMock(return_value={"agent_two_result": "data2"}) + + orchestrator.register_agent("AgentOne", mock_agent_one) + orchestrator.register_agent("AgentTwo", mock_agent_two) + + report_id = "test_report_id_success" + token_id = "test_token_id" + + # Initialize report in in_memory_reports as generate_report would + in_memory_reports[report_id] = {"token_id": token_id, "status": "processing"} + + await orchestrator.execute_agents_concurrently(report_id, token_id) + + mock_agent_one.assert_called_once_with(report_id, token_id) + mock_agent_two.assert_called_once_with(report_id, token_id) + + assert in_memory_reports[report_id]["status"] == "completed" + assert "agent_results" in in_memory_reports[report_id] + assert in_memory_reports[report_id]["agent_results"]["AgentOne"] == {"status": "completed", "data": {"agent_one_result": "data1"}} + assert in_memory_reports[report_id]["agent_results"]["AgentTwo"] == {"status": "completed", "data": {"agent_two_result": "data2"}} + +@pytest.mark.asyncio +async def test_execute_agents_concurrently_with_failure(): + orchestrator = Orchestrator() + mock_agent_one = AsyncMock(return_value={"agent_one_result": "data1"}) + mock_agent_failing = AsyncMock(side_effect=Exception("Agent failed")) + + orchestrator.register_agent("AgentOne", mock_agent_one) + orchestrator.register_agent("AgentFailing", mock_agent_failing) + + report_id = "test_report_id_failure" + token_id = "test_token_id" + + # Initialize report in in_memory_reports as generate_report would + in_memory_reports[report_id] = {"token_id": token_id, "status": "processing"} + + await orchestrator.execute_agents_concurrently(report_id, token_id) + + mock_agent_one.assert_called_once_with(report_id, token_id) + mock_agent_failing.assert_called_once_with(report_id, token_id) + + assert in_memory_reports[report_id]["status"] == "completed" + assert "agent_results" in in_memory_reports[report_id] + assert in_memory_reports[report_id]["agent_results"]["AgentOne"] == {"status": "completed", "data": {"agent_one_result": "data1"}} + assert in_memory_reports[report_id]["agent_results"]["AgentFailing"]["status"] == "failed" + assert "error" in in_memory_reports[report_id]["agent_results"]["AgentFailing"] + assert "Agent failed" in in_memory_reports[report_id]["agent_results"]["AgentFailing"]["error"] diff --git a/main.py b/main.py index b16ac46..e69de29 100644 --- a/main.py +++ b/main.py @@ -1,15 +0,0 @@ -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root():s - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} \ No newline at end of file From 2b7099f74a0fbb3806fde501260e4461caa41fcd Mon Sep 17 00:00:00 2001 From: Lance Herman Date: Tue, 21 Oct 2025 06:02:48 +0000 Subject: [PATCH 2/3] Fix: API orchestration race condition, logging, and dotenv loading --- backend/app/api/v1/routes.py | 24 ++++++++++++++++++---- backend/app/core/orchestrator.py | 28 ++++++++++++++++++++------ backend/app/services/report_service.py | 5 ++++- backend/main.py | 8 ++++---- backend/tests/test_orchestrator.py | 3 +-- 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/backend/app/api/v1/routes.py b/backend/app/api/v1/routes.py index d720f39..15712c9 100644 --- a/backend/app/api/v1/routes.py +++ b/backend/app/api/v1/routes.py @@ -1,9 +1,12 @@ -from fastapi import APIRouter +import logging +from fastapi import APIRouter, HTTPException from backend.app.models.report_models import ReportRequest, ReportResponse -from backend.app.services.report_service import generate_report +from backend.app.services.report_service import generate_report, in_memory_reports from backend.app.core.orchestrator import orchestrator import asyncio +logger = logging.getLogger(__name__) + router = APIRouter() # Dummy Agent for demonstration @@ -29,6 +32,19 @@ async def read_root(): async def generate_report_endpoint(request: ReportRequest): report_response = await generate_report(request) report_id = report_response.report_id - # Execute agents concurrently - await orchestrator.execute_agents_concurrently(report_id, request.token_id) + # Execute agents concurrently in a background task + task = asyncio.create_task(orchestrator.execute_agents_concurrently(report_id, request.token_id)) + def _on_done(t: asyncio.Task): + try: + t.result() + except Exception as e: + logger.exception('Background orchestration failed for %s: %s', report_id, e) + # Optionally update report status to failed here as well + task.add_done_callback(_on_done) return report_response + +@router.get('/report/{report_id}/status') +async def get_report_status(report_id: str): + if report_id not in in_memory_reports: + raise HTTPException(status_code=404, detail='Report not found') + return in_memory_reports[report_id] diff --git a/backend/app/core/orchestrator.py b/backend/app/core/orchestrator.py index ea492ee..a3baf2e 100644 --- a/backend/app/core/orchestrator.py +++ b/backend/app/core/orchestrator.py @@ -1,7 +1,10 @@ import asyncio +import logging from typing import Dict, Callable, Awaitable from backend.app.services.report_service import save_report_data +logger = logging.getLogger(__name__) + class Orchestrator: def __init__(self): self.registered_agents: Dict[str, Callable[[str, str], Awaitable[Dict]]] = {} @@ -23,18 +26,31 @@ async def execute_agents_concurrently(self, report_id: str, token_id: str): for i, result in enumerate(results): agent_name = agent_names[i] if isinstance(result, Exception): - print(f"Agent '{agent_name}' failed with error: {result}") - aggregated_results[agent_name] = {"status": "failed", "error": str(result)} + logger.error("Agent '%s' failed with error: %s", agent_name, result, exc_info=isinstance(result, BaseException)) + aggregated_results[agent_name] = {'status': 'failed', 'error': str(result)} else: - aggregated_results[agent_name] = {"status": "completed", "data": result} - - await save_report_data(report_id, {"agent_results": aggregated_results, "status": "completed"}) + aggregated_results[agent_name] = {'status': 'completed', 'data': result} + + failed_count = sum(1 for r in aggregated_results.values() if r['status'] == 'failed') + total = len(aggregated_results) + if failed_count == total: + overall_status = 'failed' + elif failed_count > 0: + overall_status = 'partial_success' + else: + overall_status = 'completed' + + await save_report_data(report_id, { + 'agent_results': aggregated_results, + 'status': overall_status, + 'summary': {'total': total, 'success': total - failed_count, 'failed': failed_count} + }) async def _run_agent_safely(self, name: str, agent_func: Callable[[str, str], Awaitable[Dict]], report_id: str, token_id: str) -> Dict: try: return await agent_func(report_id, token_id) except Exception as e: - print(f"Error running agent '{name}': {e}") + logger.error("Error running agent '%s': %s", name, e, exc_info=True) raise # Re-raise to be caught by asyncio.gather orchestrator = Orchestrator() diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index a33a841..ba6617e 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -1,7 +1,10 @@ +import logging from backend.app.models.report_models import ReportRequest, ReportResponse from backend.app.utils.id_generator import generate_report_id from typing import Dict +logger = logging.getLogger(__name__) + # In-memory storage for reports (to be replaced with persistent storage) in_memory_reports: Dict[str, Dict] = {} @@ -21,4 +24,4 @@ async def save_report_data(report_id: str, data: Dict): in_memory_reports[report_id].update(data) else: # Handle case where report_id does not exist, or log a warning - print(f"Warning: Report ID {report_id} not found for saving data.") + logger.warning("Report ID %s not found for saving data.", report_id) diff --git a/backend/main.py b/backend/main.py index 8babd30..73fc941 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,11 +1,11 @@ -from fastapi import FastAPI -from app.core.config import settings -from app.api.v1.routes import router as v1_router - from dotenv import load_dotenv load_dotenv() +from fastapi import FastAPI +from app.core.config import settings +from app.api.v1.routes import router as v1_router + app = FastAPI(title=settings.APP_NAME, debug=settings.DEBUG) app.include_router(v1_router, prefix="/api/v1") diff --git a/backend/tests/test_orchestrator.py b/backend/tests/test_orchestrator.py index 396293d..89bc603 100644 --- a/backend/tests/test_orchestrator.py +++ b/backend/tests/test_orchestrator.py @@ -1,6 +1,5 @@ import pytest -import asyncio -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from backend.app.core.orchestrator import Orchestrator from backend.app.services.report_service import in_memory_reports From 0f728881798bb8156e284b73094c0ea9f607ddcc Mon Sep 17 00:00:00 2001 From: Lance Herman Date: Tue, 21 Oct 2025 06:15:50 +0000 Subject: [PATCH 3/3] Fix: Adjust orchestrator test to expect partial success status --- backend/__pycache__/__init__.cpython-313.pyc | Bin 169 -> 169 bytes .../__pycache__/orchestrator.cpython-313.pyc | Bin 2903 -> 3685 bytes .../report_service.cpython-313.pyc | Bin 1314 -> 1420 bytes ..._orchestrator.cpython-313-pytest-8.4.2.pyc | Bin 7204 -> 7197 bytes backend/tests/test_orchestrator.py | 2 +- 5 files changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/__pycache__/__init__.cpython-313.pyc b/backend/__pycache__/__init__.cpython-313.pyc index 936f948a2b9a9d6f3abb9d783c8b7cff7e5c2689..e11fd64482f1d30c90491e9d03490dbc593135be 100644 GIT binary patch delta 18 YcmZ3J_2><{9 diff --git a/backend/app/core/__pycache__/orchestrator.cpython-313.pyc b/backend/app/core/__pycache__/orchestrator.cpython-313.pyc index 3984733481a531fbe7a6b62b79e95fb6ebc78ba1..c82d61354c01b5a7d7cfd557935386e1e9259a54 100644 GIT binary patch delta 1652 zcmY*ZO>7fK6rR~X@7il8PJ)v-F)V>NaRJj1LP$sx+$N>XkK1N(W#NRdau&y`v!=6a zN;nWodq~Ow=#n-)ROO0#Yo#h`r3xe}aRAB2NLX8FIkakfOO#3xqKD3G?6i#RH}k%E z@9p<~b|3kEZ}zOa-A;g4Vs2xmUAX3Hm_LuTB_IPCjsUc|Lj;iwQr{k+y%3%-2bu19?3t)3dxP<9mD7K_Ovk@ZR8{nt zGvTY8D->j=hry|^iN?xJ%vqrKP-Q z)%4@%rASKE45_VEZ<9_d>5MupeULV0Bo(17>Xlmco6zF1!&F*3oi#_;wt13+#Y`)P zk}UVtCWh&QPW)f*#bOt($`XiyTJVM!fUH*rEJkHW5}n4KA9G`kIt!NY42{(SK&qIc zZJrDw4?x?o_>07k-UK1aqQWm*5b4-;8;~SK`e_M$n7VrI4C=Rzm|t@3^WnZsHmPLv z{_xgK3pUj4Sl}Ya`zId+BR9Oa&fQMrk53eaCcf(Uq60(EbROGX5}V+sLK8$}G_9&L z3Nq44W=hW`ld7(VXp1$nhLW)yWyI!)VYfm`IE8$wp34}z&7T;}&MIgwdbsqBE+7IBXiKG9j1RV~LxsnM!7Jnt}F^ zAfD`56)73a&rpn@lboSx<7m6q6 zuZn8`cq5dVbG$U)&{XhG=7WbH1qTYjfrr6J(LY(NpIi_hSJ&q2g2n2#1-9g>FS?{s zpmi}?ayKqVita!u5MGQv4YV&rfAef#o+}0RmwdZRJA*5Ih0bGnd8$x|QGfmmAH z=JY1XBvlq^oBdI^X!3mjKKvePY=X4UUgQ5aEn_l_Z<8sR?qSUL_{M`w^nCLW8FdNe zG%c;AB)ggR`#`4uIBLd@AsQXR8|o%PGQZ{zzz*{leizq)0sW}y5ZaH`Tp3*)%{O+h z)ZIO}Fj^4bE{Wdft1Knd{G+#HvrDiae(dsrE3l5aO8bC`_3O|FK64(SZ}<+;-?2yN zALxE_P8fte=8r-H_d0o6hxu6W??9wZh?uTUib=f;dC-8)u}(5>l!$x;c}hBv`k(>a0(`_!x`to6Sz=GHO41 z7u%4xQ9q2uDh(n08F=vTSFpDL_C5hep96d#RM(UcZtT_{EjvE3$tnv~fMY7xc9 z$p;O@3-1&+mdKJAuGR+8&kc+nBogNpGnfhOrYgBt1o3j%`>LE88_r!Vf1o@C5BS~hGM!6^2 zyP+7aUZHvG{p?i*OkYcZ0Y`-qC5b0c)ICxQ5X-5MQr0>-W=%Q8a>+h4t)%i+$0CLNpbT+~bNDVd-& zMT#Y-hHZxsc{!X?^OEglHhrm%D%`N^K>J)Xt)W@vwtMz;b>qMj?TK2|_P<)0rk`A~ zvn$rmo~j+rM%u~59=q6^y^W9M41SQ(Mz5_)JY$E!tw?d zum|a9Pj@;{ZH>-#&S`V@{HFPEHMr%KH~H2Z__ZVnuHbLGo1Hw5`q4#?2G>xlcpYm% znST>$aLb*G^_7z(9#)!w@6@G6@wZF~o41GD5_PxC|JIxMSF3cw*R1m{@^= zF}%U7>OeY}EruzWJ%-tYC0Ln5iXn@oh%cBkh9{T{sE{R?8!W<~&Zx=r5@eF!#@V+R z8M!ByG39Wv0Qt>8{JC!O8>Rp!5El%B*kLRL9n2WYV9EsHD=?%pm@}s_YBKp%sRyMN z7H4{Di4Vj$ zj`;Yz#N5>Q_#)BC^(-o$AQM+IdvRD##29ZxOhPgpcU51d?V-m^|&`C8%qccyyOK zcB%hlDY{6~E$AjlM3?BP1M~6wm|<=L_l7wPg$$69X#JeNl3R23vM$jbECU0EIdG7p zqj1FXaKycXj5M zKFW+S6Q~C4;Ft7*t@J|0O7onzpI`i$^j)L%2`+d)h^_TxwT(YLnL*ZShG~jELmJ zGFxd8r^$rrO)Yh^-5qZ=>zU4*p8IW4sa~bhSg?blOpnAnUBrm^qp?InDx*JIMOg*} h5kJ83!0AJ#4^gqFu0`|MJsyCHd3>7w9(TSV&Hr!@R9651 diff --git a/backend/tests/__pycache__/test_orchestrator.cpython-313-pytest-8.4.2.pyc b/backend/tests/__pycache__/test_orchestrator.cpython-313-pytest-8.4.2.pyc index b6ca61f820bf41d24f8c56f992585df1323cc344..d8e1144b94c85f92839e238f722a6794dda49bfa 100644 GIT binary patch delta 742 zcmY+C&ui0g6u|S%(sW7MZf)15ZIout=`!a~bhgMQc(BYCcW69`P-BzE=vLh2i&sd6d)p z=BESt$3f;OZKN=N|5|F5Cvo~Dvl?1t^?kA3^=#MM(WYox(}_UOYeo)7@q;y|<86Tr zxPM>Rw4mV!EqotN3viQAK(Ns{!2Vg+Jxz_bnfhal5VFT{+uZ%|7jA~W(ryZv@(+PR BwS@ox delta 709 zcmZXS&ui0Q7{~LrP1>YQyR|lLm+I2#+|s#On6l0vun7)n*$&R5IAWW&p{^}YGFSx< z;tpOs)CatXqA)LB3yOC^|Au(zZt6ks$HAk%&jdaA96mhH_x&bMUcPx!-Y=^^R8%mCt0|Fay54>h?Re^=_^0i#^M$wQ!ofVG@qm zo~Uw9c-RU&l2{&pMf$UOF8HCknhw0Mj#&T%`oN=uD~<=nFCCiA1NQ@6M5M#HXokj9W!yp$z#jxa%> zYrs*$F@ixT5Ka)L2`33N1d~w2yINZRcVqeTZq4p_PPYT6=v=~AS|JXLyf+m%L(*A1 z(6Wn5)aryXVVQ7_aGr31!=<0V_*)Rexy+J8``tXYGB1aZqc_;l&6OBCg8LI!=1%h? zU+VQe+x7ggMe#XZU)||A9!U$^Jj=seAcV4hoQXKEn+p;>6>d1Y-VWG6$@-yf3)(=5 nz#4%*1fql(KF~9o%iFI(#@I*@_Jn)t2O&NZiuhW;86wwjCDWm@ diff --git a/backend/tests/test_orchestrator.py b/backend/tests/test_orchestrator.py index 89bc603..684c09d 100644 --- a/backend/tests/test_orchestrator.py +++ b/backend/tests/test_orchestrator.py @@ -54,7 +54,7 @@ async def test_execute_agents_concurrently_with_failure(): mock_agent_one.assert_called_once_with(report_id, token_id) mock_agent_failing.assert_called_once_with(report_id, token_id) - assert in_memory_reports[report_id]["status"] == "completed" + assert in_memory_reports[report_id]["status"] == "partial_success" assert "agent_results" in in_memory_reports[report_id] assert in_memory_reports[report_id]["agent_results"]["AgentOne"] == {"status": "completed", "data": {"agent_one_result": "data1"}} assert in_memory_reports[report_id]["agent_results"]["AgentFailing"]["status"] == "failed"