mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-07-16 01:53:37 +00:00
Compare commits
23 Commits
3-thesis-f
...
13-agentic
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a204ac2b3 | |||
|
|
816c1ebec6 | ||
| ca65f6a066 | |||
| 55a760713f | |||
| d42ab56c1e | |||
| 49c8ecacb0 | |||
| 5a2064c061 | |||
| dab95c6d63 | |||
| fdf8520b5b | |||
| a80ee0e80f | |||
|
|
707ce032cf | ||
|
|
ea11539f7d | ||
| a8ea68609c | |||
|
|
e04fb99f7b | ||
| 9c86ee337c | |||
| b3ab3e9a3a | |||
| 3c37ba7fa7 | |||
| 3cf56cfcb9 | |||
| 84b600c13c | |||
| 6cba6bb329 | |||
| 5b0afea651 | |||
| 7249a812f5 | |||
| 8072e9567e |
30
.github/workflows/pytest.yml
vendored
Normal file
30
.github/workflows/pytest.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Run Tests
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'experiments/**'
|
||||
- 'backend/**'
|
||||
- 'requirements.txt'
|
||||
- '.github/workflows/pytest.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'experiments/**'
|
||||
- 'backend/**'
|
||||
- 'requirements.txt'
|
||||
- '.github/workflows/pytest.yml'
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m venv .venv
|
||||
.venv/bin/pip install --upgrade pip
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
- name: Run tests
|
||||
run: .venv/bin/pytest -v
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
**/.env
|
||||
**/.venv
|
||||
PHANTOM.wiki/
|
||||
**/__pycache__
|
||||
**/.ipynb_checkpoints/
|
||||
**/.virtual_documents/
|
||||
**/__pycache__/
|
||||
**/.ipynb_checkpoints/
|
||||
**/session_*.svg
|
||||
**/*graph.svg
|
||||
paper/src/bib/auto
|
||||
|
||||
15
Makefile
15
Makefile
@@ -4,6 +4,10 @@ BUILDDIR := build
|
||||
TEX := main.tex
|
||||
JOBNAME := main
|
||||
PDF := paper/$(BUILDDIR)/$(JOBNAME).pdf
|
||||
VENV := .venv
|
||||
PYTHON := $(VENV)/bin/python
|
||||
PIP := $(VENV)/bin/pip
|
||||
PYTEST := $(VENV)/bin/pytest
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
@@ -35,5 +39,14 @@ clean:
|
||||
$(LATEXMK) -C -jobname=$(JOBNAME) -outdir=../$(BUILDDIR) || true
|
||||
rm -rf paper/$(BUILDDIR)/*
|
||||
|
||||
$(VENV):
|
||||
python3 -m venv $(VENV)
|
||||
$(PIP) install --upgrade pip
|
||||
|
||||
.PHONY: all pdf clean watch run.webapp
|
||||
install: $(VENV)
|
||||
$(PIP) install -r requirements.txt
|
||||
|
||||
test: $(VENV)
|
||||
$(PYTEST) -v
|
||||
|
||||
.PHONY: all pdf clean watch run.webapp install test
|
||||
|
||||
@@ -7,7 +7,7 @@ import uvicorn
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from kafka import KafkaProducer, KafkaAdminClient
|
||||
from kafka import KafkaProducer, KafkaAdminClient, KafkaConsumer
|
||||
from kafka.admin import NewTopic
|
||||
from kafka.errors import TopicAlreadyExistsError
|
||||
from dotenv import load_dotenv
|
||||
@@ -22,7 +22,7 @@ def get_producer() -> KafkaProducer:
|
||||
global _producer
|
||||
if _producer is None:
|
||||
host = os.getenv('KAFKA_HOST', 'localhost')
|
||||
port = os.getenv('KAFKA_PORT', '29092') # use internal broker port
|
||||
port = os.getenv('KAFKA_PORT', '9092')
|
||||
broker = f'{host}:{port}' if port else host
|
||||
print(f"[KAFKA_INIT] Connecting to broker: {broker}")
|
||||
_producer = KafkaProducer(
|
||||
@@ -61,7 +61,7 @@ app.add_middleware(
|
||||
async def startup_event():
|
||||
"""create kafka topics on startup"""
|
||||
host = os.getenv('KAFKA_HOST', 'localhost')
|
||||
port = os.getenv('KAFKA_PORT', '29092')
|
||||
port = os.getenv('KAFKA_PORT', '9092')
|
||||
broker = f'{host}:{port}'
|
||||
|
||||
try:
|
||||
@@ -125,10 +125,62 @@ async def ingest_logs(event: EventPayload):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/api/kafka/dump")
|
||||
def dump_logs():
|
||||
# TODO: implement a dump of logs of time period t_start to t_end (params of get)
|
||||
# OR: allow for params of last_n logs as a param - creating two modes of the dumping
|
||||
pass
|
||||
def dump_logs(
|
||||
last_n: Optional[int] = None,
|
||||
t_start: Optional[str] = None,
|
||||
t_end: Optional[str] = None
|
||||
):
|
||||
"""dump all messages from user-interactions topic
|
||||
|
||||
params:
|
||||
last_n: return only last n messages (default: all)
|
||||
t_start: filter by start timestamp iso format (future use)
|
||||
t_end: filter by end timestamp iso format (future use)
|
||||
"""
|
||||
host = os.getenv('KAFKA_HOST', 'localhost')
|
||||
port = os.getenv('KAFKA_PORT', '9092')
|
||||
broker = f'{host}:{port}'
|
||||
|
||||
try:
|
||||
consumer = KafkaConsumer(
|
||||
'user-interactions',
|
||||
bootstrap_servers=[broker],
|
||||
auto_offset_reset='earliest',
|
||||
enable_auto_commit=False,
|
||||
value_deserializer=lambda x: json.loads(x.decode('utf-8')),
|
||||
consumer_timeout_ms=5000
|
||||
)
|
||||
|
||||
events = []
|
||||
for msg in consumer:
|
||||
events.append(msg.value)
|
||||
|
||||
consumer.close()
|
||||
|
||||
# apply filters
|
||||
if t_start or t_end:
|
||||
# filter by timestamp range if provided
|
||||
filtered = []
|
||||
for e in events:
|
||||
ts = e.get('ts')
|
||||
if ts:
|
||||
if t_start and ts < t_start:
|
||||
continue
|
||||
if t_end and ts > t_end:
|
||||
continue
|
||||
filtered.append(e)
|
||||
events = filtered
|
||||
|
||||
if last_n and last_n > 0:
|
||||
events = events[-last_n:]
|
||||
|
||||
return {"success": True, "count": len(events), "data": events}
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[DUMP_ERROR] {e}")
|
||||
print(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
|
||||
|
||||
0
experiments/__init__.py
Normal file
0
experiments/__init__.py
Normal file
1
experiments/agents/__init__.py
Normal file
1
experiments/agents/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Agentic behavior runner for PHANTOM research platform."""
|
||||
44
experiments/agents/agent.py
Normal file
44
experiments/agents/agent.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from .base import Agent as BaseAgent
|
||||
from browser_use import Browser, Agent, ChatOpenAI
|
||||
from enum import Enum
|
||||
|
||||
class AgentTypes(str, Enum):
|
||||
GENERIC_BROWSER_USE_AGENT = "generic_browser_use_agent"
|
||||
|
||||
def _build_prompt(goal : str, environment_url : str) -> str:
|
||||
#TODO: Improve prompt engineering here and experiment with
|
||||
return f"""You are an autonomous agent tasked with achieving the following goal: {goal}
|
||||
You have access to a web browser to interact with the environment at {environment_url}.
|
||||
Use the browser to navigate, gather information, and perform actions necessary to accomplish your goal.
|
||||
Be thorough and ensure you complete the task fully."""
|
||||
|
||||
class GenericBrowserUseAgent(BaseAgent):
|
||||
def __init__(self,
|
||||
goal: str,
|
||||
url: str = "http://localhost:3000",
|
||||
timeout: int = 300,
|
||||
llm_model: str = "gpt-5-mini",
|
||||
headless: bool = True):
|
||||
super().__init__(goal, url, timeout)
|
||||
self.llm_model = ChatOpenAI(model=llm_model)
|
||||
self.browser = Browser(headless=headless)
|
||||
self.agent = Agent(task=_build_prompt(goal, url),
|
||||
llm=self.llm_model,
|
||||
browser=self.browser)
|
||||
async def act(self) -> str:
|
||||
self.result = await self.agent.run()
|
||||
# https://github.com/browser-use/browser-use/blob/main/browser_use/agent/views.py#L301
|
||||
return self.result.final_result()
|
||||
|
||||
def get_agent(agent_type: AgentTypes, **kwargs) -> Agent:
|
||||
if agent_type == AgentTypes.GENERIC_BROWSER_USE_AGENT:
|
||||
return GenericBrowserUseAgent(**kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown agent type: {agent_type}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
JTBD= "Name all the products on this site and try to find out more about each product by clicking into them (they might not open)"
|
||||
agent = get_agent(AgentTypes.GENERIC_BROWSER_USE_AGENT, goal=JTBD, url="http://localhost:3000/products", timeout=300)
|
||||
R=asyncio.run(agent.act())
|
||||
print(R)
|
||||
19
experiments/agents/base.py
Normal file
19
experiments/agents/base.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
class Agent(ABC):
|
||||
"""Base interface for browser automation agents"""
|
||||
|
||||
def __init__(self, goal: str, url: str = "http://localhost:3000", timeout: int = 300):
|
||||
self.goal = goal
|
||||
self.url = url
|
||||
self.timeout = timeout
|
||||
self.result: Optional[str] = None
|
||||
|
||||
@abstractmethod
|
||||
async def act(self) -> str:
|
||||
"""Execute goal and return result text"""
|
||||
pass
|
||||
|
||||
def final_result(self) -> Optional[str]:
|
||||
return self.result
|
||||
30
experiments/agents/test.py
Normal file
30
experiments/agents/test.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
from experiments.agents.agent import get_agent, AgentTypes
|
||||
import os
|
||||
|
||||
|
||||
def test_agent_init():
|
||||
agent = get_agent(AgentTypes.GENERIC_BROWSER_USE_AGENT, goal="test", url="http://example.com", timeout=100)
|
||||
assert agent.goal == "test"
|
||||
assert agent.url == "http://example.com"
|
||||
assert agent.timeout == 100
|
||||
|
||||
|
||||
def test_invalid_agent():
|
||||
with pytest.raises(ValueError):
|
||||
get_agent("invalid", goal="test")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif("OPENAI_API_KEY" not in os.environ, reason="OPENAI_API_KEY not set")
|
||||
async def test_agent_execution():
|
||||
agent = get_agent(AgentTypes.GENERIC_BROWSER_USE_AGENT, goal="get page title", url="https://example.com", timeout=60)
|
||||
|
||||
result = await agent.act()
|
||||
assert result
|
||||
assert agent.final_result()
|
||||
assert agent.final_result().history[-1].result[-1].is_done == True
|
||||
assert isinstance(result, str)
|
||||
assert "example" in result.lower()
|
||||
assert len(result) > 0
|
||||
File diff suppressed because it is too large
Load Diff
84
experiments/procesing/extract.py
Normal file
84
experiments/procesing/extract.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import pandas as pd
|
||||
import json
|
||||
import numpy as np
|
||||
import os
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
from sklearn.base import BaseEstimator, TransformerMixin
|
||||
load_dotenv()
|
||||
|
||||
BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:5000")
|
||||
N_PRICE_BUCKETS = 5
|
||||
|
||||
def get_data_from_kafka() -> pd.DataFrame:
|
||||
"""fetch all events from backend dump endpoint"""
|
||||
resp = requests.get(f"{BACKEND_URL}/api/kafka/dump")
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if not data.get('success') or not data.get('data'):
|
||||
return pd.DataFrame()
|
||||
|
||||
df = pd.DataFrame(data['data'])
|
||||
# explode metadata col json
|
||||
if 'metadata' in df.columns:
|
||||
df = df.join(pd.json_normalize(df.pop("metadata"), sep=".").add_prefix("metadata_"))
|
||||
df = df.dropna(subset=['eventName'])
|
||||
return df
|
||||
|
||||
|
||||
def join_with_experiments(df: pd.DataFrame) -> pd.DataFrame:
|
||||
# TODO: Get experiments db from supabase and join on session_id
|
||||
return df
|
||||
|
||||
|
||||
def augment_event_titles(df: pd.DataFrame) -> pd.DataFrame:
|
||||
# from taking standard view_item_page in eventName to view_item_page_{metadata_schema}
|
||||
# we want metadata schema to create product specific event names
|
||||
|
||||
# only create price buckets if we have enough unique prices
|
||||
if df["metadata_price"].notnull().sum() > 0:
|
||||
try:
|
||||
price_buckets = pd.qcut(
|
||||
df["metadata_price"],
|
||||
q=N_PRICE_BUCKETS,
|
||||
labels=[f"PB_{i+1}" for i in range(N_PRICE_BUCKETS)],
|
||||
duplicates='drop' # handle duplicate bin edges
|
||||
)
|
||||
except ValueError:
|
||||
# fallback: if still not enough unique values, use cut with fixed ranges or just use raw price
|
||||
price_buckets = df["metadata_price"].apply(lambda x: f"P_{int(x)}" if pd.notnull(x) else "")
|
||||
else:
|
||||
price_buckets = pd.Series([""] * len(df), index=df.index)
|
||||
|
||||
# metadata_schema: _product_id@price_bucket_{i} only if we have product metadata otherswise keep original event name
|
||||
# TODO: make this adaptive, if we have hover_over_title we append the title, if its view_page we say which page
|
||||
df["metadata_schema"] = np.where(
|
||||
df["productId"].notnull() & df["metadata_price"].notnull(),
|
||||
"_" + df["productId"].astype(str) + "@" + price_buckets.astype(str),
|
||||
""
|
||||
)
|
||||
df["eventName"] = df["eventName"] + df["metadata_schema"].astype(str)
|
||||
return df
|
||||
|
||||
|
||||
def extract() -> pd.DataFrame:
|
||||
df = get_data_from_kafka()
|
||||
df = join_with_experiments(df)
|
||||
df = augment_event_titles(df)
|
||||
return df
|
||||
|
||||
|
||||
class DataExtractor(BaseEstimator, TransformerMixin):
|
||||
def fit(self, X=None, y=None):
|
||||
return self
|
||||
|
||||
def transform(self, X=None):
|
||||
return extract()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
df = extract()
|
||||
print(df.head())
|
||||
print(df.tail())
|
||||
print(df.info())
|
||||
158
experiments/procesing/mapping.py
Normal file
158
experiments/procesing/mapping.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.base import BaseEstimator, TransformerMixin
|
||||
|
||||
def build_transition_prob_matrix(df: pd.DataFrame):
|
||||
df = df.dropna(subset=['eventName'])
|
||||
events = df['eventName'].tolist()
|
||||
labels = pd.Index(events).unique().tolist()
|
||||
idx = {e:i for i,e in enumerate(labels)}
|
||||
M = np.zeros((len(labels), len(labels)), dtype=float)
|
||||
for a, b in zip(events, events[1:]):
|
||||
M[idx[a], idx[b]] += 1
|
||||
row_sums = M.sum(axis=1, keepdims=True)
|
||||
with np.errstate(divide='ignore', invalid='ignore'):
|
||||
P = np.divide(M, row_sums, where=row_sums>0) # row-normalized
|
||||
return P, labels
|
||||
|
||||
# https://medium.com/data-science/time-series-data-markov-transition-matrices-7060771e362b
|
||||
from graphviz import Digraph
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
def _as_prob_df(matrix, labels=None):
|
||||
"""Return a square DataFrame with index=columns=labels."""
|
||||
if isinstance(matrix, pd.DataFrame):
|
||||
# Ensure square and aligned
|
||||
assert (matrix.index == matrix.columns).all(), "Index/columns must match."
|
||||
return matrix
|
||||
matrix = np.asarray(matrix, dtype=float)
|
||||
assert matrix.shape[0] == matrix.shape[1], "Matrix must be square."
|
||||
if labels is None:
|
||||
raise ValueError("labels are required when matrix is not a DataFrame")
|
||||
assert len(labels) == matrix.shape[0], "labels length must match matrix size."
|
||||
return pd.DataFrame(matrix, index=list(labels), columns=list(labels))
|
||||
|
||||
def _df_to_edgelist(P: pd.DataFrame, threshold=0.0, round_digits=2):
|
||||
"""Build weighted edges > threshold."""
|
||||
edges = []
|
||||
for src in P.index:
|
||||
for dst in P.columns:
|
||||
w = float(P.loc[src, dst])
|
||||
if w > threshold:
|
||||
edges.append((str(src), str(dst), f"{w:.{round_digits}f}"))
|
||||
return edges
|
||||
|
||||
def render_graph(fname, matrix, ls_index=None, threshold=0.0, fmt="svg", view=False):
|
||||
"""
|
||||
fname: output file stem (no extension)
|
||||
matrix: NumPy array or pandas DataFrame of transition PROBABILITIES
|
||||
ls_index: ordered labels (required if matrix is not a DataFrame)
|
||||
threshold: hide edges with weight <= threshold
|
||||
fmt: 'svg'|'png'|'pdf' etc.
|
||||
view: open after rendering
|
||||
"""
|
||||
P = _as_prob_df(matrix, labels=ls_index)
|
||||
edges = _df_to_edgelist(P, threshold=threshold)
|
||||
|
||||
g = Digraph(format=fmt)
|
||||
g.attr(rankdir="LR", size="30")
|
||||
g.attr("node", shape="circle")
|
||||
|
||||
# ensure isolated nodes appear
|
||||
for node in P.index:
|
||||
g.node(str(node), width="1", height="1")
|
||||
|
||||
for src, dst, label in edges:
|
||||
g.edge(src, dst, label=label)
|
||||
|
||||
g.render(fname, view=view, cleanup=True)
|
||||
return g
|
||||
|
||||
|
||||
class TransitionProbMatrixTransformer(BaseEstimator, TransformerMixin):
|
||||
def __init__(self, threshold=0.0):
|
||||
self.threshold = threshold
|
||||
self.P_ = None
|
||||
self.labels_ = None
|
||||
|
||||
def fit(self, X: pd.DataFrame, y=None):
|
||||
P, labels = build_transition_prob_matrix(X)
|
||||
self.P_ = P
|
||||
self.labels_ = labels
|
||||
return self
|
||||
|
||||
def transform(self, X: pd.DataFrame = None):
|
||||
return self.P_, self.labels_
|
||||
|
||||
def render(self, fname: str, fmt="svg", view=False):
|
||||
if self.P_ is None or self.labels_ is None:
|
||||
raise ValueError("Transformer has not been fitted yet.")
|
||||
return render_graph(
|
||||
fname,
|
||||
self.P_,
|
||||
ls_index=self.labels_,
|
||||
threshold=self.threshold,
|
||||
fmt=fmt,
|
||||
view=view
|
||||
)
|
||||
|
||||
|
||||
class SessionTransitionProbMatrixTransformer(BaseEstimator, TransformerMixin):
|
||||
def __init__(self, threshold=0.0, session_col='sessionId'):
|
||||
self.threshold = threshold
|
||||
self.session_col = session_col
|
||||
self.session_matrices_ = None
|
||||
|
||||
def fit(self, X: pd.DataFrame, y=None):
|
||||
if self.session_col not in X.columns:
|
||||
raise ValueError(f"Column '{self.session_col}' not found in DataFrame")
|
||||
|
||||
session_matrices = {}
|
||||
for session_id, grp in X.groupby(self.session_col):
|
||||
if len(grp) > 1: # need at least 2 events for transitions
|
||||
P, labels = build_transition_prob_matrix(grp)
|
||||
session_matrices[session_id] = {'matrix': P, 'labels': labels}
|
||||
|
||||
self.session_matrices_ = session_matrices
|
||||
return self
|
||||
|
||||
def transform(self, X: pd.DataFrame = None):
|
||||
if self.session_matrices_ is None:
|
||||
raise ValueError("Transformer has not been fitted yet.")
|
||||
return pd.Series(self.session_matrices_)
|
||||
|
||||
def render_session(self, session_id: str, fname: str, fmt="svg", view=False):
|
||||
if self.session_matrices_ is None:
|
||||
raise ValueError("Transformer has not been fitted yet.")
|
||||
if session_id not in self.session_matrices_:
|
||||
raise ValueError(f"Session '{session_id}' not found in fitted data.")
|
||||
|
||||
sess_data = self.session_matrices_[session_id]
|
||||
return render_graph(
|
||||
fname,
|
||||
sess_data['matrix'],
|
||||
ls_index=sess_data['labels'],
|
||||
threshold=self.threshold,
|
||||
fmt=fmt,
|
||||
view=view
|
||||
)
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
data = {
|
||||
'eventName': [
|
||||
'A', 'B', 'A', 'C', 'B', 'A', 'A', 'C', 'B', 'C',
|
||||
'A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C', 'A'
|
||||
]
|
||||
}
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
transformer = TransitionProbMatrixTransformer(threshold=0.1)
|
||||
transformer.fit(df)
|
||||
P, labels = transformer.transform(None)
|
||||
|
||||
print("Transition Probability Matrix:")
|
||||
print(pd.DataFrame(P, index=labels, columns=labels))
|
||||
|
||||
# Render the graph
|
||||
transformer.render("transition_graph", fmt="svg", view=False)
|
||||
19
experiments/procesing/pipeline.py
Normal file
19
experiments/procesing/pipeline.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from extract import DataExtractor
|
||||
from mapping import SessionTransitionProbMatrixTransformer, render_graph
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
steps = [
|
||||
('data_extraction', DataExtractor()),
|
||||
('transition_matrix', SessionTransitionProbMatrixTransformer(threshold=0.05)),
|
||||
]
|
||||
pipeline = Pipeline(steps)
|
||||
result = pipeline.fit_transform(None)
|
||||
print(f"Number of sessions: {len(result)}\n")
|
||||
|
||||
for session_id, sess_data in result.items():
|
||||
fname = f"session_{session_id}"
|
||||
render_graph(fname, sess_data['matrix'], ls_index=sess_data['labels'], threshold=0.05, fmt="svg", view=False)
|
||||
print(f"Rendered {fname}.svg")
|
||||
@@ -16,10 +16,11 @@ mkdir -p "$(dirname "$OUTPUT_FILE")"
|
||||
add_file() {
|
||||
local filepath="$1"
|
||||
local relpath="${filepath#$PROJECT_ROOT/}"
|
||||
local escaped_path="${relpath//_/\\_}"
|
||||
|
||||
# Add section header and code listing (no language-specific highlighting)
|
||||
echo "\\subsection{${relpath}}" >> "$OUTPUT_FILE"
|
||||
echo "\\begin{lstlisting}[caption={${relpath}}]" >> "$OUTPUT_FILE"
|
||||
echo "\\subsection{${escaped_path}}" >> "$OUTPUT_FILE"
|
||||
echo "\\begin{lstlisting}[caption={${escaped_path}}]" >> "$OUTPUT_FILE"
|
||||
cat "$filepath" >> "$OUTPUT_FILE"
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
echo "\\end{lstlisting}" >> "$OUTPUT_FILE"
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
(TeX-run-style-hooks
|
||||
"latex2e"
|
||||
"preamble"
|
||||
"chapters/01-intro"
|
||||
"chapters/02-literature-review"
|
||||
"chapters/03-methodology"
|
||||
"chapters/04-results"
|
||||
"chapters/05-discussion"
|
||||
"chapters/06-conclusion"
|
||||
"../build/concatenated_code"
|
||||
"acmart"
|
||||
"acmart10")
|
||||
(TeX-add-symbols
|
||||
'("footnotetextcopyrightpermission" 1))
|
||||
(LaTeX-add-labels
|
||||
"research"))
|
||||
'("footnotetextcopyrightpermission" 1)))
|
||||
:latex)
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
@techReport{,
|
||||
abstract = {We consider a single product revenue management problem where, given an initial inventory, the objective is to dynamically adjust prices over a finite sales horizon to maximize expected revenues. Realized demand is observed over time, but the underlying functional relationship between price and mean demand rate that governs these observations (otherwise known as the demand function or demand curve), is not known. We consider two instances of this problem: i.) a setting where the demand function is assumed to belong to a known parametric family with unknown parameter values; and ii.) a setting where the demand function is assumed to belong to a broad class of functions that need not admit any parametric representation. In each case we develop policies that learn the demand function "on the fly," and optimize prices based on that. The performance of these algorithms is measured in terms of the regret: the revenue loss relative to the maximal revenues that can be extracted when the demand function is known prior to the start of the selling season. We derive lower bounds on the regret that hold for any admissible pricing policy, and then show that our proposed algorithms achieve a regret that is "close" to this lower bound. The magnitude of the regret can be interpreted as the economic value of prior knowledge on the demand function; manifested as the revenue loss due to model uncertainty.},
|
||||
author = {Omar Besbes and Assaf Zeevi},
|
||||
journal = {Operations Research},
|
||||
keywords = {Revenue management,asymptotic analysis,estimation,exploration-exploitation,learning,pricing,value of information},
|
||||
title = {Dynamic Pricing Without Knowing the Demand Function: Risk Bounds and Near-Optimal Algorithms *}
|
||||
}
|
||||
|
||||
@misc{Ghaffary,
|
||||
author = {Shirin Ghaffary and Matt Day},
|
||||
note = {Updated 2025-11-05},
|
||||
title = {Amazon Sues to Stop Perplexity From Using AI Tool to Buy Stuff},
|
||||
url = {https://www.bloomberg.com/news/articles/2025-11-04/amazon-demands-perplexity-stop-ai-agent-from-making-purchases}
|
||||
}
|
||||
@phdthesis{,
|
||||
abstract = {Algorithmic pricing is an emerging business practice that uses computational algorithms to determine
|
||||
the prices of products and services based on a number of dynamic factors. The aim of this thesis is to
|
||||
draw attention to the existence of these business practices, and the ethical and social implications that
|
||||
derive from them, and then focus on what could be effective solutions to increase the well-being of
|
||||
the community.
|
||||
In Chapter 2 of the thesis, a general introduction to the topic will be made, starting from its history
|
||||
and its evolution over the years; Chapter 3 will examine the different types of pricing algorithms.
|
||||
Subsequently, in Chapter 4 we will analyze the sectors in which they are most applicable, and the
|
||||
relative advantages and disadvantages they bring with them, with a critical analysis of the trade-offs
|
||||
generated. The effect of algorithmic pricing on competition will be studied, considering how the
|
||||
ability of algorithms to adapt quickly to market conditions can foster anti-competitive practices, such
|
||||
as price discrimination. Later, in Chapter 5, we will look at the issue of price transparency and how
|
||||
the opacity of algorithms can make it difficult for consumers to understand the pricing process and
|
||||
assess whether they are receiving fair treatment.
|
||||
To address these ethical issues, several possible solutions will be brought to light, described in
|
||||
Chapter 6, which will focus on the role of the government, as a regulatory, of the end consumer, who
|
||||
must be encouraged to educate and inform himself about the use of these practices, and of the
|
||||
company, as responsible for making its customers aware and acting in compliance with government
|
||||
laws, for fair and non-discriminatory use.},
|
||||
author = {Fabio Salassa and Paolo Pautassi},
|
||||
school = {Politecnico di Torino},
|
||||
title = {Politecnico di Torino Algorithmic Pricing in the digital age "Ethical considerations on its economic and social implications, and an analysis of possible solutions to overcome its critical issues" Tutor: Candidate},
|
||||
url = {https://webthesis.biblio.polito.it/secure/31375/1/tesi.pdf}
|
||||
}
|
||||
@inproceedings{Mueller2019,
|
||||
author = {Jonas W Mueller and Vasilis Syrgkanis and Matt Taddy},
|
||||
booktitle = {Advances in Neural Information Processing Systems 32 (NeurIPS 2019)},
|
||||
pages = {15442-15452},
|
||||
title = {Low-Rank Bandit Methods for High-Dimensional Dynamic Pricing},
|
||||
url = {https://proceedings.neurips.cc/paper/2019/file/0a3df70393993583a13c0dd6686f3f32-Paper.pdf},
|
||||
year = {2019}
|
||||
}
|
||||
@article{Amjad2017,
|
||||
abstract = { In this paper, the question of interest is estimating true demand of a product at a given store location and time period in the retail environment based on a single noisy and potentially censored observation. To address this question, we introduce a %non-parametric framework to make inference from multiple time series. Somewhat surprisingly, we establish that the algorithm introduced for the purpose of "matrix completion" can be used to solve the relevant inference problem. Specifically, using the Universal Singular Value Thresholding (USVT) algorithm [7], we show that our estimator is consistent: the average mean squared error of the estimated average demand with respect to the true average demand goes to 0 as the number of store locations and time intervals increase to $\infty$. We establish naturally appealing properties of the resulting estimator both analytically as well as through a sequence of instructive simulations. Using a real dataset in retail (Walmart), we argue for the practical relevance of our approach. },
|
||||
author = {Muhammad J. Amjad and Devavrat Shah},
|
||||
doi = {10.1145/3154489},
|
||||
issue = {2},
|
||||
journal = {Proceedings of the ACM on Measurement and Analysis of Computing Systems},
|
||||
month = {12},
|
||||
pages = {1-28},
|
||||
publisher = {Association for Computing Machinery (ACM)},
|
||||
title = {Censored Demand Estimation in Retail},
|
||||
volume = {1},
|
||||
url = {https://par.nsf.gov/servlets/purl/10066022},
|
||||
year = {2017}
|
||||
}
|
||||
@article{Prez-Ricardo2025,
|
||||
abstract = {The study aims to explore tourists' booking intentions by analyzing the price elasticity of demand in tourist accommodations. This analysis should reveal how changes in price affect booking behavior across different customer segments, using online booking records. A dataset was compiled from 106 hotels in Malaga, Spain, comprising 27,910 online bookings sourced exclusively from hotel websites. To understand the price elasticity of demand, a simple log-log regression was applied, segmenting the data based on key revenue-related variables. Subsequently, a cluster segmentation was performed using the Elbow method and K-means algorithm to identify distinct market segments. The findings highlighted that Family Travelers and Short Stay Travelers segments exhibited elastic demand, indicating higher sensitivity to price fluctuations. In contrast, Early Bookers and Mid-Season Long Stayers demonstrated inelastic demand, with lower responsiveness to changes in tourist accommodation prices. The number of variables analyzed in this study, along with the cluster analysis, represent a novelty and contribute to the existing literature on market segmentation and price elasticity of demand. This integration enriches both fields of research, offering mutual benefits and deeper insights that enhance the understanding of booking intention and pricing strategies.},
|
||||
author = {Elizabeth del Carmen Pérez-Ricardo and Josefa García-Mestanza},
|
||||
doi = {10.1016/j.iedeen.2025.100271},
|
||||
issn = {24448834},
|
||||
issue = {1},
|
||||
journal = {European Research on Management and Business Economics},
|
||||
keywords = {Booking intention,Price elasticity,Tourist segmentation},
|
||||
month = {1},
|
||||
publisher = {European Academy of Management and Business Economics},
|
||||
title = {Exploring booking intentions through price elasticity of demand in tourism accommodations using large-scale data analytics},
|
||||
volume = {31},
|
||||
year = {2025}
|
||||
}
|
||||
@article{Iliou2021,
|
||||
author = {Christos Iliou and Theodoros Kostoulas and Theodora Tsikrika and Vasilis Katos and Stefanos Vrochidis and Ioannis Kompatsiaris},
|
||||
doi = {10.1145/3447815},
|
||||
issue = {3},
|
||||
journal = {Digital Threats: Research and Practice},
|
||||
pages = {1-26},
|
||||
title = {Detection of Advanced Web Bots by Combining Web Logs with Mouse Behavioural Biometrics},
|
||||
volume = {2},
|
||||
url = {https://dl.acm.org/doi/10.1145/3447815},
|
||||
year = {2021}
|
||||
}
|
||||
@article{ArnoudVdenBoer2015,
|
||||
author = {Arnoud V. den Boer},
|
||||
doi = {10.1016/j.sorms.2015.03.001},
|
||||
issue = {1},
|
||||
journal = {Surveys in Operations Research and Management Science},
|
||||
month = {6},
|
||||
pages = {1-18},
|
||||
title = {Dynamic pricing and learning: Historical origins, current research, and new directions},
|
||||
volume = {20},
|
||||
url = {https://www.sciencedirect.com/science/article/pii/S1876735415000021},
|
||||
year = {2015}
|
||||
}
|
||||
@article{Calvano2018,
|
||||
author = {Emilio Calvano and Giacomo Calzolari and Vincenzo Denicolo and Sergio Pastorello},
|
||||
doi = {10.2139/ssrn.3304991},
|
||||
journal = {SSRN Electronic Journal},
|
||||
title = {Artificial Intelligence, Algorithmic Pricing and Collusion},
|
||||
url = {https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3304991},
|
||||
year = {2018}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
\begin{document}
|
||||
|
||||
\title{First Proposal: Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms}
|
||||
\title{Pricing Heuristics Against Non-human Transaction Orchestration Mechanisms}
|
||||
|
||||
\author{Daniel Rösel}
|
||||
\email{daniel@alves.world}
|
||||
@@ -34,60 +34,19 @@ The primary objective of this thesis is to develop and validate pricing heuristi
|
||||
|
||||
\maketitle
|
||||
|
||||
\section{Preliminary literature review}
|
||||
|
||||
From very relevant news, the legal conflicts of agentic access to platforms have clearly indicated a need for prevention of secondary negative effects on ``legacy'' systems which power modern pricing systems \cite{Ghaffary}. Dynamic pricing algorithms rely on directly translating demand features $q$ to $\hat{p}$ new price assignments across a catalogue of products. This demand estimation does often take into account a small degree of error and noise from the data. However, adversarially introduced interactions, which are non-conducive to pricing optimization nor are a fully accurate representation of the driving human demand, have not been considered as part of the systems. Research such as \cite{Mueller2019} introduces very clear methodology for pricing algorithms backed by demand estimation for online pricing optimization which can be followed for proposing adjustments and improvements as highlighted in \ref{research}. Another often encountered demand distortion occurs through censored demand environments \cite{Amjad2017}.
|
||||
|
||||
Other efforts such as \cite{Calvano2018} explore ways of modeling the interactions between multiple pricing algorithms or agents which in an effort to maximize their reward drive the market to supra-competitive pricing which leaves the boundaries of the market equilibrium, creating a harmful effect on the customers by this process of algorithmic collusion. This harm can be directly translated to our setting where through interactions between two learners there is a potential of market destabilization.
|
||||
|
||||
|
||||
\section{Research question or objective} \label{research}
|
||||
|
||||
\begin{quote}
|
||||
How do agent-generated interactions contaminate demand functions in dynamic pricing algorithms, and how significantly does this contamination affect key performance indicators ($\Delta$)?
|
||||
\end{quote}
|
||||
The objectives are to gather data on how humans ($H$) and agents ($A$) interact with commerce platforms, and to identify the most reliable methodology for true demand estimation to fuel the dynamic pricing algorithm. This discrimination task can be accomplished through three distinct approaches:
|
||||
|
||||
\begin{enumerate}
|
||||
\item \textbf{Explicit filtering approach:} Decompose pipeline components and employ an estimator $P(A|s)$ (where $s$ represents session interaction data) to explicitly filter agent-generated interactions from the processing stream.
|
||||
|
||||
\item \textbf{Learned transformation approach:} Utilize a learned transformation on the product demand feature $B$, where $B = B_H + B_A$, with the goal of deriving a more representative demand feature $B_\text{clean} = B_H + W_\epsilon B_A$ that appropriately weights agent contributions.
|
||||
|
||||
\item \textbf{Reinforcement learning approach:} Frame the problem as a reinforcement learning task where interactions are modeled as environmental components, guiding the algorithm to learn an appropriate pricing policy that implicitly accounts for genuine human demand ($B_H$).
|
||||
\end{enumerate}
|
||||
|
||||
|
||||
\section{Execution plan with approximate calendar}
|
||||
|
||||
|
||||
This is a tentative execution plan for this research, keeping in mind a more agile approach rather than a waterfall-like set of goals and targets:
|
||||
|
||||
\begin{description}
|
||||
\item[November 2024:] Complete platform deployment for data collection and observations (70\% complete). Implement user authentication system with magic link invites to enable participant enrollment.
|
||||
|
||||
\item[December 2024:] Gather initial interaction data and explore the separability of distributions between human and agentic interaction patterns. Begin testing online algorithms for session-based pricing optimizations.
|
||||
|
||||
\item[January 2025:] Conduct controlled experiments comparing human versus agent execution of identical tasks. Establish behavioral signature models and quantify contamination impact ($\Delta$). Develop and validate the explicit filtering approach using $P(A|s)$ estimator.
|
||||
|
||||
\item[February 2025:] Design and train the learned transformation model for demand feature adjustment ($B_\text{clean}$). Implement reinforcement learning framework and train pricing policy that implicitly accounts for genuine human demand.
|
||||
|
||||
\item[March 2025:] Conduct comparative evaluation across all three proposed approaches. Finalize experimental results and perform statistical analysis of revenue recovery and KPI improvements.
|
||||
|
||||
\item[April 2025:] Internal review, revisions, and thesis documentation finalization. Prepare for final submission.
|
||||
\end{description}
|
||||
|
||||
\section{Desired measurable outcome or answer}
|
||||
|
||||
The first step is measuring how well we can separate human from agent session data. We can start with standard accuracy metrics as a baseline.
|
||||
What really matters for the larger picture is the economic impact of accurate demand estimation. We measure this through revenue leakage and revenue recovery. For benchmarking, we need to compare scenarios under default pricing policies versus adjusted ones - this gives us lower and upper bounds for our performance.
|
||||
Since we're also concerned with human-centric outcomes, we need to collect user friction ratings that compare more radical solutions (like CAPTCHAs) against minimal or no defenses.
|
||||
\input{chapters/01-intro}
|
||||
\input{chapters/02-literature-review}
|
||||
\input{chapters/03-methodology}
|
||||
\input{chapters/04-results}
|
||||
\input{chapters/05-discussion}
|
||||
\input{chapters/06-conclusion}
|
||||
|
||||
|
||||
\printbibliography
|
||||
|
||||
% \clearpage
|
||||
% \onecolumn
|
||||
% \appendix
|
||||
|
||||
\clearpage
|
||||
\onecolumn
|
||||
\appendix
|
||||
\input{../build/concatenated_code}
|
||||
|
||||
\end{document}
|
||||
|
||||
7
pytest.ini
Normal file
7
pytest.ini
Normal file
@@ -0,0 +1,7 @@
|
||||
[pytest]
|
||||
testpaths = experiments
|
||||
python_files = test*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
asyncio_mode = auto
|
||||
asyncio_default_fixture_loop_scope = function
|
||||
@@ -5,3 +5,8 @@ jupyter
|
||||
ipykernel
|
||||
matplotlib
|
||||
graphviz
|
||||
browser-use
|
||||
pytest
|
||||
pytest-asyncio
|
||||
uv
|
||||
scikit-learn
|
||||
|
||||
Reference in New Issue
Block a user