diff --git a/examples/agent/microsoft-365-agents-sdk/.env.example b/examples/agent/microsoft-365-agents-sdk/.env.example new file mode 100644 index 0000000..9328c25 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/.env.example @@ -0,0 +1,33 @@ +# --- Splunk AO: Scenario A — O11Y (lab0 / us0 / us1) --- +SPLUNK_AO_REALM=your-realm # e.g. lab0, us0, us1 +SPLUNK_AO_O11Y_TOKEN=your-o11y-ingest-token +SPLUNK_AO_O11Y_API_TOKEN=your-o11y-api-token + +# --- Splunk AO: Scenario B — Standalone (commented out) --- +# SPLUNK_AO_API_KEY=your-splunk-ao-api-key +# SPLUNK_AO_CONSOLE_URL=https://console.your-splunk-ao-instance.com + +# --- Routing --- +SPLUNK_AO_PROJECT=your-project-name +SPLUNK_AO_AGENT_STREAM=your-agent-stream-name + +# --- Azure OpenAI --- +AZURE_OPENAI_ENDPOINT=https://your-resource.cognitiveservices.azure.com/ +AZURE_OPENAI_API_KEY=your-azure-openai-api-key +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +AZURE_OPENAI_API_VERSION=2024-12-01-preview + +# --- OTel: capture LLM prompt/completion content in spans --- +OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY + +# --- Microsoft 365 Agents SDK auth --- +# Leave empty for local unauthenticated testing with npx @microsoft/m365agentsplayground. +# For Teams / Copilot, fill in credentials from your Azure App Registration: +# portal.azure.com → Entra ID → App registrations → +# Supported account types: "Accounts in any org directory + personal Microsoft accounts" +MicrosoftAppId= +MicrosoftAppPassword= +MicrosoftAppTenantId= + +# --- macOS SSL (if needed) --- +# SSL_CERT_FILE=/opt/homebrew/etc/openssl@3/cert.pem diff --git a/examples/agent/microsoft-365-agents-sdk/.npmrc b/examples/agent/microsoft-365-agents-sdk/.npmrc new file mode 100644 index 0000000..214c29d --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/examples/agent/microsoft-365-agents-sdk/app.py b/examples/agent/microsoft-365-agents-sdk/app.py new file mode 100644 index 0000000..f3e9995 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/app.py @@ -0,0 +1,147 @@ +""" +Microsoft 365 Agents SDK demo — MS A365 auto-instrumentation via OpenAIAgentsTraceInstrumentor. + +Instrumentation: + - OpenAIAgentsTraceInstrumentor hooks into OpenAI Agents SDK tracing → emits gen_ai.* spans. + - InvokeAgentScope wraps each turn manually (no auto-instrumentor for M365 turn layer yet). + - All spans → OTLP → local collector. + +Run: uv run python start_server.py +Test: npx @microsoft/m365agentsplayground +""" + +import os + +from dotenv import load_dotenv +from opentelemetry import context as otel_context + +load_dotenv(override=False) + +# --- MS A365 Observability SDK: configure first, then attach OTLP exporter --- +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +from microsoft_agents_a365.observability.core import ( + AgentDetails, + Channel, + InvokeAgentScope, + InvokeAgentScopeDetails, + Request, + SpanDetails, + configure as a365_configure, + get_tracer_provider as a365_get_tracer_provider, +) + +a365_configure(service_name="m365-splunk-ao-demo", service_namespace="splunk.ao.demo") + +_otlp_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") +a365_get_tracer_provider().add_span_processor( + BatchSpanProcessor(OTLPSpanExporter(endpoint=f"{_otlp_endpoint}/v1/traces")) +) + +# --- Auto-instrumentation: hooks into OpenAI Agents SDK tracing --- +from microsoft_agents_a365.observability.extensions.openai import OpenAIAgentsTraceInstrumentor + +OpenAIAgentsTraceInstrumentor().instrument() + +# --- OpenAI Agents SDK (wraps Azure OpenAI) --- +from agents import Agent, Runner +from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel +from openai import AsyncAzureOpenAI + +_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") +_azure_client = AsyncAzureOpenAI( + azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + api_key=os.environ["AZURE_OPENAI_API_KEY"], + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), +) +_agent = Agent( + name="QAAgent", + instructions="You are a concise, helpful assistant.", + model=OpenAIChatCompletionsModel(model=_deployment, openai_client=_azure_client), +) + +# --- Splunk AO Logger (optional — skipped without creds) --- +from splunk_ao import SplunkAOLogger + +_ao_logger: SplunkAOLogger | None = None +if os.environ.get("SPLUNK_AO_API_KEY") or os.environ.get("SPLUNK_AO_REALM"): + _ao_logger = SplunkAOLogger() +else: + print("[splunk-ao] No credentials — otel-tui-only mode.") + +# --- Microsoft 365 Agents SDK --- +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnContext, TurnState +from microsoft_agents.hosting.core.authorization import ( + AgentAuthConfiguration, + AnonymousTokenProvider, + ConnectionManager, +) + +_app_id = os.environ.get("MicrosoftAppId", "") +_app_password = os.environ.get("MicrosoftAppPassword", "") +_tenant_id = os.environ.get("MicrosoftAppTenantId", "") + +if _app_id and _app_password: + from microsoft_agents.authentication.msal import MsalConnectionManager + _connection_manager = MsalConnectionManager( + connections_configurations={ + "SERVICE_CONNECTION": AgentAuthConfiguration( + client_id=_app_id, + client_secret=_app_password, + tenant_id=_tenant_id or "common", + ) + } + ) +else: + _connection_manager = ConnectionManager( + provider_factory=lambda config: AnonymousTokenProvider(), + connections_configurations={"SERVICE_CONNECTION": AgentAuthConfiguration(anonymous_allowed=True)}, + ) + +STORAGE = MemoryStorage() +ADAPTER = CloudAdapter(connection_manager=_connection_manager) +AGENT_APP = AgentApplication[TurnState](storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState) -> None: + await context.send_activity("Hello! MS A365 auto-instrumentation demo. Ask me anything.") + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, state: TurnState) -> None: + user_text = context.activity.text or "" + if not user_text.strip(): + await context.send_activity("Please send a message.") + return + + conv_id = context.activity.conversation.id if context.activity.conversation else "unknown" + channel_id = context.activity.channel_id or "unknown" + + request = Request( + content=user_text, + session_id=conv_id, + conversation_id=conv_id, + channel=Channel(name=channel_id), + ) + agent_details = AgentDetails(agent_id="m365-splunk-ao-demo", agent_name="M365 Splunk AO Demo Agent") + + # InvokeAgentScope: manual turn-level span (no auto-instrumentor for M365 turn layer) + with InvokeAgentScope.start( + request, InvokeAgentScopeDetails(), agent_details, + span_details=SpanDetails(parent_context=otel_context.get_current()), + ): + # OpenAI Agents SDK run — auto-instrumented by OpenAIAgentsTraceInstrumentor + result = await Runner.run(_agent, input=user_text) + reply = result.final_output + + # Splunk AO Logger (optional) + if _ao_logger: + _ao_logger.start_session(name=conv_id, metadata={"channel": channel_id}) + _ao_logger.start_trace(input=user_text, metadata={"conversation_id": conv_id}) + _ao_logger.conclude(output=reply) + _ao_logger.flush() + + await context.send_activity(reply) diff --git a/examples/agent/microsoft-365-agents-sdk/app_auto_lab0.py b/examples/agent/microsoft-365-agents-sdk/app_auto_lab0.py new file mode 100644 index 0000000..27950ff --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/app_auto_lab0.py @@ -0,0 +1,140 @@ +""" +app_auto_lab0.py — Auto-instrumentation via SplunkAOSpanProcessor → Splunk AO lab0. + + OpenAIAgentsTraceInstrumentor hooks into the OpenAI Agents SDK and emits spans + automatically. SplunkAOSpanProcessor exports them to Splunk AO — no local collector + or OTEL_EXPORTER_OTLP_ENDPOINT needed. + + Prerequisites: + Copy .env.example to .env and fill in: + SPLUNK_AO_REALM, SPLUNK_AO_O11Y_TOKEN, SPLUNK_AO_PROJECT, SPLUNK_AO_AGENT_STREAM + AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY + + Run: APP_MODE=auto_lab0 uv run python start_server.py + Test: npx @microsoft/m365agentsplayground (or send a POST to /api/messages) + Find: Splunk AO → Agent Stream: microsoft-365-agents-sdk → service: m365-auto-lab0 +""" + +import os + +from dotenv import load_dotenv +from opentelemetry import context as otel_context + +load_dotenv(override=False) + +# --- MS A365 Observability SDK: configure TracerProvider first --------------- +from microsoft_agents_a365.observability.core import ( + AgentDetails, + Channel, + InvokeAgentScope, + InvokeAgentScopeDetails, + Request, + SpanDetails, + configure as a365_configure, + get_tracer_provider as a365_get_tracer_provider, +) + +a365_configure( + service_name=os.environ.get("OTEL_SERVICE_NAME", "m365-auto-lab0"), + service_namespace="splunk.ao.demo", +) + +# --- Attach SplunkAOSpanProcessor to the MS A365 TracerProvider -------------- +from splunk_ao.otel import SplunkAOSpanProcessor, add_splunk_ao_span_processor + +add_splunk_ao_span_processor( + a365_get_tracer_provider(), + SplunkAOSpanProcessor( + project=os.environ.get("SPLUNK_AO_PROJECT"), + agentstream=os.environ.get("SPLUNK_AO_AGENT_STREAM"), + ), +) + +# --- Auto-instrumentation: hooks into OpenAI Agents SDK tracing -------------- +from microsoft_agents_a365.observability.extensions.openai import OpenAIAgentsTraceInstrumentor + +OpenAIAgentsTraceInstrumentor().instrument() + +# --- OpenAI Agents SDK (wraps Azure OpenAI) ---------------------------------- +from agents import Agent, Runner +from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel +from openai import AsyncAzureOpenAI + +_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") +_azure_client = AsyncAzureOpenAI( + azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + api_key=os.environ["AZURE_OPENAI_API_KEY"], + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), +) +_agent = Agent( + name="QAAgent", + instructions="You are a concise, helpful assistant.", + model=OpenAIChatCompletionsModel(model=_deployment, openai_client=_azure_client), +) + +# --- Microsoft 365 Agents SDK ------------------------------------------------ +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnContext, TurnState +from microsoft_agents.hosting.core.authorization import ( + AgentAuthConfiguration, + AnonymousTokenProvider, + ConnectionManager, +) + +_app_id = os.environ.get("MicrosoftAppId", "") +_app_password = os.environ.get("MicrosoftAppPassword", "") +_tenant_id = os.environ.get("MicrosoftAppTenantId", "") + +if _app_id and _app_password: + from microsoft_agents.authentication.msal import MsalConnectionManager + _connection_manager = MsalConnectionManager( + connections_configurations={ + "SERVICE_CONNECTION": AgentAuthConfiguration( + client_id=_app_id, + client_secret=_app_password, + tenant_id=_tenant_id or "common", + ) + } + ) +else: + _connection_manager = ConnectionManager( + provider_factory=lambda config: AnonymousTokenProvider(), + connections_configurations={"SERVICE_CONNECTION": AgentAuthConfiguration(anonymous_allowed=True)}, + ) + +STORAGE = MemoryStorage() +ADAPTER = CloudAdapter(connection_manager=_connection_manager) +AGENT_APP = AgentApplication[TurnState](storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState) -> None: + await context.send_activity("Hello! MS A365 auto-instrumentation (lab0) demo. Ask me anything.") + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, state: TurnState) -> None: + user_text = context.activity.text or "" + if not user_text.strip(): + await context.send_activity("Please send a message.") + return + + conv_id = context.activity.conversation.id if context.activity.conversation else "unknown" + channel_id = context.activity.channel_id or "unknown" + + request = Request( + content=user_text, + session_id=conv_id, + conversation_id=conv_id, + channel=Channel(name=channel_id), + ) + agent_details = AgentDetails(agent_id="m365-splunk-ao-demo", agent_name="M365 Splunk AO Demo Agent") + + with InvokeAgentScope.start( + request, InvokeAgentScopeDetails(), agent_details, + span_details=SpanDetails(parent_context=otel_context.get_current()), + ): + result = await Runner.run(_agent, input=user_text) + reply = result.final_output + + await context.send_activity(reply) diff --git a/examples/agent/microsoft-365-agents-sdk/app_manual_lab0.py b/examples/agent/microsoft-365-agents-sdk/app_manual_lab0.py new file mode 100644 index 0000000..0bc8543 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/app_manual_lab0.py @@ -0,0 +1,163 @@ +""" +app_manual_lab0.py — Manual instrumentation via SplunkAOLogger → Splunk AO lab0. + + SplunkAOLogger is used directly in the turn handler to build the trace hierarchy: + trace → invoke_agent M365 Splunk AO Demo Agent + agent_span → Agent workflow + agent_span → invoke_agent QAAgent + agent_span → turn + llm_span → chat gpt-4o-mini + + Session is created once per conversation (conv_id as external_id) so multi-turn + messages in the same Teams conversation are grouped under one session. + + Prerequisites: + Copy .env.example to .env and fill in: + SPLUNK_AO_REALM, SPLUNK_AO_O11Y_TOKEN, SPLUNK_AO_PROJECT, SPLUNK_AO_AGENT_STREAM + AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY + + Run: APP_MODE=manual_lab0 uv run python start_server.py + Test: npx @microsoft/m365agentsplayground (or send a POST to /api/messages) + Find: Splunk AO → Agent Stream: microsoft-365-agents-sdk → service: m365-manual-lab0 +""" + +import os +import time + +from dotenv import load_dotenv + +load_dotenv(override=False) + +os.environ.setdefault("OTEL_SERVICE_NAME", "m365-manual-lab0") + +# --- Splunk AO Logger -------------------------------------------------------- +from splunk_ao import SplunkAOLogger + +_logger = SplunkAOLogger( + project=os.environ.get("SPLUNK_AO_PROJECT"), + agent_stream=os.environ.get("SPLUNK_AO_AGENT_STREAM"), +) + +# conv_id → session_id; avoids a network round-trip on every turn +_session_cache: dict[str, str] = {} + +# --- Direct Azure OpenAI client ---------------------------------------------- +from openai import AzureOpenAI + +_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") +_azure_client = AzureOpenAI( + azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], + api_key=os.environ["AZURE_OPENAI_API_KEY"], + api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), +) + +# --- Microsoft 365 Agents SDK ------------------------------------------------ +from microsoft_agents.hosting.aiohttp import CloudAdapter +from microsoft_agents.hosting.core import AgentApplication, MemoryStorage, TurnContext, TurnState +from microsoft_agents.hosting.core.authorization import ( + AgentAuthConfiguration, + AnonymousTokenProvider, + ConnectionManager, +) + +_app_id = os.environ.get("MicrosoftAppId", "") +_app_password = os.environ.get("MicrosoftAppPassword", "") +_tenant_id = os.environ.get("MicrosoftAppTenantId", "") + +if _app_id and _app_password: + from microsoft_agents.authentication.msal import MsalConnectionManager + _connection_manager = MsalConnectionManager( + connections_configurations={ + "SERVICE_CONNECTION": AgentAuthConfiguration( + client_id=_app_id, + client_secret=_app_password, + tenant_id=_tenant_id or "common", + ) + } + ) +else: + _connection_manager = ConnectionManager( + provider_factory=lambda config: AnonymousTokenProvider(), + connections_configurations={"SERVICE_CONNECTION": AgentAuthConfiguration(anonymous_allowed=True)}, + ) + +STORAGE = MemoryStorage() +ADAPTER = CloudAdapter(connection_manager=_connection_manager) +AGENT_APP = AgentApplication[TurnState](storage=STORAGE, adapter=ADAPTER, connection_manager=_connection_manager) + + +@AGENT_APP.conversation_update("membersAdded") +async def on_members_added(context: TurnContext, state: TurnState) -> None: + await context.send_activity("Hello! MS 365 manual-instrumentation (lab0) demo. Ask me anything.") + + +@AGENT_APP.activity("message") +async def on_message(context: TurnContext, state: TurnState) -> None: + user_text = context.activity.text or "" + if not user_text.strip(): + await context.send_activity("Please send a message.") + return + + conv_id = context.activity.conversation.id if context.activity.conversation else "unknown" + channel_id = context.activity.channel_id or "unknown" + + # --- Session: one per conversation, server-side deduped by conv_id ------- + if conv_id not in _session_cache: + session_id = _logger.start_session( + external_id=conv_id, + metadata={"channel": channel_id}, + ) + _session_cache[conv_id] = session_id + _logger.set_session(_session_cache[conv_id]) + + # ------------------------------------------------------------------------- + # Trace hierarchy (mirrors auto-instrumented span tree): + # + # start_trace "invoke_agent M365 Splunk AO Demo Agent" + # add_agent_span "Agent workflow" + # add_agent_span "invoke_agent QAAgent" + # add_agent_span "turn" + # add_llm_span gpt-4o-mini + # conclude ← turn + # conclude ← invoke_agent QAAgent + # conclude ← Agent workflow + # conclude ← trace + # ------------------------------------------------------------------------- + t0 = time.monotonic_ns() + + _logger.start_trace(input=user_text, name="invoke_agent M365 Splunk AO Demo Agent") + _logger.add_agent_span(input=user_text, name="Agent workflow") + _logger.add_agent_span(input=user_text, name="invoke_agent QAAgent") + _logger.add_agent_span(input=user_text, name="turn") + + t_llm = time.monotonic_ns() + response = _azure_client.chat.completions.create( + model=_deployment, + messages=[ + {"role": "system", "content": "You are a concise, helpful assistant."}, + {"role": "user", "content": user_text}, + ], + ) + llm_elapsed_ns = time.monotonic_ns() - t_llm + + reply = response.choices[0].message.content or "" + usage = response.usage + + _logger.add_llm_span( + input=user_text, + output=reply, + model=_deployment, + num_input_tokens=usage.prompt_tokens if usage else None, + num_output_tokens=usage.completion_tokens if usage else None, + total_tokens=usage.total_tokens if usage else None, + duration_ns=llm_elapsed_ns, + ) + + total_elapsed_ns = time.monotonic_ns() - t0 + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # turn + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # invoke_agent QAAgent + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # Agent workflow + _logger.conclude(output=reply, duration_ns=total_elapsed_ns) # trace + _logger.flush() + + await context.send_activity(reply) diff --git a/examples/agent/microsoft-365-agents-sdk/pyproject.toml b/examples/agent/microsoft-365-agents-sdk/pyproject.toml new file mode 100644 index 0000000..4a41a62 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "microsoft-365-agents-sdk-example" +version = "0.1.0" +description = "Microsoft 365 Agents SDK + MS A365 Observability + Splunk AO Logger demo" +requires-python = ">=3.11" +dependencies = [ + "splunk-ao", + "openai>=1.0.0", + "opentelemetry-exporter-otlp-proto-http", + "microsoft-agents-hosting-aiohttp>=1.1.0", + "microsoft-agents-authentication-msal>=1.1.0", + "microsoft-agents-a365-observability-core>=1.0.0", + "microsoft-agents-a365-observability-hosting>=1.0.0", + "microsoft-agents-a365-observability-extensions-openai>=1.0.0", + "azure-identity>=1.19.0", + "python-dotenv>=1.0.0", +] + +[tool.uv.sources] +splunk-ao = { path = "../../..", editable = true } diff --git a/examples/agent/microsoft-365-agents-sdk/start_server.py b/examples/agent/microsoft-365-agents-sdk/start_server.py new file mode 100644 index 0000000..65b7b66 --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/start_server.py @@ -0,0 +1,51 @@ +""" +aiohttp server entry point for the Microsoft 365 Agents SDK example. + +Starts the agent on http://localhost:3978/api/messages. +Test locally with: npx @microsoft/m365agentsplayground + +APP_MODE=auto → app_auto.py (auto-instrumented, local collector) +APP_MODE=manual → app_manual.py (manual InferenceScope, local collector) +APP_MODE=auto_lab0 → app_auto_lab0.py (SplunkAOSpanProcessor, auto-instrumented, lab0/staging) +APP_MODE=manual_lab0 → app_manual_lab0.py (SplunkAOLogger, manual instrumentation, lab0/staging) +APP_MODE= → app.py (default) +""" + +import os + +from aiohttp.web import Application, Request, Response, run_app +from dotenv import load_dotenv + +load_dotenv(override=False) + +_mode = os.environ.get("APP_MODE", "").lower() +if _mode == "auto": + from app_auto import ADAPTER, AGENT_APP +elif _mode == "manual": + from app_manual import ADAPTER, AGENT_APP +elif _mode == "auto_lab0": + from app_auto_lab0 import ADAPTER, AGENT_APP +elif _mode == "manual_lab0": + from app_manual_lab0 import ADAPTER, AGENT_APP +else: + from app import ADAPTER, AGENT_APP + +from microsoft_agents.hosting.aiohttp import start_agent_process + + +async def messages(request: Request) -> Response: + return await start_agent_process(request, AGENT_APP, ADAPTER) + + +def main() -> None: + web_app = Application() + web_app.router.add_post("/api/messages", messages) + + port = int(os.environ.get("PORT", 3978)) + print(f"Microsoft 365 Agents SDK example running on http://localhost:{port}/api/messages") + print("Test with: npx @microsoft/m365agentsplayground") + run_app(web_app, host="localhost", port=port) + + +if __name__ == "__main__": + main() diff --git a/examples/agent/microsoft-365-agents-sdk/uv.toml b/examples/agent/microsoft-365-agents-sdk/uv.toml new file mode 100644 index 0000000..5bdd9aa --- /dev/null +++ b/examples/agent/microsoft-365-agents-sdk/uv.toml @@ -0,0 +1,3 @@ +[[index]] +url = "https://pypi.org/simple" +default = true